Skip to content

Commit 0aad64e

Browse files
committed
Add proper scopes
1 parent 01024bd commit 0aad64e

File tree

1 file changed

+82
-14
lines changed

1 file changed

+82
-14
lines changed

src/oauth/oauthHelper.ts

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,28 @@ const CLIENT_NAME = "VS Code Coder Extension";
3030

3131
const REQUIRED_GRANT_TYPES = [AUTH_GRANT_TYPE, REFRESH_GRANT_TYPE] as const;
3232

33-
// Token refresh timing constants
34-
const TOKEN_REFRESH_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes before expiry
33+
// Token refresh timing constants (5 minutes before expiry)
34+
const TOKEN_REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
35+
36+
/**
37+
* Minimal scopes required by the VS Code extension:
38+
* - workspace:read: List and read workspace details
39+
* - workspace:update: Update workspace version
40+
* - workspace:start: Start stopped workspaces
41+
* - workspace:ssh: SSH configuration for remote connections
42+
* - workspace:application_connect: Connect to workspace agents/apps
43+
* - template:read: Read templates and versions
44+
* - user:read_personal: Read authenticated user info
45+
*/
46+
const DEFAULT_OAUTH_SCOPES = [
47+
"workspace:read",
48+
"workspace:update",
49+
"workspace:start",
50+
"workspace:ssh",
51+
"workspace:application_connect",
52+
"template:read",
53+
"user:read_personal",
54+
].join(" ");
3555

3656
export class CoderOAuthHelper {
3757
private clientRegistration: ClientRegistrationResponse | undefined;
@@ -156,9 +176,22 @@ export class CoderOAuthHelper {
156176
private async loadTokens(): Promise<void> {
157177
const tokens = await this.secretsManager.getOAuthTokens();
158178
if (tokens) {
179+
if (!this.hasRequiredScopes(tokens.scope)) {
180+
this.logger.warn(
181+
"Stored token missing required scopes, clearing tokens",
182+
{
183+
stored_scope: tokens.scope,
184+
required_scopes: DEFAULT_OAUTH_SCOPES,
185+
},
186+
);
187+
await this.secretsManager.clearOAuthTokens();
188+
return;
189+
}
190+
159191
this.storedTokens = tokens;
160192
this.logger.info("Loaded stored OAuth tokens", {
161193
expires_at: new Date(tokens.expiry_timestamp).toISOString(),
194+
scope: tokens.scope,
162195
});
163196

164197
if (tokens.refresh_token) {
@@ -167,6 +200,40 @@ export class CoderOAuthHelper {
167200
}
168201
}
169202

203+
/**
204+
* Check if granted scopes cover all required scopes.
205+
* Supports wildcard scopes like "workspace:*" which grant all "workspace:" prefixed scopes.
206+
*/
207+
private hasRequiredScopes(grantedScope: string | undefined): boolean {
208+
if (!grantedScope) {
209+
return false;
210+
}
211+
212+
const grantedScopes = new Set(grantedScope.split(" "));
213+
const requiredScopes = DEFAULT_OAUTH_SCOPES.split(" ");
214+
215+
for (const required of requiredScopes) {
216+
// Check exact match
217+
if (grantedScopes.has(required)) {
218+
continue;
219+
}
220+
221+
// Check wildcard match (e.g., "workspace:*" grants "workspace:read")
222+
const colonIndex = required.indexOf(":");
223+
if (colonIndex !== -1) {
224+
const prefix = required.substring(0, colonIndex);
225+
const wildcard = `${prefix}:*`;
226+
if (grantedScopes.has(wildcard)) {
227+
continue;
228+
}
229+
}
230+
231+
return false;
232+
}
233+
234+
return true;
235+
}
236+
170237
private async saveClientRegistration(
171238
registration: ClientRegistrationResponse,
172239
): Promise<void> {
@@ -231,16 +298,19 @@ export class CoderOAuthHelper {
231298
clientId: string,
232299
state: string,
233300
challenge: string,
234-
scope = "all",
301+
scope: string,
235302
): string {
236-
if (
237-
metadata.scopes_supported &&
238-
!metadata.scopes_supported.includes(scope)
239-
) {
240-
this.logger.warn(
241-
`Requested scope "${scope}" not in server's supported scopes. Server may still accept it.`,
242-
{ supported_scopes: metadata.scopes_supported },
303+
if (metadata.scopes_supported) {
304+
const requestedScopes = scope.split(" ");
305+
const unsupportedScopes = requestedScopes.filter(
306+
(s) => !metadata.scopes_supported?.includes(s),
243307
);
308+
if (unsupportedScopes.length > 0) {
309+
this.logger.warn(
310+
`Requested scopes not in server's supported scopes: ${unsupportedScopes.join(", ")}. Server may still accept them.`,
311+
{ supported_scopes: metadata.scopes_supported },
312+
);
313+
}
244314
}
245315

246316
const params: AuthorizationRequestParams = {
@@ -264,9 +334,7 @@ export class CoderOAuthHelper {
264334
return url;
265335
}
266336

267-
async startAuthorization(
268-
scope = "all",
269-
): Promise<{ code: string; verifier: string }> {
337+
async startAuthorization(): Promise<{ code: string; verifier: string }> {
270338
const metadata = await this.getMetadata();
271339
const clientId = await this.registerClient();
272340
const state = generateState();
@@ -277,7 +345,7 @@ export class CoderOAuthHelper {
277345
clientId,
278346
state,
279347
challenge,
280-
scope,
348+
DEFAULT_OAUTH_SCOPES,
281349
);
282350

283351
return new Promise<{ code: string; verifier: string }>(

0 commit comments

Comments
 (0)