From 6e493e9bc77e7c72e8c37ac13cfdd3b4f937d593 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sat, 14 Feb 2026 21:55:54 -0600 Subject: [PATCH] fix: detect and clear stale IndexedDB auth tokens (refs #190) Co-Authored-By: Claude Opus 4.6 --- frontend/src/core/auth/Auth0Provider.tsx | 28 +++++++++++++++++++- frontend/src/core/utils/indexeddb-storage.ts | 17 ++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/frontend/src/core/auth/Auth0Provider.tsx b/frontend/src/core/auth/Auth0Provider.tsx index 7678046..a1025dc 100644 --- a/frontend/src/core/auth/Auth0Provider.tsx +++ b/frontend/src/core/auth/Auth0Provider.tsx @@ -57,8 +57,9 @@ export const Auth0Provider: React.FC = ({ children }) => { // Component to inject token into API client with mobile support const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { getAccessTokenSilently, isAuthenticated, isLoading, user } = useAuth0(); + const { getAccessTokenSilently, isAuthenticated, isLoading, user, logout } = useAuth0(); const [retryCount, setRetryCount] = React.useState(0); + const validatingRef = React.useRef(false); // Basic component loading debug console.log('[TokenInjector] Component loaded'); @@ -116,6 +117,31 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => return null; }; + // Prevent stale session state when cached token is no longer valid + React.useEffect(() => { + if (!isAuthenticated || isLoading || validatingRef.current) return; + + const validateToken = async () => { + validatingRef.current = true; + try { + await getAccessTokenSilently({ cacheMode: 'off', timeoutInSeconds: 10 }); + } catch (error: any) { + const errorType = error?.error || error?.message || ''; + if (errorType.includes('login_required') || errorType.includes('consent_required') || + errorType.includes('invalid_grant')) { + console.warn('[Auth] Stale token detected, clearing auth state'); + const { indexedDBStorage } = await import('../utils/indexeddb-storage'); + await indexedDBStorage.clearAll(); + logout({ openUrl: false }); + return; + } + } + validatingRef.current = false; + }; + + validateToken(); + }, [isAuthenticated, isLoading]); + // Force authentication check for devices when user seems logged in but isAuthenticated is false React.useEffect(() => { const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); diff --git a/frontend/src/core/utils/indexeddb-storage.ts b/frontend/src/core/utils/indexeddb-storage.ts index 6ce8dd6..3014e50 100644 --- a/frontend/src/core/utils/indexeddb-storage.ts +++ b/frontend/src/core/utils/indexeddb-storage.ts @@ -157,6 +157,23 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache { } } + async clearAll(): Promise { + await this.initPromise; + if (!this.db) { + this.memoryCache.clear(); + return; + } + const tx = this.db.transaction(this.storeName, 'readwrite'); + tx.objectStore(this.storeName).clear(); + await new Promise((resolve, reject) => { + tx.oncomplete = () => { + this.memoryCache.clear(); + resolve(); + }; + tx.onerror = () => reject(tx.error); + }); + } + key(index: number): string | null { const keys = Array.from(this.memoryCache.keys()); return keys[index] || null;