Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit639df18

Browse files
committed
Allow callback handling in multiple VS Code windows
1 parentb93e027 commit639df18

File tree

3 files changed

+85
-77
lines changed

3 files changed

+85
-77
lines changed

‎src/core/secretsManager.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,19 @@ const OAUTH_CLIENT_REGISTRATION_KEY = "oauthClientRegistration";
1313

1414
constOAUTH_TOKENS_KEY="oauthTokens";
1515

16+
constOAUTH_CALLBACK_KEY="coder.oauthCallback";
17+
1618
exporttypeStoredOAuthTokens=Omit<TokenResponse,"expires_in">&{
1719
expiry_timestamp:number;
1820
deployment_url:string;
1921
};
2022

23+
interfaceOAuthCallbackData{
24+
state:string;
25+
code:string|null;
26+
error:string|null;
27+
}
28+
2129
exportenumAuthAction{
2230
LOGIN,
2331
LOGOUT,
@@ -163,4 +171,36 @@ export class SecretsManager {
163171
}
164172
returnundefined;
165173
}
174+
175+
/**
176+
* Write an OAuth callback result to secrets storage.
177+
* Used for cross-window communication when OAuth callback arrives in a different window.
178+
*/
179+
publicasyncsetOAuthCallback(data:OAuthCallbackData):Promise<void>{
180+
awaitthis.secrets.store(OAUTH_CALLBACK_KEY,JSON.stringify(data));
181+
}
182+
183+
/**
184+
* Listen for OAuth callback results from any VS Code window.
185+
* The listener receives the state parameter, code (if success), and error (if failed).
186+
*/
187+
publiconDidChangeOAuthCallback(
188+
listener:(data:OAuthCallbackData)=>void,
189+
):Disposable{
190+
returnthis.secrets.onDidChange(async(e)=>{
191+
if(e.key!==OAUTH_CALLBACK_KEY){
192+
return;
193+
}
194+
195+
try{
196+
constdata=awaitthis.secrets.get(OAUTH_CALLBACK_KEY);
197+
if(data){
198+
constparsed=JSON.parse(data)asOAuthCallbackData;
199+
listener(parsed);
200+
}
201+
}catch{
202+
// Ignore parse errors
203+
}
204+
});
205+
}
166206
}

‎src/extension.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
163163
constcode=params.get("code");
164164
conststate=params.get("state");
165165
consterror=params.get("error");
166-
oauthSessionManager.handleCallback(code,state,error);
166+
awaitoauthSessionManager.handleCallback(code,state,error);
167167
return;
168168
}
169169

‎src/oauth/sessionManager.ts‎

Lines changed: 44 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,7 @@ export class OAuthSessionManager implements vscode.Disposable {
6464
privaterefreshInProgress=false;
6565
privatelastRefreshAttempt=0;
6666

67-
// Pending authorization flow state
68-
privatependingAuthResolve:
69-
|((value:{code:string;verifier:string})=>void)
70-
|undefined;
7167
privatependingAuthReject:((reason:Error)=>void)|undefined;
72-
privateexpectedState:string|undefined;
73-
privatependingVerifier:string|undefined;
7468

7569
/**
7670
* Create and initialize a new OAuth session manager.
@@ -370,106 +364,80 @@ export class OAuthSessionManager implements vscode.Disposable {
370364
challenge,
371365
);
372366

373-
returnnewPromise<{code:string;verifier:string}>(
367+
constcallbackPromise=newPromise<{code:string;verifier:string}>(
374368
(resolve,reject)=>{
375369
consttimeoutMins=5;
376-
consttimeout=setTimeout(
370+
consttimeoutHandle=setTimeout(
377371
()=>{
378-
this.clearPendingAuth();
372+
cleanup();
379373
reject(
380374
newError(`OAuth flow timed out after${timeoutMins} minutes`),
381375
);
382376
},
383377
timeoutMins*60*1000,
384378
);
385379

386-
constclearPromise=()=>{
387-
clearTimeout(timeout);
388-
this.clearPendingAuth();
389-
};
390-
391-
this.pendingAuthResolve=(result)=>{
392-
clearPromise();
393-
resolve(result);
394-
};
395-
396-
this.pendingAuthReject=(error)=>{
397-
clearPromise();
398-
reject(error);
399-
};
380+
constlistener=this.secretsManager.onDidChangeOAuthCallback(
381+
({state:callbackState, code, error})=>{
382+
if(callbackState!==state){
383+
return;
384+
}
400385

401-
this.expectedState=state;
402-
this.pendingVerifier=verifier;
386+
cleanup();
403387

404-
vscode.env.openExternal(vscode.Uri.parse(authUrl)).then(
405-
()=>{},
406-
(error)=>{
407-
if(errorinstanceofError){
408-
this.pendingAuthReject?.(error);
388+
if(error){
389+
reject(newError(`OAuth error:${error}`));
390+
}elseif(code){
391+
resolve({ code, verifier});
409392
}else{
410-
this.pendingAuthReject?.(newError("Failed to open browser"));
393+
reject(newError("No authorization code received"));
411394
}
412395
},
413396
);
397+
398+
constcleanup=()=>{
399+
clearTimeout(timeoutHandle);
400+
listener.dispose();
401+
};
402+
403+
this.pendingAuthReject=(error)=>{
404+
cleanup();
405+
reject(error);
406+
};
414407
},
415408
);
416-
}
417409

418-
/**
419-
* Clear pending authorization flow state.
420-
*/
421-
privateclearPendingAuth():void{
422-
this.pendingAuthResolve=undefined;
423-
this.pendingAuthReject=undefined;
424-
this.expectedState=undefined;
425-
this.pendingVerifier=undefined;
410+
try{
411+
awaitvscode.env.openExternal(vscode.Uri.parse(authUrl));
412+
}catch(error){
413+
throwerrorinstanceofError
414+
?error
415+
:newError("Failed to open browser");
416+
}
417+
418+
returncallbackPromise;
426419
}
427420

428421
/**
429422
* Handle OAuth callback from browser redirect.
430-
* Validates state and resolves pending authorization promise.
431-
*
432-
* // TODO this has to work across windows!
423+
* Writes the callback result to secrets storage, triggering the waiting window to proceed.
433424
*/
434-
handleCallback(
425+
asynchandleCallback(
435426
code:string|null,
436427
state:string|null,
437428
error:string|null,
438-
):void{
439-
if(!this.pendingAuthResolve||!this.pendingAuthReject){
440-
this.logger.warn("Received OAuth callback but no pending auth flow");
441-
return;
442-
}
443-
444-
if(error){
445-
this.pendingAuthReject(newError(`OAuth error:${error}`));
446-
return;
447-
}
448-
449-
if(!code){
450-
this.pendingAuthReject(newError("No authorization code received"));
451-
return;
452-
}
453-
429+
):Promise<void>{
454430
if(!state){
455-
this.pendingAuthReject(newError("No state received"));
456-
return;
457-
}
458-
459-
if(state!==this.expectedState){
460-
this.pendingAuthReject(
461-
newError("State mismatch - possible CSRF attack"),
462-
);
431+
this.logger.warn("Received OAuth callback with no state parameter");
463432
return;
464433
}
465434

466-
constverifier=this.pendingVerifier;
467-
if(!verifier){
468-
this.pendingAuthReject(newError("No PKCE verifier found"));
469-
return;
435+
try{
436+
awaitthis.secretsManager.setOAuthCallback({ state, code, error});
437+
this.logger.debug("OAuth callback processed successfully");
438+
}catch(err){
439+
this.logger.error("Failed to process OAuth callback:",err);
470440
}
471-
472-
this.pendingAuthResolve({ code, verifier});
473441
}
474442

475443
/**
@@ -712,13 +680,13 @@ export class OAuthSessionManager implements vscode.Disposable {
712680
}
713681

714682
/**
715-
* Clears all in-memory state and rejects any pending operations.
683+
* Clears all in-memory state.
716684
*/
717685
dispose():void{
718686
if(this.pendingAuthReject){
719687
this.pendingAuthReject(newError("OAuth session manager disposed"));
720688
}
721-
this.clearPendingAuth();
689+
this.pendingAuthReject=undefined;
722690
this.storedTokens=undefined;
723691
this.refreshInProgress=false;
724692
this.lastRefreshAttempt=0;

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp