Compare commits

...

1 Commits

Author SHA1 Message Date
celestial-vault c4d21fc71a fix secrets persistence 2025-08-01 16:26:12 -07:00
7 changed files with 44 additions and 38 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix auth login logout
+2 -2
View File
@@ -96,7 +96,7 @@ export class Controller {
this.context.extension?.packageJSON?.version ?? "1.0.0",
)
this.accountService = ClineAccountService.getInstance()
this.authService = AuthService.getInstance(context)
this.authService = AuthService.getInstance(this)
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
// Clean up legacy checkpoints
@@ -132,7 +132,7 @@ export class Controller {
async handleSignOut() {
try {
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
await storeSecret(this.context, "clineAccountId", undefined)
this.cacheService.setSecret("clineAccountId", undefined)
await updateGlobalState(this.context, "userInfo", undefined)
// Update API providers through cache service
+4 -1
View File
@@ -664,7 +664,10 @@ export async function activate(context: vscode.ExtensionContext) {
if (event.key === "clineAccountId") {
// Check if the secret was removed (logout) or added/updated (login)
const secretValue = await context.secrets.get("clineAccountId")
const authService = AuthService.getInstance(context)
const activeWebviewProvider = WebviewProvider.getVisibleInstance()
const controller = activeWebviewProvider?.controller
const authService = AuthService.getInstance(controller)
if (secretValue) {
// Secret was added or updated - restore auth info (login from another window)
authService?.restoreRefreshTokenAndRetrieveAuthInfo()
+15 -15
View File
@@ -56,7 +56,7 @@ export class AuthService {
protected _clineAuthInfo: ClineAuthInfo | null = null
protected _provider: { provider: FirebaseAuthProvider } | null = null
protected _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler<AuthState>]>()
protected _context: vscode.ExtensionContext
protected _controller: Controller
/**
* Creates an instance of AuthService.
@@ -64,7 +64,7 @@ export class AuthService {
* @param authProvider - Optional authentication provider to use.
* @param controller - Optional reference to the Controller instance.
*/
protected constructor(context: vscode.ExtensionContext, config: ServiceConfig, authProvider?: any) {
protected constructor(controller: Controller, config: ServiceConfig, authProvider?: any) {
const providerName = authProvider || "firebase"
this._config = Object.assign({ URI: DefaultClineAccountURI }, config)
@@ -95,7 +95,7 @@ export class AuthService {
this._setProvider(authProviders.find((authProvider) => authProvider.name === providerName).name)
this._context = context
this._controller = controller
}
/**
@@ -105,29 +105,29 @@ export class AuthService {
* @param controller - Optional reference to the Controller instance.
* @returns The singleton instance of AuthService.
*/
public static getInstance(context?: vscode.ExtensionContext, config?: ServiceConfig, authProvider?: any): AuthService {
public static getInstance(controller?: Controller, config?: ServiceConfig, authProvider?: any): AuthService {
if (!AuthService.instance) {
if (!context) {
if (!controller) {
console.warn("Extension context was not provided to AuthService.getInstance, using default context")
context = {} as vscode.ExtensionContext
controller = {} as Controller
}
if (process.env.E2E_TEST) {
// Use require instead of import to avoid circular dependency issues
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { AuthServiceMock } = require("./AuthServiceMock")
AuthService.instance = AuthServiceMock.getInstance(context, config || {}, authProvider)
AuthService.instance = AuthServiceMock.getInstance(controller, config || {}, authProvider)
} else {
AuthService.instance = new AuthService(context, config || {}, authProvider)
AuthService.instance = new AuthService(controller, config || {}, authProvider)
}
}
if (context !== undefined && AuthService.instance) {
AuthService.instance.context = context
if (controller !== undefined && AuthService.instance) {
AuthService.instance.controller = controller
}
return AuthService.instance!
}
set context(context: vscode.ExtensionContext) {
this._context = context
set controller(controller: Controller) {
this._controller = controller
}
get authProvider(): any {
@@ -228,7 +228,7 @@ export class AuthService {
}
try {
this._clineAuthInfo = await this._provider.provider.signIn(this._context, token, provider)
this._clineAuthInfo = await this._provider.provider.signIn(this._controller, token, provider)
this._authenticated = true
if (this._clineAuthInfo) {
@@ -248,7 +248,7 @@ export class AuthService {
* This is typically called when the user logs out.
*/
async clearAuthToken(): Promise<void> {
await storeSecret(this._context, "clineAccountId", undefined)
this._controller.cacheService.setSecret("clineAccountId", undefined)
}
/**
@@ -261,7 +261,7 @@ export class AuthService {
}
try {
this._clineAuthInfo = await this._provider.provider.retrieveClineAuthInfo(this._context)
this._clineAuthInfo = await this._provider.provider.retrieveClineAuthInfo(this._controller)
if (this._clineAuthInfo) {
this._authenticated = true
telemetryService.identifyAccount(this._clineAuthInfo.userInfo)
+11 -14
View File
@@ -4,10 +4,11 @@ import { clineEnvConfig } from "@/config"
import { WebviewProvider } from "@/core/webview"
import type { UserResponse } from "@/shared/ClineAccount"
import { AuthService, type ServiceConfig } from "./AuthService"
import { Controller } from "@/core/controller"
export class AuthServiceMock extends AuthService {
protected constructor(context: vscode.ExtensionContext, config: ServiceConfig, authProvider?: any) {
super(context, config, authProvider)
protected constructor(controller: Controller, config: ServiceConfig, authProvider?: any) {
super(controller, config, authProvider)
if (process?.env?.CLINE_ENVIRONMENT !== "local") {
throw new Error("AuthServiceMock should only be used in local environment for testing purposes.")
@@ -18,26 +19,22 @@ export class AuthServiceMock extends AuthService {
const providerName = "firebase"
this._setProvider(providerName)
this._context = context
this._controller = controller
}
/**
* Gets the singleton instance of AuthServiceMock.
*/
public static override getInstance(
context?: vscode.ExtensionContext,
config?: ServiceConfig,
authProvider?: any,
): AuthServiceMock {
public static override getInstance(controller?: Controller, config?: ServiceConfig, authProvider?: any): AuthServiceMock {
if (!AuthServiceMock.instance) {
if (!context) {
console.warn("Extension context was not provided to AuthServiceMock.getInstance, using default context")
context = {} as vscode.ExtensionContext
if (!controller) {
console.error("Extension controller was not provided to AuthServiceMock.getInstance")
throw new Error("Extension controller was not provided to AuthServiceMock.getInstance")
}
AuthServiceMock.instance = new AuthServiceMock(context, config || {}, authProvider)
AuthServiceMock.instance = new AuthServiceMock(controller, config || {}, authProvider)
}
if (context !== undefined) {
AuthServiceMock.instance.context = context
if (controller !== undefined) {
AuthServiceMock.instance.controller = controller
}
return AuthServiceMock.instance
}
@@ -7,6 +7,7 @@ import { ExtensionContext } from "vscode"
import { ClineAccountUserInfo, ClineAuthInfo } from "../AuthService"
import { jwtDecode } from "jwt-decode"
import { clineEnvConfig } from "@/config"
import { Controller } from "@/core/controller"
export class FirebaseAuthProvider {
private _config: any
@@ -41,8 +42,8 @@ export class FirebaseAuthProvider {
* @returns {Promise<User>} A promise that resolves with the authenticated user.
* @throws {Error} Throws an error if the restoration fails.
*/
async retrieveClineAuthInfo(context: ExtensionContext): Promise<ClineAuthInfo | null> {
const userRefreshToken = await getSecret(context, "clineAccountId")
async retrieveClineAuthInfo(controller: Controller): Promise<ClineAuthInfo | null> {
const userRefreshToken = controller.cacheService.getSecretKey("clineAccountId")
if (!userRefreshToken) {
console.error("No stored authentication credential found.")
return null
@@ -100,7 +101,7 @@ export class FirebaseAuthProvider {
* @returns {Promise<User>} A promise that resolves with the authenticated user.
* @throws {Error} Throws an error if the sign-in fails.
*/
async signIn(context: ExtensionContext, token: string, provider: string): Promise<ClineAuthInfo | null> {
async signIn(controller: Controller, token: string, provider: string): Promise<ClineAuthInfo | null> {
try {
let credential
switch (provider) {
@@ -123,7 +124,7 @@ export class FirebaseAuthProvider {
// store the long-lived refresh token in secret storage
try {
await storeSecret(context, "clineAccountId", userCredential.refreshToken)
controller.cacheService.setSecret("clineAccountId", userCredential.refreshToken)
} catch (error) {
ErrorService.logMessage("Firebase store token error", "error")
ErrorService.logException(error)
@@ -131,7 +132,7 @@ export class FirebaseAuthProvider {
}
// userCredential = await this._signInWithCredential(context, credential)
return await this.retrieveClineAuthInfo(context)
return await this.retrieveClineAuthInfo(controller)
} catch (error) {
ErrorService.logMessage("Firebase sign-in error", "error")
ErrorService.logException(error)
+1 -1
View File
@@ -262,7 +262,7 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
}
// Store the API key securely
await storeSecret(visibleWebview.controller.context, "clineAccountId", apiKey)
visibleWebview.controller.cacheService.setSecret("clineAccountId", apiKey)
visibleWebview.controller.cacheService.setApiConfiguration(updatedConfig)