Skip to content

Commit 4017fd1

Browse files
Show quick pick when redirect fails to complete redirect to be more robust (microsoft#156515)
* show quick pick when redirect fails to complete redirect to be more robust * matt feedback
1 parent 78f4023 commit 4017fd1

File tree

2 files changed

+78
-18
lines changed

2 files changed

+78
-18
lines changed

extensions/microsoft-authentication/src/AADHelper.ts

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as nls from 'vscode-nls';
1111
import { v4 as uuid } from 'uuid';
1212
import fetch, { Response } from 'node-fetch';
1313
import Logger from './logger';
14-
import { toBase64UrlEncoding } from './utils';
14+
import { isSupportedEnvironment, toBase64UrlEncoding } from './utils';
1515
import { sha256 } from './env/node/sha256';
1616
import { BetterTokenStorage, IDidChangeInOtherWindowEvent } from './betterSecretStorage';
1717
import { LoopbackAuthServer } from './authServer';
@@ -319,13 +319,7 @@ export class AzureActiveDirectoryService {
319319
}, 5000);
320320
}
321321

322-
const token = await this.exchangeCodeForToken(codeToExchange, codeVerifier, scopeData);
323-
if (token.expiresIn) {
324-
this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER);
325-
}
326-
await this.setToken(token, scopeData);
327-
Logger.info(`Login successful for scopes: ${scopeData.scopeStr}`);
328-
const session = await this.convertToSession(token);
322+
const session = await this.exchangeCodeForSession(codeToExchange, codeVerifier, scopeData);
329323
return session;
330324
}
331325

@@ -355,9 +349,11 @@ export class AzureActiveDirectoryService {
355349
const uri = vscode.Uri.parse(`${signInUrl}?${oauthStartQuery.toString()}`);
356350
vscode.env.openExternal(uri);
357351

352+
let inputBox: vscode.InputBox | undefined;
358353
const timeoutPromise = new Promise((_: (value: vscode.AuthenticationSession) => void, reject) => {
359354
const wait = setTimeout(() => {
360355
clearTimeout(wait);
356+
inputBox?.dispose();
361357
reject('Login timed out.');
362358
}, 1000 * 60 * 5);
363359
});
@@ -369,7 +365,12 @@ export class AzureActiveDirectoryService {
369365
// before completing it.
370366
let existingPromise = this._codeExchangePromises.get(scopeData.scopeStr);
371367
if (!existingPromise) {
372-
existingPromise = this.handleCodeResponse(scopeData);
368+
if (isSupportedEnvironment(callbackUri)) {
369+
existingPromise = this.handleCodeResponse(scopeData);
370+
} else {
371+
inputBox = vscode.window.createInputBox();
372+
existingPromise = Promise.race([this.handleCodeInputBox(inputBox, codeVerifier, scopeData), this.handleCodeResponse(scopeData)]);
373+
}
373374
this._codeExchangePromises.set(scopeData.scopeStr, existingPromise);
374375
}
375376

@@ -659,13 +660,7 @@ export class AzureActiveDirectoryService {
659660
throw new Error('No available code verifier');
660661
}
661662

662-
const token = await this.exchangeCodeForToken(code, verifier, scopeData);
663-
if (token.expiresIn) {
664-
this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER);
665-
}
666-
await this.setToken(token, scopeData);
667-
668-
const session = await this.convertToSession(token);
663+
const session = await this.exchangeCodeForSession(code, verifier, scopeData);
669664
resolve(session);
670665
} catch (err) {
671666
reject(err);
@@ -680,8 +675,33 @@ export class AzureActiveDirectoryService {
680675
});
681676
}
682677

683-
private async exchangeCodeForToken(code: string, codeVerifier: string, scopeData: IScopeData): Promise<IToken> {
678+
private async handleCodeInputBox(inputBox: vscode.InputBox, verifier: string, scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
679+
inputBox.ignoreFocusOut = true;
680+
inputBox.title = localize('pasteCodeTitle', 'Microsoft Authentication');
681+
inputBox.prompt = localize('pasteCodePrompt', 'Provide the authorization code to complete the sign in flow.');
682+
inputBox.placeholder = localize('pasteCodePlaceholder', 'Paste authorization code here...');
683+
return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => {
684+
inputBox.show();
685+
inputBox.onDidAccept(async () => {
686+
const code = inputBox.value;
687+
if (code) {
688+
inputBox.dispose();
689+
const session = await this.exchangeCodeForSession(code, verifier, scopeData);
690+
resolve(session);
691+
}
692+
});
693+
inputBox.onDidHide(() => {
694+
if (!inputBox.value) {
695+
inputBox.dispose();
696+
reject('Cancelled');
697+
}
698+
});
699+
});
700+
}
701+
702+
private async exchangeCodeForSession(code: string, codeVerifier: string, scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
684703
Logger.info(`Exchanging login code for token for scopes: ${scopeData.scopeStr}`);
704+
let token: IToken | undefined;
685705
try {
686706
const postData = querystring.stringify({
687707
grant_type: 'authorization_code',
@@ -698,11 +718,18 @@ export class AzureActiveDirectoryService {
698718

699719
const json = await this.fetchTokenResponse(endpoint, postData, scopeData);
700720
Logger.info(`Exchanging login code for token (for scopes: ${scopeData.scopeStr}) succeeded!`);
701-
return this.convertToTokenSync(json, scopeData);
721+
token = this.convertToTokenSync(json, scopeData);
702722
} catch (e) {
703723
Logger.error(`Error exchanging code for token (for scopes ${scopeData.scopeStr}): ${e}`);
704724
throw e;
705725
}
726+
727+
if (token.expiresIn) {
728+
this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER);
729+
}
730+
await this.setToken(token, scopeData);
731+
Logger.info(`Login successful for scopes: ${scopeData.scopeStr}`);
732+
return await this.convertToSession(token);
706733
}
707734

708735
private async fetchTokenResponse(endpoint: string, postData: string, scopeData: IScopeData): Promise<ITokenResponse> {

extensions/microsoft-authentication/src/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,40 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
5+
import { env, UIKind, Uri } from 'vscode';
56

67
export function toBase64UrlEncoding(base64string: string) {
78
return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding
89
}
10+
11+
const LOCALHOST_ADDRESSES = ['localhost', '127.0.0.1', '0:0:0:0:0:0:0:1', '::1'];
12+
function isLocalhost(uri: Uri): boolean {
13+
if (!/^https?$/i.test(uri.scheme)) {
14+
return false;
15+
}
16+
const host = uri.authority.split(':')[0];
17+
return LOCALHOST_ADDRESSES.indexOf(host) >= 0;
18+
}
19+
20+
export function isSupportedEnvironment(uri: Uri): boolean {
21+
if (env.uiKind === UIKind.Desktop) {
22+
return true;
23+
}
24+
// local development (localhost:* or 127.0.0.1:*)
25+
if (isLocalhost(uri)) {
26+
return true;
27+
}
28+
// At this point we should only ever see https
29+
if (uri.scheme !== 'https') {
30+
return false;
31+
}
32+
33+
return (
34+
// vscode.dev & insiders.vscode.dev
35+
/(?:^|\.)vscode\.dev$/.test(uri.authority) ||
36+
// github.dev & codespaces
37+
/(?:^|\.)github\.dev$/.test(uri.authority) ||
38+
// github.dev/codespaces local setup (github.localhost)
39+
/(?:^|\.)github\.localhost$/.test(uri.authority)
40+
);
41+
}

0 commit comments

Comments
 (0)