diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 8117b0b0f5b..1246b2ec40e 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -452,8 +452,8 @@ export class AzureActiveDirectoryService { if (isSupportedEnvironment(callbackUri)) { existingPromise = this.handleCodeResponse(scopeData); } else { - inputBox = vscode.window.createInputBox(); - existingPromise = this.handleCodeInputBox(inputBox, codeVerifier, scopeData); + // This code path shouldn't be hit often, so just surface an error. + throw new Error('Unsupported environment for authentication'); } this._codeExchangePromises.set(scopeData.scopeStr, existingPromise); } @@ -744,34 +744,6 @@ export class AzureActiveDirectoryService { }); } - private async handleCodeInputBox(inputBox: vscode.InputBox, verifier: string, scopeData: IScopeData): Promise { - this._logger.trace(`[${scopeData.scopeStr}] Starting login flow with input box`); - inputBox.ignoreFocusOut = true; - inputBox.title = vscode.l10n.t('Microsoft Authentication'); - inputBox.prompt = vscode.l10n.t('Provide the authorization code to complete the sign in flow.'); - inputBox.placeholder = vscode.l10n.t('Paste authorization code here...'); - return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => { - inputBox.show(); - inputBox.onDidAccept(async () => { - const code = inputBox.value; - if (code) { - inputBox.dispose(); - const session = await this.exchangeCodeForSession(code, verifier, scopeData); - this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' sending session changed event because session was added.`); - this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); - this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' session successfully created!`); - resolve(session); - } - }); - inputBox.onDidHide(() => { - if (!inputBox.value) { - inputBox.dispose(); - reject('Cancelled'); - } - }); - }); - } - private async exchangeCodeForSession(code: string, codeVerifier: string, scopeData: IScopeData): Promise { this._logger.trace(`[${scopeData.scopeStr}] Exchanging login code for session`); let token: IToken | undefined; diff --git a/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts b/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts index 3d4c56723ea..f7e41805d70 100644 --- a/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts +++ b/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts @@ -5,17 +5,14 @@ import type { ILoopbackClient, ServerAuthorizationCodeResponse } from '@azure/msal-node'; import type { UriEventHandler } from '../UriEventHandler'; -import { Disposable, env, l10n, LogOutputChannel, Uri, window } from 'vscode'; -import { DeferredPromise, toPromise } from './async'; -import { isSupportedClient } from './env'; +import { env, LogOutputChannel, Uri } from 'vscode'; +import { toPromise } from './async'; export interface ILoopbackClientAndOpener extends ILoopbackClient { openBrowser(url: string): Promise; } export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener { - private _responseDeferred: DeferredPromise | undefined; - constructor( private readonly _uriHandler: UriEventHandler, private readonly _redirectUri: string, @@ -24,14 +21,16 @@ export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener { ) { } async listenForAuthCode(): Promise { - await this._responseDeferred?.cancel(); - this._responseDeferred = new DeferredPromise(); - const result = await this._responseDeferred.p; - this._responseDeferred = undefined; - if (result) { - return result; - } - throw new Error('No valid response received for authorization code.'); + const url = await toPromise(this._uriHandler.event); + this._logger.debug(`Received URL event. Authority: ${url.authority}`); + const result = new URL(url.toString(true)); + return { + code: result.searchParams.get('code') ?? undefined, + state: result.searchParams.get('state') ?? undefined, + error: result.searchParams.get('error') ?? undefined, + error_description: result.searchParams.get('error_description') ?? undefined, + error_uri: result.searchParams.get('error_uri') ?? undefined, + }; } getRedirectUri(): string { @@ -45,93 +44,7 @@ export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener { } async openBrowser(url: string): Promise { - if (isSupportedClient(this._callbackUri)) { - void this._getCodeResponseFromUriHandler(); - } else { - // Unsupported clients will be shown the code in the browser, but it will not redirect back since this - // isn't a supported client. Instead, they will copy that code in the browser and paste it in an input box - // that will be shown to them by the extension. - void this._getCodeResponseFromQuickPick(); - } - const uri = Uri.parse(url + `&state=${encodeURI(this._callbackUri.toString(true))}`); await env.openExternal(uri); } - - private async _getCodeResponseFromUriHandler(): Promise { - if (!this._responseDeferred) { - throw new Error('No listener for auth code'); - } - const url = await toPromise(this._uriHandler.event); - this._logger.debug(`Received URL event. Authority: ${url.authority}`); - const result = new URL(url.toString(true)); - - this._responseDeferred?.complete({ - code: result.searchParams.get('code') ?? undefined, - state: result.searchParams.get('state') ?? undefined, - error: result.searchParams.get('error') ?? undefined, - error_description: result.searchParams.get('error_description') ?? undefined, - error_uri: result.searchParams.get('error_uri') ?? undefined, - }); - } - - private async _getCodeResponseFromQuickPick(): Promise { - if (!this._responseDeferred) { - throw new Error('No listener for auth code'); - } - const inputBox = window.createInputBox(); - inputBox.ignoreFocusOut = true; - inputBox.title = l10n.t('Microsoft Authentication'); - inputBox.prompt = l10n.t('Provide the authorization code to complete the sign in flow.'); - inputBox.placeholder = l10n.t('Paste authorization code here...'); - inputBox.show(); - const code = await new Promise((resolve) => { - let resolvedValue: string | undefined = undefined; - const disposable = Disposable.from( - inputBox, - inputBox.onDidAccept(async () => { - if (!inputBox.value) { - inputBox.validationMessage = l10n.t('Authorization code is required.'); - return; - } - const code = inputBox.value; - resolvedValue = code; - resolve(code); - inputBox.hide(); - }), - inputBox.onDidChangeValue(() => { - inputBox.validationMessage = undefined; - }), - inputBox.onDidHide(() => { - disposable.dispose(); - if (!resolvedValue) { - resolve(undefined); - } - }) - ); - Promise.allSettled([this._responseDeferred?.p]).then(() => disposable.dispose()); - }); - // Something canceled the original deferred promise, so just return. - if (this._responseDeferred.isSettled) { - return; - } - if (code) { - this._logger.debug('Received auth code from quick pick'); - this._responseDeferred.complete({ - code, - state: undefined, - error: undefined, - error_description: undefined, - error_uri: undefined - }); - return; - } - this._responseDeferred.complete({ - code: undefined, - state: undefined, - error: 'User cancelled', - error_description: 'User cancelled', - error_uri: undefined - }); - } } diff --git a/extensions/microsoft-authentication/src/common/test/loopbackClientAndOpener.test.ts b/extensions/microsoft-authentication/src/common/test/loopbackClientAndOpener.test.ts index c64f122c022..31375af860f 100644 --- a/extensions/microsoft-authentication/src/common/test/loopbackClientAndOpener.test.ts +++ b/extensions/microsoft-authentication/src/common/test/loopbackClientAndOpener.test.ts @@ -51,7 +51,7 @@ suite('UriHandlerLoopbackClient', () => { }); // Skipped for now until `listenForAuthCode` is refactored to not show quick pick - suite.skip('listenForAuthCode', () => { + suite('listenForAuthCode', () => { test('should return auth code from URL', async () => { const code = '1234'; const state = '5678';