Compare commits

...

4 Commits

Author SHA1 Message Date
arafatkatze 78b09b7f4b fix: token refresh logic with firebase 2025-07-10 10:52:24 -06:00
arafatkatze f4bb344e8b fix: token refresh logic with firebase 2025-07-10 10:51:43 -06:00
arafatkatze 75e7ce5bd4 fix: token refresh logic with firebase 2025-07-10 10:36:50 -06:00
arafatkatze 2ad44a97e4 fix: token refresh logic with firebase 2025-07-10 10:25:27 -06:00
6 changed files with 142 additions and 18 deletions
+24 -1
View File
@@ -79,7 +79,6 @@ export class Controller {
)
this.accountService = ClineAccountService.getInstance()
this.authService = AuthService.getInstance(context)
this.authService.restoreAuthToken()
// Clean up legacy checkpoints
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
@@ -87,6 +86,30 @@ export class Controller {
})
}
/**
* Initialize the controller after construction - must be called after creating the Controller
*/
async initialize(): Promise<void> {
await this.initializeAuthService()
}
/**
* Initializes the AuthService and handles token restoration
*/
private async initializeAuthService(): Promise<void> {
try {
await this.authService.restoreAuthToken()
console.log("Auth service initialization completed")
// Post state to webview after auth initialization
await this.postStateToWebview()
} catch (error) {
console.error("Auth service initialization failed:", error)
// Continue without authentication - user can sign in manually
await this.postStateToWebview()
}
}
private async getCurrentMode(): Promise<"plan" | "act"> {
return ((await getGlobalState(this.context, "mode")) as "plan" | "act" | undefined) || "act"
}
+5
View File
@@ -29,6 +29,11 @@ export abstract class WebviewProvider {
this.clientId = uuidv4()
WebviewProvider.clientIdMap.set(this, this.clientId)
this.controller = new Controller(context, outputChannel, (message) => this.postMessageToWebview(message), this.clientId)
// Initialize the controller asynchronously to prevent race conditions
this.controller.initialize().catch((error) => {
console.error("Failed to initialize controller:", error)
})
}
// Add a method to get the client ID
+4
View File
@@ -704,6 +704,10 @@ export async function deactivate() {
// Dispose all webview instances
await WebviewProvider.disposeAllInstances()
// Dispose AuthService singleton
const { AuthService } = await import("./services/auth/AuthService")
AuthService.dispose()
await telemetryService.sendCollectedEvents()
// Clean up test mode
+96 -12
View File
@@ -33,6 +33,8 @@ export class AuthService {
private _authNonce: string | null = null
private _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler]>()
private _context: vscode.ExtensionContext
private _refreshTimeoutId: NodeJS.Timeout | null = null
private _isInitialized: boolean = false
/**
* Creates an instance of AuthService.
@@ -246,27 +248,39 @@ export class AuthService {
* This is typically called when the extension is activated.
*/
async restoreAuthToken(): Promise<void> {
if (this._isInitialized) {
console.log("AuthService already initialized, skipping token restoration")
return
}
if (!this._provider || !this._provider.provider) {
throw new Error("Auth provider is not set")
console.error("Auth provider is not set")
this._isInitialized = true
return
}
try {
this._user = await this._provider.provider.restoreAuthCredential(this._context)
if (this._user) {
this._authenticated = true
this._isInitialized = true
console.log("Token restored successfully")
await this.sendAuthStatusUpdate()
this.setupAutoRefreshAuth()
// Setup auto-refresh for the auth token
} else {
console.warn("No user found after restoring auth token")
console.log("No stored authentication credentials found")
this._authenticated = false
this._user = null
this._isInitialized = true
}
} catch (error) {
console.error("Error restoring auth token:", error)
this._authenticated = false
this._user = null
return
this._isInitialized = true
// Clear corrupted credentials
await this.clearAuthToken()
}
}
@@ -279,23 +293,93 @@ export class AuthService {
return
}
await this._provider.provider.refreshAuthToken()
this.sendAuthStatusUpdate()
try {
await this._provider.provider.refreshAuthToken()
console.log("Token refreshed successfully")
await this.sendAuthStatusUpdate()
} catch (error) {
console.error("Token refresh failed:", error)
// Don't clear auth on single failure - let user re-authenticate manually if needed
}
}
private setupAutoRefreshAuth(): void {
// Set timeoutDuration to refresh the auth token 5 minutes before it expires
const timeoutDuration = Math.floor(this._user.stsTokenManager.expirationTime - 5 * 60000 - Date.now()) // Milliseconds until 5 minutes before expiration
setTimeout(() => this._autoRefreshAuth(), timeoutDuration)
// Clear any existing timeout
if (this._refreshTimeoutId) {
clearTimeout(this._refreshTimeoutId)
this._refreshTimeoutId = null
}
if (!this._user || !this._user.stsTokenManager?.expirationTime) {
console.warn("Cannot setup auto-refresh: invalid user or token")
return
}
// Simple timeout calculation with bounds check - refresh 5 minutes before expiration
const timeoutDuration = Math.max(0, Math.floor(this._user.stsTokenManager.expirationTime - 5 * 60000 - Date.now()))
if (timeoutDuration <= 0) {
console.log("Token expires very soon, refreshing immediately")
this._autoRefreshAuth().catch((error) => {
console.error("Immediate token refresh failed:", error)
})
return
}
console.log(`Scheduling token refresh in ${Math.round(timeoutDuration / 1000)}s`)
this._refreshTimeoutId = setTimeout(() => {
this._autoRefreshAuth().catch((error) => {
console.error("Scheduled token refresh failed:", error)
})
}, timeoutDuration)
}
private async _autoRefreshAuth(): Promise<void> {
if (!this._user) {
console.warn("No user is authenticated, skipping auth refresh")
console.warn("No user is authenticated, skipping auto-refresh")
return
}
await this.refreshAuth()
this.setupAutoRefreshAuth() // Reschedule the next auto-refresh
try {
await this.refreshAuth()
// Only reschedule on success
this.setupAutoRefreshAuth()
} catch (error) {
console.error("Auto-refresh failed:", error)
// Don't reschedule if refresh failed - user needs to re-authenticate
}
}
/**
* Cleanup method to clear timeouts and reset state
*/
private cleanup(): void {
if (this._refreshTimeoutId) {
clearTimeout(this._refreshTimeoutId)
this._refreshTimeoutId = null
}
}
/**
* Dispose method for proper cleanup when extension is deactivated
*/
dispose(): void {
this.cleanup()
this._activeAuthStatusUpdateSubscriptions.clear()
this._authenticated = false
this._user = null
this._isInitialized = false
}
/**
* Static method to dispose the singleton instance
*/
static dispose(): void {
if (AuthService.instance) {
AuthService.instance.dispose()
AuthService.instance = null
}
}
/**
@@ -107,21 +107,26 @@ export class FirebaseAuthProvider {
/**
* Restores the authentication token using a provided token.
* @param token - The authentication token to restore.
* @returns {Promise<User>} A promise that resolves with the authenticated user.
* @throws {Error} Throws an error if the restoration fails.
* @param context - The extension context for accessing stored credentials
* @returns {Promise<User | null>} A promise that resolves with the authenticated user or null if no credentials
* @throws {Error} Throws an error if the restoration fails
*/
async restoreAuthCredential(context: ExtensionContext): Promise<User | null> {
const credentialJSON = await getSecret(context, "clineAccountId")
if (!credentialJSON) {
console.error("No stored authentication credential found.")
console.log("No stored authentication credential found.")
return null
}
try {
const credentialData: AuthCredential = OAuthCredential.fromJSON(credentialJSON) as AuthCredential
const parsedCredential = JSON.parse(credentialJSON)
const credentialData: AuthCredential = OAuthCredential.fromJSON(parsedCredential) as AuthCredential
const userCredential = await this._signInWithCredential(credentialData)
console.log("Firebase credential restored successfully")
return userCredential.user
} catch (error) {
console.error("Firebase restore credential failed:", error)
ErrorService.logMessage("Firebase restore token error", "error")
ErrorService.logException(error)
throw error
+3
View File
@@ -20,6 +20,9 @@ async function main() {
hostProviders.initializeHostProviders(createWebview, new ExternalHostBridgeClientManager())
activate(extensionContext)
const controller = new Controller(extensionContext, outputChannel, postMessage, uuidv4())
controller.initialize().catch((error) => {
console.error("Failed to initialize controller:", error)
})
startProtobusService(controller)
}