@@ -30,8 +30,28 @@ const CLIENT_NAME = "VS Code Coder Extension";
3030
3131const 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
3656export 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