Skip to content

Commit 81000ed

Browse files
committed
Move client token + session id to shared secret
1 parent e40a4c9 commit 81000ed

File tree

3 files changed

+71
-43
lines changed

3 files changed

+71
-43
lines changed

packages/cloud/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"dependencies": {
1414
"@roo-code/telemetry": "workspace:^",
1515
"@roo-code/types": "workspace:^",
16-
"axios": "^1.7.4"
16+
"axios": "^1.7.4",
17+
"zod": "^3.24.2"
1718
},
1819
"devDependencies": {
1920
"@roo-code/config-eslint": "workspace:^",

packages/cloud/src/AuthService.ts

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import EventEmitter from "events"
33

44
import axios from "axios"
55
import * as vscode from "vscode"
6+
import { z } from "zod"
67

78
import type { CloudUserInfo } from "@roo-code/types"
89

@@ -15,8 +16,14 @@ export interface AuthServiceEvents {
1516
"user-info": [data: { userInfo: CloudUserInfo }]
1617
}
1718

18-
const CLIENT_TOKEN_KEY = "clerk-client-token"
19-
const SESSION_ID_KEY = "clerk-session-id"
19+
const authCredentialsSchema = z.object({
20+
clientToken: z.string().min(1, "Client token cannot be empty"),
21+
sessionId: z.string().min(1, "Session ID cannot be empty"),
22+
})
23+
24+
type AuthCredentials = z.infer<typeof authCredentialsSchema>
25+
26+
const AUTH_CREDENTIALS_KEY = "clerk-auth-credentials"
2027
const AUTH_STATE_KEY = "clerk-auth-state"
2128

2229
type AuthState = "initializing" | "logged-out" | "active-session" | "inactive-session"
@@ -26,9 +33,8 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
2633
private timer: RefreshTimer
2734
private state: AuthState = "initializing"
2835

29-
private clientToken: string | null = null
36+
private credentials: AuthCredentials | null = null
3037
private sessionToken: string | null = null
31-
private sessionId: string | null = null
3238
private userInfo: CloudUserInfo | null = null
3339

3440
constructor(context: vscode.ExtensionContext) {
@@ -60,19 +66,16 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
6066
}
6167

6268
try {
63-
this.clientToken = (await this.context.secrets.get(CLIENT_TOKEN_KEY)) || null
64-
this.sessionId = this.context.globalState.get<string>(SESSION_ID_KEY) || null
69+
const credentials = await this.loadCredentials()
6570

66-
// Determine initial state.
67-
if (!this.clientToken || !this.sessionId) {
68-
// TODO: it may be possible to get a new session with the client,
69-
// but the obvious Clerk endpoints don't support that.
71+
if (credentials) {
72+
this.credentials = credentials
73+
this.state = "inactive-session"
74+
this.timer.start()
75+
} else {
7076
const previousState = this.state
7177
this.state = "logged-out"
7278
this.emit("logged-out", { previousState })
73-
} else {
74-
this.state = "inactive-session"
75-
this.timer.start()
7679
}
7780

7881
console.log(`[auth] Initialized with state: ${this.state}`)
@@ -82,6 +85,32 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
8285
}
8386
}
8487

88+
private async storeCredentials(credentials: AuthCredentials): Promise<void> {
89+
await this.context.secrets.store(AUTH_CREDENTIALS_KEY, JSON.stringify(credentials))
90+
}
91+
92+
private async loadCredentials(): Promise<AuthCredentials | null> {
93+
const credentialsJson = await this.context.secrets.get(AUTH_CREDENTIALS_KEY)
94+
if (!credentialsJson) return null
95+
96+
try {
97+
const parsedJson = JSON.parse(credentialsJson)
98+
// Validate using Zod schema
99+
return authCredentialsSchema.parse(parsedJson)
100+
} catch (error) {
101+
if (error instanceof z.ZodError) {
102+
console.error("[auth] Invalid credentials format:", error.errors)
103+
} else {
104+
console.error("[auth] Failed to parse stored credentials:", error)
105+
}
106+
return null
107+
}
108+
}
109+
110+
private async clearCredentials(): Promise<void> {
111+
await this.context.secrets.delete(AUTH_CREDENTIALS_KEY)
112+
}
113+
85114
/**
86115
* Start the login process
87116
*
@@ -132,13 +161,11 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
132161
throw new Error("Invalid state parameter. Authentication request may have been tampered with.")
133162
}
134163

135-
const { clientToken, sessionToken, sessionId } = await this.clerkSignIn(code)
164+
const { credentials, sessionToken } = await this.clerkSignIn(code)
136165

137-
await this.context.secrets.store(CLIENT_TOKEN_KEY, clientToken)
138-
await this.context.globalState.update(SESSION_ID_KEY, sessionId)
166+
await this.storeCredentials(credentials)
139167

140-
this.clientToken = clientToken
141-
this.sessionId = sessionId
168+
this.credentials = credentials
142169
this.sessionToken = sessionToken
143170

144171
const previousState = this.state
@@ -168,23 +195,20 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
168195
try {
169196
this.timer.stop()
170197

171-
await this.context.secrets.delete(CLIENT_TOKEN_KEY)
172-
await this.context.globalState.update(SESSION_ID_KEY, undefined)
198+
await this.clearCredentials()
173199
await this.context.globalState.update(AUTH_STATE_KEY, undefined)
174200

175-
const oldClientToken = this.clientToken
176-
const oldSessionId = this.sessionId
201+
const oldCredentials = this.credentials
177202

178-
this.clientToken = null
203+
this.credentials = null
179204
this.sessionToken = null
180-
this.sessionId = null
181205
this.userInfo = null
182206
const previousState = this.state
183207
this.state = "logged-out"
184208
this.emit("logged-out", { previousState })
185209

186-
if (oldClientToken && oldSessionId) {
187-
await this.clerkLogout(oldClientToken, oldSessionId)
210+
if (oldCredentials) {
211+
await this.clerkLogout(oldCredentials)
188212
}
189213

190214
this.fetchUserInfo()
@@ -228,8 +252,8 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
228252
* This method refreshes the session token using the client token.
229253
*/
230254
private async refreshSession(): Promise<void> {
231-
if (!this.sessionId || !this.clientToken) {
232-
console.log("[auth] Cannot refresh session: missing session ID or token")
255+
if (!this.credentials) {
256+
console.log("[auth] Cannot refresh session: missing credentials")
233257
this.state = "inactive-session"
234258
return
235259
}
@@ -245,7 +269,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
245269
}
246270

247271
private async fetchUserInfo(): Promise<void> {
248-
if (!this.clientToken) {
272+
if (!this.credentials) {
249273
return
250274
}
251275

@@ -262,9 +286,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
262286
return this.userInfo
263287
}
264288

265-
private async clerkSignIn(
266-
ticket: string,
267-
): Promise<{ clientToken: string; sessionToken: string; sessionId: string }> {
289+
private async clerkSignIn(ticket: string): Promise<{ credentials: AuthCredentials; sessionToken: string }> {
268290
const formData = new URLSearchParams()
269291
formData.append("strategy", "ticket")
270292
formData.append("ticket", ticket)
@@ -284,14 +306,14 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
284306
}
285307

286308
// 4. Find the session using created_session_id and extract the JWT.
287-
const createdSessionId = response.data?.response?.created_session_id
309+
const sessionId = response.data?.response?.created_session_id
288310

289-
if (!createdSessionId) {
311+
if (!sessionId) {
290312
throw new Error("No session ID found in the response")
291313
}
292314

293315
// Find the session in the client sessions array.
294-
const session = response.data?.client?.sessions?.find((s: { id: string }) => s.id === createdSessionId)
316+
const session = response.data?.client?.sessions?.find((s: { id: string }) => s.id === sessionId)
295317

296318
if (!session) {
297319
throw new Error("Session not found in the response")
@@ -304,20 +326,22 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
304326
throw new Error("Session does not have a token")
305327
}
306328

307-
return { clientToken, sessionToken, sessionId: session.id }
329+
const credentials = authCredentialsSchema.parse({ clientToken, sessionId })
330+
331+
return { credentials, sessionToken }
308332
}
309333

310334
private async clerkCreateSessionToken(): Promise<string> {
311335
const formData = new URLSearchParams()
312336
formData.append("_is_native", "1")
313337

314338
const response = await axios.post(
315-
`${getClerkBaseUrl()}/v1/client/sessions/${this.sessionId}/tokens`,
339+
`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`,
316340
formData,
317341
{
318342
headers: {
319343
"Content-Type": "application/x-www-form-urlencoded",
320-
Authorization: `Bearer ${this.clientToken}`,
344+
Authorization: `Bearer ${this.credentials!.clientToken}`,
321345
"User-Agent": this.userAgent(),
322346
},
323347
},
@@ -335,7 +359,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
335359
private async clerkMe(): Promise<CloudUserInfo> {
336360
const response = await axios.get(`${getClerkBaseUrl()}/v1/me`, {
337361
headers: {
338-
Authorization: `Bearer ${this.clientToken}`,
362+
Authorization: `Bearer ${this.credentials!.clientToken}`,
339363
"User-Agent": this.userAgent(),
340364
},
341365
})
@@ -362,13 +386,13 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
362386
return userInfo
363387
}
364388

365-
private async clerkLogout(clientToken: string, sessionId: string): Promise<void> {
389+
private async clerkLogout(credentials: AuthCredentials): Promise<void> {
366390
const formData = new URLSearchParams()
367391
formData.append("_is_native", "1")
368392

369-
await axios.post(`${getClerkBaseUrl()}/v1/client/sessions/${sessionId}/remove`, formData, {
393+
await axios.post(`${getClerkBaseUrl()}/v1/client/sessions/${credentials.sessionId}/remove`, formData, {
370394
headers: {
371-
Authorization: `Bearer ${clientToken}`,
395+
Authorization: `Bearer ${credentials.clientToken}`,
372396
"User-Agent": this.userAgent(),
373397
},
374398
})

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)