Compare commits

...

4 Commits

Author SHA1 Message Date
Saoud Rizwan 22ed9e90c4 Add endpoint; remove refresh logic 2025-07-12 01:15:20 -07:00
Saoud Rizwan afe15f1acb Fix firebase token refreshing by using backend admin sdk endpoint approach 2025-07-11 21:03:36 -07:00
Saoud Rizwan 398e7586ee Fix implementation for refreshing 2025-07-11 13:33:08 -07:00
Saoud Rizwan 72a7f4423e Fix Firebase token refreshing 2025-07-11 13:25:50 -07:00
3 changed files with 68 additions and 93 deletions
+1 -1
View File
@@ -275,7 +275,7 @@ export class ClineAccountService {
throw error
} finally {
// Request a new authentication token
await this._authService.refreshAuth()
// await this._authService.refreshAuth()
}
}
}
-30
View File
@@ -220,7 +220,6 @@ export class AuthService {
this._authenticated = true
await this.sendAuthStatusUpdate()
this.setupAutoRefreshAuth()
return this._user
} catch (error) {
console.error("Error signing in with custom token:", error)
@@ -250,7 +249,6 @@ export class AuthService {
if (this._user) {
this._authenticated = true
await this.sendAuthStatusUpdate()
this.setupAutoRefreshAuth()
// Setup auto-refresh for the auth token
} else {
console.warn("No user found after restoring auth token")
@@ -265,34 +263,6 @@ export class AuthService {
}
}
/**
* Refreshes the authentication status and sends an update to all subscribers.
*/
async refreshAuth(): Promise<void> {
if (!this._user) {
console.warn("No user is authenticated, skipping auth refresh")
return
}
await this._provider.provider.refreshAuthToken()
this.sendAuthStatusUpdate()
}
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)
}
private async _autoRefreshAuth(): Promise<void> {
if (!this._user) {
console.warn("No user is authenticated, skipping auth refresh")
return
}
await this.refreshAuth()
this.setupAutoRefreshAuth() // Reschedule the next auto-refresh
}
/**
* Subscribe to authStatusUpdate events
* @param controller The controller instance
@@ -1,15 +1,14 @@
import { getSecret, storeSecret } from "@/core/storage/state"
import { ErrorService } from "@/services/error/ErrorService"
import axios from "axios"
import { initializeApp } from "firebase/app"
import {
AuthCredential,
GoogleAuthProvider,
GithubAuthProvider,
OAuthCredential,
GoogleAuthProvider,
User,
UserCredential,
getAuth,
signInWithCredential,
signInWithCustomToken,
signOut,
} from "firebase/auth"
import { ExtensionContext } from "vscode"
@@ -39,26 +38,6 @@ export class FirebaseAuthProvider {
return idToken
}
/**
* Gets the refresh token of the current user.
* @returns {Promise<string | null>} A promise that resolves to the refresh token of the current user, or null if no user is signed in.
*/
async getRefreshToken(): Promise<string | null> {
const user = getAuth().currentUser
const refreshToken = user ? user.refreshToken : null
return refreshToken
}
/**
* Refreshes the authentication token of the current user.
* @returns {Promise<string | null>} A promise that resolves to the refreshed authentication token of the current user, or null if no user is signed in.
*/
async refreshAuthToken(): Promise<string | null> {
const user = getAuth().currentUser
const idToken = user ? await user.getIdToken(true) : null
return idToken
}
/**
* Converts Firebase User object to a generic user object.
* @param user - The Firebase User object.
@@ -89,22 +68,6 @@ export class FirebaseAuthProvider {
})
}
/**
* Stores the authentication token using a provided token.
* @param token - The authentication token to store.
* @returns {Promise<User>} A promise that resolves with the authenticated user.
* @throws {Error} Throws an error if the storage fails.
*/
private async _storeAuthCredential(context: ExtensionContext, credential: AuthCredential): Promise<void> {
try {
await storeSecret(context, "clineAccountId", JSON.stringify(credential.toJSON()))
} catch (error) {
ErrorService.logMessage("Firebase store token error", "error")
ErrorService.logException(error)
throw error
}
}
/**
* Restores the authentication token using a provided token.
* @param token - The authentication token to restore.
@@ -112,30 +75,61 @@ export class FirebaseAuthProvider {
* @throws {Error} Throws an error if the restoration fails.
*/
async restoreAuthCredential(context: ExtensionContext): Promise<User | null> {
const credentialJSON = await getSecret(context, "clineAccountId")
if (!credentialJSON) {
const userRefreshToken = await getSecret(context, "clineAccountId")
if (!userRefreshToken) {
console.error("No stored authentication credential found.")
return null
}
try {
const credentialData: AuthCredential = OAuthCredential.fromJSON(credentialJSON) as AuthCredential
const userCredential = await this._signInWithCredential(credentialData)
return userCredential.user
} catch (error) {
ErrorService.logMessage("Firebase restore token error", "error")
ErrorService.logException(error)
throw error
}
}
// Step 1: Exchange refresh token for new access token using Firebase's secure token endpoint
// https://stackoverflow.com/questions/38233687/how-to-use-the-firebase-refreshtoken-to-reauthenticate/57119131#57119131
const firebaseApiKey = this._config.apiKey
const googleAccessTokenResponse = await axios.post(
`https://securetoken.googleapis.com/v1/token?key=${firebaseApiKey}`,
`grant_type=refresh_token&refresh_token=${userRefreshToken}`, // NOTE: we need to make sure to pass in the refreshToken and not the idToken JWT
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
)
async _signInWithCredential(credential: AuthCredential): Promise<UserCredential> {
const firebaseConfig = Object.assign({}, this._config)
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
try {
return await signInWithCredential(auth, credential)
// console.log("googleAccessTokenResponse", googleAccessTokenResponse)
// This returns an object with access_token, expires_in (3600), id_token (can be used as bearer token to authenticate requests, we'll use this in the future instead of firebase but need to be aware of how we use firebase sdk for e.g. user info like the profile image), project_id, refresh_token, token_type (always Bearer), and user_id
const googleAccessIdToken = googleAccessTokenResponse.data.id_token
// Step 2: Exchange access token for custom token from our backend (backend has the admin key, which firebase requires to create a custom token)
const customTokenResponse = await axios.post(
"https://api.cline.bot/api/v1/custom-token",
{}, // Empty request body
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${googleAccessIdToken}`,
},
},
)
const customToken = customTokenResponse.data.data.token
// Step 3: Use the custom token to sign in with Firebase and create a user object (we then use user.getIdToken() to refresh the access token periodically)
const firebaseConfig = Object.assign({}, this._config)
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
// signs user into firebase sdk internally
const user = (await signInWithCustomToken(auth, customToken)).user
return user
// let userObject = JSON.parse(credentialJSON)
// let user = User.
// userObject = User.constructor._fromJSON(auth, user2);
// const credentialData: AuthCredential = OAuthCredential.fromJSON(credentialJSON) as AuthCredential
// const userCredential = await this._signInWithCredential(context, credentialData)
// return userCredential.user
} catch (error) {
ErrorService.logMessage("Firebase sign-in with credential error", "error")
console.error("Firebase restore token error", error)
ErrorService.logMessage("Firebase restore token error", "error")
ErrorService.logException(error)
throw error
}
@@ -149,7 +143,6 @@ export class FirebaseAuthProvider {
async signIn(context: ExtensionContext, token: string, provider: string): Promise<User> {
try {
let credential
let userCredential
switch (provider) {
case "google":
credential = GoogleAuthProvider.credential(token)
@@ -160,9 +153,21 @@ export class FirebaseAuthProvider {
default:
throw new Error(`Unsupported provider: ${provider}`)
}
this._storeAuthCredential(context, credential)
userCredential = await this._signInWithCredential(credential)
return userCredential.user
// we've received the short-lived tokens from google/github, now we need to sign in to firebase with them
const firebaseConfig = Object.assign({}, this._config)
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
// this signs the user into firebase sdk internally
const user = (await signInWithCredential(auth, credential)).user
// store the long-lived refresh token in secret storage. this will be used in the future to re-signin the user using restoreAuthCredential above.
try {
await storeSecret(context, "clineAccountId", user.refreshToken)
} catch (error) {
ErrorService.logMessage("Firebase store token error", "error")
ErrorService.logException(error)
throw error
}
return user
} catch (error) {
ErrorService.logMessage("Firebase sign-in error", "error")
ErrorService.logException(error)