Path: blob/main/extensions/microsoft-authentication/src/common/loopbackClientAndOpener.ts
3320 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type { ILoopbackClient, ServerAuthorizationCodeResponse } from '@azure/msal-node';6import type { UriEventHandler } from '../UriEventHandler';7import { Disposable, env, l10n, LogOutputChannel, Uri, window } from 'vscode';8import { DeferredPromise, toPromise } from './async';9import { isSupportedClient } from './env';1011export interface ILoopbackClientAndOpener extends ILoopbackClient {12openBrowser(url: string): Promise<void>;13}1415export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener {16private _responseDeferred: DeferredPromise<ServerAuthorizationCodeResponse> | undefined;1718constructor(19private readonly _uriHandler: UriEventHandler,20private readonly _redirectUri: string,21private readonly _logger: LogOutputChannel22) { }2324async listenForAuthCode(): Promise<ServerAuthorizationCodeResponse> {25await this._responseDeferred?.cancel();26this._responseDeferred = new DeferredPromise();27const result = await this._responseDeferred.p;28this._responseDeferred = undefined;29if (result) {30return result;31}32throw new Error('No valid response received for authorization code.');33}3435getRedirectUri(): string {36// We always return the constant redirect URL because37// it will handle redirecting back to the extension38return this._redirectUri;39}4041closeServer(): void {42// No-op43}4445async openBrowser(url: string): Promise<void> {46const callbackUri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://vscode.microsoft-authentication`));4748if (isSupportedClient(callbackUri)) {49void this._getCodeResponseFromUriHandler();50} else {51// Unsupported clients will be shown the code in the browser, but it will not redirect back since this52// isn't a supported client. Instead, they will copy that code in the browser and paste it in an input box53// that will be shown to them by the extension.54void this._getCodeResponseFromQuickPick();55}5657const uri = Uri.parse(url + `&state=${encodeURI(callbackUri.toString(true))}`);58await env.openExternal(uri);59}6061private async _getCodeResponseFromUriHandler(): Promise<void> {62if (!this._responseDeferred) {63throw new Error('No listener for auth code');64}65const url = await toPromise(this._uriHandler.event);66this._logger.debug(`Received URL event. Authority: ${url.authority}`);67const result = new URL(url.toString(true));6869this._responseDeferred?.complete({70code: result.searchParams.get('code') ?? undefined,71state: result.searchParams.get('state') ?? undefined,72error: result.searchParams.get('error') ?? undefined,73error_description: result.searchParams.get('error_description') ?? undefined,74error_uri: result.searchParams.get('error_uri') ?? undefined,75});76}7778private async _getCodeResponseFromQuickPick(): Promise<void> {79if (!this._responseDeferred) {80throw new Error('No listener for auth code');81}82const inputBox = window.createInputBox();83inputBox.ignoreFocusOut = true;84inputBox.title = l10n.t('Microsoft Authentication');85inputBox.prompt = l10n.t('Provide the authorization code to complete the sign in flow.');86inputBox.placeholder = l10n.t('Paste authorization code here...');87inputBox.show();88const code = await new Promise<string | undefined>((resolve) => {89let resolvedValue: string | undefined = undefined;90const disposable = Disposable.from(91inputBox,92inputBox.onDidAccept(async () => {93if (!inputBox.value) {94inputBox.validationMessage = l10n.t('Authorization code is required.');95return;96}97const code = inputBox.value;98resolvedValue = code;99resolve(code);100inputBox.hide();101}),102inputBox.onDidChangeValue(() => {103inputBox.validationMessage = undefined;104}),105inputBox.onDidHide(() => {106disposable.dispose();107if (!resolvedValue) {108resolve(undefined);109}110})111);112Promise.allSettled([this._responseDeferred?.p]).then(() => disposable.dispose());113});114// Something canceled the original deferred promise, so just return.115if (this._responseDeferred.isSettled) {116return;117}118if (code) {119this._logger.debug('Received auth code from quick pick');120this._responseDeferred.complete({121code,122state: undefined,123error: undefined,124error_description: undefined,125error_uri: undefined126});127return;128}129this._responseDeferred.complete({130code: undefined,131state: undefined,132error: 'User cancelled',133error_description: 'User cancelled',134error_uri: undefined135});136}137}138139140