From 81000ed58cf5c05c1afbcc427f2484ce97fadfb1 Mon Sep 17 00:00:00 2001 From: John Richmond <5629+jr@users.noreply.github.com> Date: Sat, 31 May 2025 21:23:21 -0700 Subject: [PATCH 1/3] Move client token + session id to shared secret --- packages/cloud/package.json | 3 +- packages/cloud/src/AuthService.ts | 108 ++++++++++++++++++------------ pnpm-lock.yaml | 3 + 3 files changed, 71 insertions(+), 43 deletions(-) diff --git a/packages/cloud/package.json b/packages/cloud/package.json index 7a15f4f0e9..375838dc95 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -13,7 +13,8 @@ "dependencies": { "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", - "axios": "^1.7.4" + "axios": "^1.7.4", + "zod": "^3.24.2" }, "devDependencies": { "@roo-code/config-eslint": "workspace:^", diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/AuthService.ts index f19653a78b..b50fd82fc6 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/AuthService.ts @@ -3,6 +3,7 @@ import EventEmitter from "events" import axios from "axios" import * as vscode from "vscode" +import { z } from "zod" import type { CloudUserInfo } from "@roo-code/types" @@ -15,8 +16,14 @@ export interface AuthServiceEvents { "user-info": [data: { userInfo: CloudUserInfo }] } -const CLIENT_TOKEN_KEY = "clerk-client-token" -const SESSION_ID_KEY = "clerk-session-id" +const authCredentialsSchema = z.object({ + clientToken: z.string().min(1, "Client token cannot be empty"), + sessionId: z.string().min(1, "Session ID cannot be empty"), +}) + +type AuthCredentials = z.infer + +const AUTH_CREDENTIALS_KEY = "clerk-auth-credentials" const AUTH_STATE_KEY = "clerk-auth-state" type AuthState = "initializing" | "logged-out" | "active-session" | "inactive-session" @@ -26,9 +33,8 @@ export class AuthService extends EventEmitter { private timer: RefreshTimer private state: AuthState = "initializing" - private clientToken: string | null = null + private credentials: AuthCredentials | null = null private sessionToken: string | null = null - private sessionId: string | null = null private userInfo: CloudUserInfo | null = null constructor(context: vscode.ExtensionContext) { @@ -60,19 +66,16 @@ export class AuthService extends EventEmitter { } try { - this.clientToken = (await this.context.secrets.get(CLIENT_TOKEN_KEY)) || null - this.sessionId = this.context.globalState.get(SESSION_ID_KEY) || null + const credentials = await this.loadCredentials() - // Determine initial state. - if (!this.clientToken || !this.sessionId) { - // TODO: it may be possible to get a new session with the client, - // but the obvious Clerk endpoints don't support that. + if (credentials) { + this.credentials = credentials + this.state = "inactive-session" + this.timer.start() + } else { const previousState = this.state this.state = "logged-out" this.emit("logged-out", { previousState }) - } else { - this.state = "inactive-session" - this.timer.start() } console.log(`[auth] Initialized with state: ${this.state}`) @@ -82,6 +85,32 @@ export class AuthService extends EventEmitter { } } + private async storeCredentials(credentials: AuthCredentials): Promise { + await this.context.secrets.store(AUTH_CREDENTIALS_KEY, JSON.stringify(credentials)) + } + + private async loadCredentials(): Promise { + const credentialsJson = await this.context.secrets.get(AUTH_CREDENTIALS_KEY) + if (!credentialsJson) return null + + try { + const parsedJson = JSON.parse(credentialsJson) + // Validate using Zod schema + return authCredentialsSchema.parse(parsedJson) + } catch (error) { + if (error instanceof z.ZodError) { + console.error("[auth] Invalid credentials format:", error.errors) + } else { + console.error("[auth] Failed to parse stored credentials:", error) + } + return null + } + } + + private async clearCredentials(): Promise { + await this.context.secrets.delete(AUTH_CREDENTIALS_KEY) + } + /** * Start the login process * @@ -132,13 +161,11 @@ export class AuthService extends EventEmitter { throw new Error("Invalid state parameter. Authentication request may have been tampered with.") } - const { clientToken, sessionToken, sessionId } = await this.clerkSignIn(code) + const { credentials, sessionToken } = await this.clerkSignIn(code) - await this.context.secrets.store(CLIENT_TOKEN_KEY, clientToken) - await this.context.globalState.update(SESSION_ID_KEY, sessionId) + await this.storeCredentials(credentials) - this.clientToken = clientToken - this.sessionId = sessionId + this.credentials = credentials this.sessionToken = sessionToken const previousState = this.state @@ -168,23 +195,20 @@ export class AuthService extends EventEmitter { try { this.timer.stop() - await this.context.secrets.delete(CLIENT_TOKEN_KEY) - await this.context.globalState.update(SESSION_ID_KEY, undefined) + await this.clearCredentials() await this.context.globalState.update(AUTH_STATE_KEY, undefined) - const oldClientToken = this.clientToken - const oldSessionId = this.sessionId + const oldCredentials = this.credentials - this.clientToken = null + this.credentials = null this.sessionToken = null - this.sessionId = null this.userInfo = null const previousState = this.state this.state = "logged-out" this.emit("logged-out", { previousState }) - if (oldClientToken && oldSessionId) { - await this.clerkLogout(oldClientToken, oldSessionId) + if (oldCredentials) { + await this.clerkLogout(oldCredentials) } this.fetchUserInfo() @@ -228,8 +252,8 @@ export class AuthService extends EventEmitter { * This method refreshes the session token using the client token. */ private async refreshSession(): Promise { - if (!this.sessionId || !this.clientToken) { - console.log("[auth] Cannot refresh session: missing session ID or token") + if (!this.credentials) { + console.log("[auth] Cannot refresh session: missing credentials") this.state = "inactive-session" return } @@ -245,7 +269,7 @@ export class AuthService extends EventEmitter { } private async fetchUserInfo(): Promise { - if (!this.clientToken) { + if (!this.credentials) { return } @@ -262,9 +286,7 @@ export class AuthService extends EventEmitter { return this.userInfo } - private async clerkSignIn( - ticket: string, - ): Promise<{ clientToken: string; sessionToken: string; sessionId: string }> { + private async clerkSignIn(ticket: string): Promise<{ credentials: AuthCredentials; sessionToken: string }> { const formData = new URLSearchParams() formData.append("strategy", "ticket") formData.append("ticket", ticket) @@ -284,14 +306,14 @@ export class AuthService extends EventEmitter { } // 4. Find the session using created_session_id and extract the JWT. - const createdSessionId = response.data?.response?.created_session_id + const sessionId = response.data?.response?.created_session_id - if (!createdSessionId) { + if (!sessionId) { throw new Error("No session ID found in the response") } // Find the session in the client sessions array. - const session = response.data?.client?.sessions?.find((s: { id: string }) => s.id === createdSessionId) + const session = response.data?.client?.sessions?.find((s: { id: string }) => s.id === sessionId) if (!session) { throw new Error("Session not found in the response") @@ -304,7 +326,9 @@ export class AuthService extends EventEmitter { throw new Error("Session does not have a token") } - return { clientToken, sessionToken, sessionId: session.id } + const credentials = authCredentialsSchema.parse({ clientToken, sessionId }) + + return { credentials, sessionToken } } private async clerkCreateSessionToken(): Promise { @@ -312,12 +336,12 @@ export class AuthService extends EventEmitter { formData.append("_is_native", "1") const response = await axios.post( - `${getClerkBaseUrl()}/v1/client/sessions/${this.sessionId}/tokens`, + `${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`, formData, { headers: { "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Bearer ${this.clientToken}`, + Authorization: `Bearer ${this.credentials!.clientToken}`, "User-Agent": this.userAgent(), }, }, @@ -335,7 +359,7 @@ export class AuthService extends EventEmitter { private async clerkMe(): Promise { const response = await axios.get(`${getClerkBaseUrl()}/v1/me`, { headers: { - Authorization: `Bearer ${this.clientToken}`, + Authorization: `Bearer ${this.credentials!.clientToken}`, "User-Agent": this.userAgent(), }, }) @@ -362,13 +386,13 @@ export class AuthService extends EventEmitter { return userInfo } - private async clerkLogout(clientToken: string, sessionId: string): Promise { + private async clerkLogout(credentials: AuthCredentials): Promise { const formData = new URLSearchParams() formData.append("_is_native", "1") - await axios.post(`${getClerkBaseUrl()}/v1/client/sessions/${sessionId}/remove`, formData, { + await axios.post(`${getClerkBaseUrl()}/v1/client/sessions/${credentials.sessionId}/remove`, formData, { headers: { - Authorization: `Bearer ${clientToken}`, + Authorization: `Bearer ${credentials.clientToken}`, "User-Agent": this.userAgent(), }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 672ec49553..09ebf6ae25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,6 +129,9 @@ importers: axios: specifier: ^1.7.4 version: 1.9.0 + zod: + specifier: ^3.24.2 + version: 3.24.4 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ From 9a30535852d61b13304306774870a4ddff556a51 Mon Sep 17 00:00:00 2001 From: John Richmond <5629+jr@users.noreply.github.com> Date: Sat, 31 May 2025 22:12:28 -0700 Subject: [PATCH 2/3] Add secret event handling --- packages/cloud/src/AuthService.ts | 110 ++++++++++++++++++------------ 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/AuthService.ts index b50fd82fc6..a16f17a2d5 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/AuthService.ts @@ -53,6 +53,55 @@ export class AuthService extends EventEmitter { }) } + private async handleCredentialsChange(): Promise { + try { + const credentials = await this.loadCredentials() + + if (credentials) { + if ( + this.credentials === null || + this.credentials.clientToken !== credentials.clientToken || + this.credentials.sessionId !== credentials.sessionId + ) { + this.transitionToInactiveSession(credentials) + } + } else { + if (this.state !== "logged-out") { + this.transitionToLoggedOut() + } + } + } catch (error) { + console.error("[auth] Error handling credentials change:", error) + } + } + + private transitionToLoggedOut(): void { + this.timer.stop() + + const previousState = this.state + + this.credentials = null + this.sessionToken = null + this.userInfo = null + this.state = "logged-out" + + this.emit("logged-out", { previousState }) + + console.log("[auth] Transitioned to logged-out state") + } + + private transitionToInactiveSession(credentials: AuthCredentials): void { + this.credentials = credentials + this.state = "inactive-session" + + this.sessionToken = null + this.userInfo = null + + this.timer.start() + + console.log("[auth] Transitioned to inactive-session state") + } + /** * Initialize the auth state * @@ -65,24 +114,15 @@ export class AuthService extends EventEmitter { return } - try { - const credentials = await this.loadCredentials() + this.handleCredentialsChange() - if (credentials) { - this.credentials = credentials - this.state = "inactive-session" - this.timer.start() - } else { - const previousState = this.state - this.state = "logged-out" - this.emit("logged-out", { previousState }) - } - - console.log(`[auth] Initialized with state: ${this.state}`) - } catch (error) { - console.error(`[auth] Error initializing AuthService: ${error}`) - this.state = "logged-out" - } + this.context.subscriptions.push( + this.context.secrets.onDidChange((e) => { + if (e.key === AUTH_CREDENTIALS_KEY) { + this.handleCredentialsChange() + } + }), + ) } private async storeCredentials(credentials: AuthCredentials): Promise { @@ -95,7 +135,6 @@ export class AuthService extends EventEmitter { try { const parsedJson = JSON.parse(credentialsJson) - // Validate using Zod schema return authCredentialsSchema.parse(parsedJson) } catch (error) { if (error instanceof z.ZodError) { @@ -161,20 +200,10 @@ export class AuthService extends EventEmitter { throw new Error("Invalid state parameter. Authentication request may have been tampered with.") } - const { credentials, sessionToken } = await this.clerkSignIn(code) + const { credentials } = await this.clerkSignIn(code) await this.storeCredentials(credentials) - this.credentials = credentials - this.sessionToken = sessionToken - - const previousState = this.state - this.state = "active-session" - this.emit("active-session", { previousState }) - this.timer.start() - - this.fetchUserInfo() - vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud") console.log("[auth] Successfully authenticated with Roo Code Cloud") } catch (error) { @@ -192,27 +221,21 @@ export class AuthService extends EventEmitter { * This method removes all stored tokens and stops the refresh timer. */ public async logout(): Promise { - try { - this.timer.stop() + const oldCredentials = this.credentials + try { + // Clear credentials from storage - onDidChange will handle state transitions await this.clearCredentials() await this.context.globalState.update(AUTH_STATE_KEY, undefined) - const oldCredentials = this.credentials - - this.credentials = null - this.sessionToken = null - this.userInfo = null - const previousState = this.state - this.state = "logged-out" - this.emit("logged-out", { previousState }) - if (oldCredentials) { - await this.clerkLogout(oldCredentials) + try { + await this.clerkLogout(oldCredentials) + } catch (error) { + console.error("[auth] Error calling clerkLogout:", error) + } } - this.fetchUserInfo() - vscode.window.showInformationMessage("Logged out from Roo Code Cloud") console.log("[auth] Logged out from Roo Code Cloud") } catch (error) { @@ -263,6 +286,7 @@ export class AuthService extends EventEmitter { this.state = "active-session" if (previousState !== "active-session") { + console.log("[auth] Transitioned to active-session state") this.emit("active-session", { previousState }) this.fetchUserInfo() } From 899f9530afae7d0e981869052f05258625e6fcc9 Mon Sep 17 00:00:00 2001 From: John Richmond <5629+jr@users.noreply.github.com> Date: Sun, 1 Jun 2025 00:29:28 -0700 Subject: [PATCH 3/3] Update packages/cloud/src/AuthService.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- packages/cloud/src/AuthService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/AuthService.ts index a16f17a2d5..8198c4e8f4 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/AuthService.ts @@ -114,7 +114,7 @@ export class AuthService extends EventEmitter { return } - this.handleCredentialsChange() + await this.handleCredentialsChange() this.context.subscriptions.push( this.context.secrets.onDidChange((e) => {