From 8ab768e77056ea2e651013723fded6378cd55b96 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Sat, 8 Feb 2025 14:33:17 -0800 Subject: [PATCH] more logging and updated authentication system to be more robust and reliable --- src/services/auth/FirebaseAuthManager.ts | 290 +++++++++++++++++++---- 1 file changed, 243 insertions(+), 47 deletions(-) diff --git a/src/services/auth/FirebaseAuthManager.ts b/src/services/auth/FirebaseAuthManager.ts index fd9e638109..6b2dd62ab9 100644 --- a/src/services/auth/FirebaseAuthManager.ts +++ b/src/services/auth/FirebaseAuthManager.ts @@ -8,11 +8,36 @@ import { setPersistence, signInWithCustomToken, signOut, + 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' +} + +interface AuthError { + type: AuthErrorType + message: string + originalError?: any +} + +interface RetryConfig { + maxAttempts: number + baseDelay: number // in ms + maxDelay: number // in ms +} + +const DEFAULT_RETRY_CONFIG: RetryConfig = { + maxAttempts: 3, + baseDelay: 1000, + maxDelay: 10000 +} + export interface UserInfo { displayName: string | null email: string | null @@ -28,22 +53,109 @@ export class FirebaseAuthManager { constructor(provider: ClineProvider) { console.log("Initializing FirebaseAuthManager", { provider }) this.providerRef = new WeakRef(provider) - const app = initializeApp(firebaseConfig) - this.auth = getAuth(app) - console.log("Firebase app initialized", { appConfig: firebaseConfig }) + + try { + const app = initializeApp(firebaseConfig) + this.auth = getAuth(app) + console.log("Firebase app initialized", { appConfig: firebaseConfig }) - // Set persistence to LOCAL to maintain auth state across sessions - setPersistence(this.auth, browserLocalPersistence) - .then(() => { + // Set persistence to LOCAL to maintain auth state across sessions + this.setupPersistence() + + // Auth state listener + const unsubscribe = onAuthStateChanged(this.auth, this.handleAuthStateChange.bind(this)) + this.disposables.push({ dispose: () => unsubscribe() }) + console.log("Auth state change listener added") + } catch (error) { + console.error("Error initializing FirebaseAuthManager:", error) + throw error + } + } + + private async setupPersistence() { + try { + await this.retryWithBackoff(async () => { + await setPersistence(this.auth, browserLocalPersistence) console.log("Firebase persistence set to LOCAL") }) - .catch((error) => { - console.error("Error setting persistence:", error) - }) + } catch (error) { + const authError = this.classifyError(error) + 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.") + } + } - // Auth state listener - onAuthStateChanged(this.auth, this.handleAuthStateChange.bind(this)) - console.log("Auth state change listener added") + 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 + } + } + + if (error?.code === "auth/invalid-custom-token" || + error?.code === "auth/custom-token-mismatch" || + error?.code === "auth/argument-error") { + return { + type: AuthErrorType.InvalidToken, + message: "Invalid authentication token", + originalError: error + } + } + + return { + type: AuthErrorType.Other, + message: error?.message || "Unknown authentication error", + originalError: error + } + } + + private async delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) + } + + 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})`) + return await operation() + } 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) + 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 + ) + + console.log(`Attempt ${attempt} failed, retrying in ${delay}ms:`, authError) + await this.delay(delay) + } + } + + throw lastError } private async restoreSession() { @@ -59,17 +171,26 @@ export class FirebaseAuthManager { if (storedToken) { console.log("Found stored custom token, attempting to restore session") try { - await this.signInWithCustomToken(storedToken) - console.log("Session restored successfully with custom token") + await this.retryWithBackoff(async () => { + await this.signInWithCustomToken(storedToken) + console.log("Session restored successfully with custom token") + }) } catch (error) { - console.error("Failed to restore session with custom token:", error) - await provider.setAuthToken(undefined) - await provider.setUserInfo(undefined) - // Attempt to sign out to ensure clean state - try { - await this.signOut() - } catch (signOutError) { - console.error("Error during cleanup after failed session restore:", signOutError) + 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 + 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 { @@ -77,45 +198,120 @@ 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 }) + 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() if (!provider) { - console.log("Provider reference lost") + console.error("Provider reference lost during auth state change") return } - if (user) { - console.log("User signed in", { userId: user.uid }) - // Store public user info in state - await provider.setUserInfo({ - displayName: user.displayName, - email: user.email, - photoURL: user.photoURL, - }) - console.log("User info set in provider", { user }) - } else if (!this.isInitialAuthState) { - // Only clear auth state if this isn't the initial null state - console.log("User signed out (not initial state)") - await provider.setAuthToken(undefined) - await provider.setUserInfo(undefined) - } else { - console.log("Initial auth state is null, attempting session restore") - this.isInitialAuthState = false - await this.restoreSession() - } + try { + if (user) { + console.log("User signed in", { + userId: user.uid, + lastLoginAt: user.metadata.lastSignInTime, + createdAt: user.metadata.creationTime + }) - await provider.postStateToWebview() - console.log("Webview state updated") + // Store public user info in state + const userInfo = { + displayName: user.displayName, + email: user.email, + photoURL: user.photoURL, + } + 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 + console.log("User signed out (not initial state)") + await this.retryWithBackoff(async () => { + await provider.setAuthToken(undefined) + await provider.setUserInfo(undefined) + }) + } else { + console.log("Initial auth state is null, attempting session restore") + this.isInitialAuthState = false + try { + await this.restoreSession() + } catch (error) { + const authError = this.classifyError(error) + if (authError.type === AuthErrorType.Network) { + console.log("Session restore failed due to network error - will retry on next auth state change") + // Keep isInitialAuthState true so we retry on next change + this.isInitialAuthState = true + } else { + console.error("Session restore failed with non-network error:", authError) + } + } + } + + await provider.postStateToWebview() + console.log("Webview state updated after auth state change") + } 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) + } + } } async signInWithCustomToken(token: string) { - console.log("Signing in with custom token", { token }) - await signInWithCustomToken(this.auth, token) + console.log("Signing in with custom token") + try { + await this.retryWithBackoff(async () => { + await signInWithCustomToken(this.auth, token) + console.log("Successfully signed in with custom token") + }) + } catch (error) { + const authError = this.classifyError(error) + console.error("Failed to sign in with custom token:", authError) + throw error + } } async signOut() {