@@ -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 {
3757private clientRegistration :ClientRegistrationResponse | undefined ;
@@ -156,9 +176,22 @@ export class CoderOAuthHelper {
156176private async loadTokens ( ) :Promise < void > {
157177const tokens = await this . secretsManager . getOAuthTokens ( ) ;
158178if ( 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+
159191this . storedTokens = tokens ;
160192this . logger . info ( "Loaded stored OAuth tokens" , {
161193expires_at :new Date ( tokens . expiry_timestamp ) . toISOString ( ) ,
194+ scope :tokens . scope ,
162195} ) ;
163196
164197if ( 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+
170237private async saveClientRegistration (
171238registration :ClientRegistrationResponse ,
172239) :Promise < void > {
@@ -231,16 +298,19 @@ export class CoderOAuthHelper {
231298clientId :string ,
232299state :string ,
233300challenge :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
246316const params :AuthorizationRequestParams = {
@@ -264,9 +334,7 @@ export class CoderOAuthHelper {
264334return url ;
265335}
266336
267- async startAuthorization (
268- scope = "all" ,
269- ) :Promise < { code :string ; verifier :string } > {
337+ async startAuthorization ( ) :Promise < { code :string ; verifier :string } > {
270338const metadata = await this . getMetadata ( ) ;
271339const clientId = await this . registerClient ( ) ;
272340const state = generateState ( ) ;
@@ -277,7 +345,7 @@ export class CoderOAuthHelper {
277345clientId ,
278346state ,
279347challenge ,
280- scope ,
348+ DEFAULT_OAUTH_SCOPES ,
281349) ;
282350
283351return new Promise < { code :string ; verifier :string } > (