diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 5d6a86cef6..75792ebd28 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -990,9 +990,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { } catch (error) { console.error("Failed to handle auth callback:", error) vscode.window.showErrorMessage("Failed to log in to Cline") - // Clean up stored tokens on failure - await this.storeSecret("authToken", undefined) - await this.storeSecret("clineApiKey", undefined) + // Even on login failure, we preserve any existing tokens + // Only clear tokens on explicit logout } } diff --git a/src/services/auth/FirebaseAuthManager.ts b/src/services/auth/FirebaseAuthManager.ts index 6b2dd62ab9..58d37deefb 100644 --- a/src/services/auth/FirebaseAuthManager.ts +++ b/src/services/auth/FirebaseAuthManager.ts @@ -8,16 +8,18 @@ import { setPersistence, signInWithCustomToken, signOut, - AuthError as FirebaseAuthError + AuthError as FirebaseAuthError, } from "firebase/auth" import * as vscode from "vscode" import { ClineProvider } from "../../core/webview/ClineProvider" import { firebaseConfig } from "./config" enum AuthErrorType { - Network = 'network', - InvalidToken = 'invalid_token', - Other = 'other' + Network = "network", + InvalidToken = "invalid_token", + ExpiredToken = "expired_token", + TokenMismatch = "token_mismatch", + Other = "other", } interface AuthError { @@ -28,14 +30,14 @@ interface AuthError { interface RetryConfig { maxAttempts: number - baseDelay: number // in ms - maxDelay: number // in ms + baseDelay: number // in ms + maxDelay: number // in ms } const DEFAULT_RETRY_CONFIG: RetryConfig = { maxAttempts: 3, baseDelay: 1000, - maxDelay: 10000 + maxDelay: 10000, } export interface UserInfo { @@ -53,7 +55,7 @@ export class FirebaseAuthManager { constructor(provider: ClineProvider) { console.log("Initializing FirebaseAuthManager", { provider }) this.providerRef = new WeakRef(provider) - + try { const app = initializeApp(firebaseConfig) this.auth = getAuth(app) @@ -83,48 +85,64 @@ export class FirebaseAuthManager { console.error("Failed to set persistence after retries:", authError) // Don't throw - persistence failure shouldn't prevent auth initialization // But we should log it clearly for debugging - vscode.window.showErrorMessage("Warning: Failed to set authentication persistence. You may need to log in more frequently.") + vscode.window.showErrorMessage( + "Warning: Failed to set authentication persistence. You may need to log in more frequently.", + ) } } private classifyError(error: any): AuthError { console.log("Classifying auth error:", error) - + if (error?.code === "auth/network-request-failed") { return { type: AuthErrorType.Network, message: "Network error during authentication", - originalError: error + originalError: error, } } - - if (error?.code === "auth/invalid-custom-token" || - error?.code === "auth/custom-token-mismatch" || - error?.code === "auth/argument-error") { + + // Only consider a token invalid if it's explicitly invalid or malformed + if (error?.code === "auth/invalid-custom-token" || error?.code === "auth/argument-error") { return { type: AuthErrorType.InvalidToken, - message: "Invalid authentication token", - originalError: error + message: "Invalid authentication token format", + originalError: error, } } - + + // Token mismatch indicates the token might be for a different project/environment + if (error?.code === "auth/custom-token-mismatch") { + return { + type: AuthErrorType.TokenMismatch, + message: "Token mismatch - may be for different environment", + originalError: error, + } + } + + // Handle expired tokens separately + if (error?.code === "auth/user-token-expired") { + return { + type: AuthErrorType.ExpiredToken, + message: "Authentication token has expired", + originalError: error, + } + } + return { type: AuthErrorType.Other, message: error?.message || "Unknown authentication error", - originalError: error + originalError: error, } } private async delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)) + return new Promise((resolve) => setTimeout(resolve, ms)) } - private async retryWithBackoff( - operation: () => Promise, - config: RetryConfig = DEFAULT_RETRY_CONFIG - ): Promise { + private async retryWithBackoff(operation: () => Promise, config: RetryConfig = DEFAULT_RETRY_CONFIG): Promise { let lastError: any - + for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { try { console.log(`Attempting operation (attempt ${attempt}/${config.maxAttempts})`) @@ -132,32 +150,55 @@ export class FirebaseAuthManager { } catch (error) { lastError = error const authError = this.classifyError(error) - - // Don't retry for invalid token errors - if (authError.type === AuthErrorType.InvalidToken) { - console.log("Invalid token error - not retrying:", authError) + + // Don't retry for token-related errors + if ( + authError.type === AuthErrorType.InvalidToken || + authError.type === AuthErrorType.TokenMismatch || + authError.type === AuthErrorType.ExpiredToken + ) { + console.log("Token-related error - not retrying:", authError) throw error } - + if (attempt === config.maxAttempts) { console.error(`All ${config.maxAttempts} attempts failed:`, authError) throw error } - + // Calculate delay with exponential backoff - const delay = Math.min( - config.baseDelay * Math.pow(2, attempt - 1), - config.maxDelay - ) - + const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay) + console.log(`Attempt ${attempt} failed, retrying in ${delay}ms:`, authError) await this.delay(delay) } } - + throw lastError } + private async cleanupFailedAuth(provider: ClineProvider, error: AuthError) { + console.log("Cleaning up failed authentication state", { errorType: error.type }) + try { + // Clear user info since it's no longer valid + await provider.setUserInfo(undefined) + console.log("User info cleared") + + // We no longer clear the auth token here - it will only be cleared on explicit user logout + // Instead, we just sign out of Firebase if needed + if ( + error.type === AuthErrorType.InvalidToken || + error.type === AuthErrorType.TokenMismatch || + error.type === AuthErrorType.ExpiredToken + ) { + console.log("Auth error detected - signing out but preserving token") + await this.signOut() + } + } catch (cleanupError) { + console.error("Error during auth state cleanup:", cleanupError) + } + } + private async restoreSession() { console.log("Attempting to restore session") const provider = this.providerRef.deref() @@ -178,19 +219,14 @@ export class FirebaseAuthManager { } catch (error) { const authError = this.classifyError(error) console.error("Failed to restore session with custom token:", authError) - - if (authError.type === AuthErrorType.InvalidToken) { - // Only clean up state for invalid token errors - console.log("Invalid token detected - cleaning up auth state") - await this.cleanupFailedAuth(provider) - } else if (authError.type === AuthErrorType.Network) { - // For network errors, preserve the token and throw to allow retry + + // Clean up Firebase auth state but preserve the token + await this.cleanupFailedAuth(provider, authError) + + if (authError.type === AuthErrorType.Network) { + // For network errors, throw to allow retry console.log("Network error during session restore - will retry later") throw error - } else { - // For other errors, log but preserve the token - console.log("Non-critical error during session restore - preserving token for retry") - console.error("Error details:", authError) } } } else { @@ -198,46 +234,22 @@ export class FirebaseAuthManager { } } - private async cleanupFailedAuth(provider: ClineProvider) { - console.log("Cleaning up failed authentication state") - try { - // First clear user info since it's less critical - await provider.setUserInfo(undefined) - console.log("User info cleared") - - // Then clear auth token and sign out - await provider.setAuthToken(undefined) - console.log("Auth token cleared") - - await this.signOut() - console.log("Authentication state cleaned up successfully") - } catch (error) { - console.error("Error during auth state cleanup:", error) - // Even if cleanup fails, we want to ensure the token is cleared for security - try { - await provider.setAuthToken(undefined) - console.log("Auth token cleared after cleanup error") - } catch (tokenError) { - console.error("Critical: Failed to clear auth token:", tokenError) - // At this point, we've tried our best to clean up - } - } - } - getCurrentUser(): User | null { return this.auth.currentUser } private async handleAuthStateChange(user: User | null) { - console.log("Auth state changed", { - user: user ? { - uid: user.uid, - email: user.email, - emailVerified: user.emailVerified, - isAnonymous: user.isAnonymous, - metadata: user.metadata - } : null, - isInitialState: this.isInitialAuthState + console.log("Auth state changed", { + user: user + ? { + uid: user.uid, + email: user.email, + emailVerified: user.emailVerified, + isAnonymous: user.isAnonymous, + metadata: user.metadata, + } + : null, + isInitialState: this.isInitialAuthState, }) const provider = this.providerRef.deref() @@ -251,7 +263,7 @@ export class FirebaseAuthManager { console.log("User signed in", { userId: user.uid, lastLoginAt: user.metadata.lastSignInTime, - createdAt: user.metadata.creationTime + createdAt: user.metadata.creationTime, }) // Store public user info in state @@ -262,14 +274,10 @@ export class FirebaseAuthManager { } await provider.setUserInfo(userInfo) console.log("User info set in provider", { userInfo }) - } else if (!this.isInitialAuthState) { - // Only clear auth state if this isn't the initial null state + // Only clear user info if this isn't the initial null state console.log("User signed out (not initial state)") - await this.retryWithBackoff(async () => { - await provider.setAuthToken(undefined) - await provider.setUserInfo(undefined) - }) + await provider.setUserInfo(undefined) } else { console.log("Initial auth state is null, attempting session restore") this.isInitialAuthState = false @@ -292,11 +300,8 @@ export class FirebaseAuthManager { } catch (error) { console.error("Error handling auth state change:", error) // Attempt to clean up state if something went wrong - try { - await this.cleanupFailedAuth(provider) - } catch (cleanupError) { - console.error("Failed to cleanup after auth state change error:", cleanupError) - } + const authError = this.classifyError(error) + await this.cleanupFailedAuth(provider, authError) } }