diff --git a/frontend/src/desktop/services/authService.ts b/frontend/src/desktop/services/authService.ts index 51b3ebfd0..e11e8967a 100644 --- a/frontend/src/desktop/services/authService.ts +++ b/frontend/src/desktop/services/authService.ts @@ -3,6 +3,7 @@ import { listen } from '@tauri-apps/api/event'; import { open as shellOpen } from '@tauri-apps/plugin-shell'; import { connectionModeService } from '@app/services/connectionModeService'; import { tauriBackendService } from '@app/services/tauriBackendService'; +import { resetOAuthState } from '@proprietary/auth/oauthStorage'; import axios from 'axios'; import { DESKTOP_DEEP_LINK_CALLBACK, STIRLING_SAAS_URL, SUPABASE_KEY } from '@app/constants/connection'; @@ -112,8 +113,21 @@ export class AuthService { this.cachedToken = null; console.log('[Desktop AuthService] Cache invalidated'); - await invoke('clear_auth_token'); - localStorage.removeItem('stirling_jwt'); + // Best effort: clear Tauri keyring + try { + await invoke('clear_auth_token'); + console.log('[Desktop AuthService] Cleared Tauri keyring token'); + } catch (error) { + console.warn('[Desktop AuthService] Failed to clear Tauri keyring token', error); + } + + // Best effort: clear web storage + try { + localStorage.removeItem('stirling_jwt'); + console.log('[Desktop AuthService] Cleared localStorage token'); + } catch (error) { + console.warn('[Desktop AuthService] Failed to clear localStorage token', error); + } } subscribeToAuth(listener: (status: AuthStatus, userInfo: UserInfo | null) => void): () => void { @@ -257,9 +271,64 @@ export class AuthService { try { console.log('Logging out'); + // Best-effort backend logout so any server-side session/cookies are cleared + try { + const currentConfig = await connectionModeService.getCurrentConfig().catch(() => null); + const serverUrl = currentConfig?.server_config?.url; + const token = await this.getAuthToken(); + + if (serverUrl) { + const base = serverUrl.replace(/\/+$/, ''); + const headers: Record = {}; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + await axios + .post(`${base}/api/v1/auth/logout`, null, { headers, withCredentials: true }) + .catch((err) => { + console.warn('[Desktop AuthService] Backend logout failed via /api/v1/auth/logout', err); + }); + + // Also attempt framework logout endpoint to clear cookies/sessions + await axios.post(`${base}/logout`, null, { withCredentials: true }).catch(() => {}); + } + } catch (err) { + console.warn('[Desktop AuthService] Failed to call backend logout endpoint', err); + } + + // Clear any cookies the backend may have set (e.g., refresh/session) + try { + document.cookie.split(';').forEach(cookie => { + const eqPos = cookie.indexOf('='); + const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + if (name) { + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;`; + } + }); + } catch (err) { + console.warn('[Desktop AuthService] Failed to clear cookies during logout', err); + } + // Clear token from all storage locations await this.clearTokenEverywhere(); + // Clear any Supabase auth tokens that may persist in localStorage + try { + Object.keys(localStorage) + .filter((key) => key.startsWith('sb-') || key.includes('supabase')) + .forEach((key) => localStorage.removeItem(key)); + + // Clear any stored OAuth redirect state (used by SaaS auth) + try { + resetOAuthState(); + } catch (err) { + console.warn('[Desktop AuthService] Failed to clear OAuth redirect state', err); + } + } catch (error) { + console.warn('[Desktop AuthService] Failed to clear Supabase tokens', error); + } + // Clear user info from Tauri store await invoke('clear_user_info'); diff --git a/frontend/src/proprietary/auth/UseSession.tsx b/frontend/src/proprietary/auth/UseSession.tsx index 64e5baef0..4d5a771ef 100644 --- a/frontend/src/proprietary/auth/UseSession.tsx +++ b/frontend/src/proprietary/auth/UseSession.tsx @@ -79,6 +79,16 @@ export function AuthProvider({ children }: { children: ReactNode }) { console.debug('[Auth] Signed out successfully'); setSession(null); } + + // In desktop builds, also clear the desktop auth store/keyring to avoid auto-login on reload + if (typeof window !== 'undefined' && (window as any).__TAURI__) { + try { + const { authService } = await import('@app/services/authService'); + await authService.logout(); + } catch (desktopErr) { + console.warn('[Auth] Failed to clear desktop auth state after signOut', desktopErr); + } + } } catch (err) { console.error('[Auth] Unexpected error during sign out:', err); setError(err as AuthError); @@ -94,6 +104,15 @@ export function AuthProvider({ children }: { children: ReactNode }) { const initializeAuth = async () => { try { console.debug('[Auth] Initializing auth...'); + // Force-clear any stale cached token in desktop keyring on startup of auth page to prevent auto-login loops after logout + if (typeof window !== 'undefined' && (window as any).__TAURI__ && window.location.pathname.startsWith('/login')) { + try { + const { authService } = await import('@app/services/authService'); + await authService.logout(); + } catch (desktopErr) { + console.warn('[Auth] Failed to clear desktop auth state on login page init', desktopErr); + } + } // Skip config check entirely - let the app handle login state // The config will be fetched by useAppConfig when needed diff --git a/frontend/src/proprietary/auth/oauthStorage.ts b/frontend/src/proprietary/auth/oauthStorage.ts new file mode 100644 index 000000000..17597d5e1 --- /dev/null +++ b/frontend/src/proprietary/auth/oauthStorage.ts @@ -0,0 +1,34 @@ +/** + * Helper utilities for clearing cached OAuth redirect/session state + */ + +const OAUTH_REDIRECT_COOKIE = 'stirling_redirect_path'; + +/** + * Clear any persisted OAuth redirect path/cached state so the app + * does not automatically resume a previous OAuth session after logout. + */ +export function resetOAuthState(): void { + try { + // Remove redirect cookie + if (typeof document !== 'undefined') { + document.cookie = `${OAUTH_REDIRECT_COOKIE}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;SameSite=Lax`; + } + } catch (err) { + console.warn('[OAuthStorage] Failed to clear redirect cookie', err); + } + + // Remove any related localStorage entries we might have used + try { + if (typeof window !== 'undefined' && window.localStorage) { + window.localStorage.removeItem(OAUTH_REDIRECT_COOKIE); + window.localStorage.removeItem('oauth_redirect_path'); + } + } catch (err) { + console.warn('[OAuthStorage] Failed to clear OAuth localStorage', err); + } +} + +export default { + resetOAuthState, +}; diff --git a/frontend/src/proprietary/auth/springAuthClient.ts b/frontend/src/proprietary/auth/springAuthClient.ts index f77e34cb6..345dfc8b1 100644 --- a/frontend/src/proprietary/auth/springAuthClient.ts +++ b/frontend/src/proprietary/auth/springAuthClient.ts @@ -11,6 +11,8 @@ import apiClient from '@app/services/apiClient'; import { AxiosError } from 'axios'; import { BASE_PATH } from '@app/constants/app'; import { type OAuthProvider } from '@app/auth/oauthTypes'; +import { resetOAuthState } from '@proprietary/auth/oauthStorage'; +import { invoke } from '@tauri-apps/api/core'; // Helper to extract error message from axios error function getErrorMessage(error: unknown, fallback: string): string { @@ -296,6 +298,40 @@ class SpringAuthClient { // Clean up local storage localStorage.removeItem('stirling_jwt'); + try { + Object.keys(localStorage) + .filter((key) => key.startsWith('sb-') || key.includes('supabase')) + .forEach((key) => localStorage.removeItem(key)); + + // Clear any cached OAuth redirect/session state + resetOAuthState(); + } catch (err) { + console.warn('[SpringAuth] Failed to clear Supabase/local auth tokens', err); + } + + // Clear cookies that might hold refresh/session tokens + try { + document.cookie.split(';').forEach(cookie => { + const eqPos = cookie.indexOf('='); + const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim(); + if (name) { + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;`; + } + }); + } catch (err) { + console.warn('[SpringAuth] Failed to clear cookies on sign out', err); + } + + // If running in the desktop app, also clear persisted desktop credentials + if (typeof window !== 'undefined' && (window as any).__TAURI__) { + try { + await invoke('clear_auth_token'); + await invoke('clear_user_info'); + console.debug('[SpringAuth] Cleared desktop auth data (keyring + user info)'); + } catch (desktopError) { + console.warn('[SpringAuth] Failed to clear desktop auth data', desktopError); + } + } // Notify listeners this.notifyListeners('SIGNED_OUT', null);