mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 02:28:34 +01:00
Show InputBox for unsupported clients (#239389)
* Show InputBox for unsupported clients Fixes https://github.com/microsoft/vscode/issues/238147 * comment * Add 127.0.0.1 for good measure
This commit is contained in:
committed by
GitHub
parent
8237317c2f
commit
4c32889faf
@@ -111,3 +111,69 @@ function once<T>(event: Event<T>): Event<T> {
|
||||
export function toPromise<T>(event: Event<T>): Promise<T> {
|
||||
return new Promise(resolve => once(event)(resolve));
|
||||
}
|
||||
|
||||
//#region DeferredPromise
|
||||
|
||||
export type ValueCallback<T = unknown> = (value: T | Promise<T>) => void;
|
||||
|
||||
const enum DeferredOutcome {
|
||||
Resolved,
|
||||
Rejected
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a promise whose resolution or rejection can be controlled imperatively.
|
||||
*/
|
||||
export class DeferredPromise<T> {
|
||||
|
||||
private completeCallback!: ValueCallback<T>;
|
||||
private errorCallback!: (err: unknown) => void;
|
||||
private outcome?: { outcome: DeferredOutcome.Rejected; value: any } | { outcome: DeferredOutcome.Resolved; value: T };
|
||||
|
||||
public get isRejected() {
|
||||
return this.outcome?.outcome === DeferredOutcome.Rejected;
|
||||
}
|
||||
|
||||
public get isResolved() {
|
||||
return this.outcome?.outcome === DeferredOutcome.Resolved;
|
||||
}
|
||||
|
||||
public get isSettled() {
|
||||
return !!this.outcome;
|
||||
}
|
||||
|
||||
public get value() {
|
||||
return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined;
|
||||
}
|
||||
|
||||
public readonly p: Promise<T>;
|
||||
|
||||
constructor() {
|
||||
this.p = new Promise<T>((c, e) => {
|
||||
this.completeCallback = c;
|
||||
this.errorCallback = e;
|
||||
});
|
||||
}
|
||||
|
||||
public complete(value: T) {
|
||||
return new Promise<void>(resolve => {
|
||||
this.completeCallback(value);
|
||||
this.outcome = { outcome: DeferredOutcome.Resolved, value };
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
public error(err: unknown) {
|
||||
return new Promise<void>(resolve => {
|
||||
this.errorCallback(err);
|
||||
this.outcome = { outcome: DeferredOutcome.Rejected, value: err };
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
return this.error(new CancellationError());
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
30
extensions/microsoft-authentication/src/common/env.ts
Normal file
30
extensions/microsoft-authentication/src/common/env.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
const VALID_DESKTOP_CALLBACK_SCHEMES = [
|
||||
'vscode',
|
||||
'vscode-insiders',
|
||||
// On Windows, some browsers don't seem to redirect back to OSS properly.
|
||||
// As a result, you get stuck in the auth flow. We exclude this from the
|
||||
// list until we can figure out a way to fix this behavior in browsers.
|
||||
// 'code-oss',
|
||||
'vscode-wsl',
|
||||
'vscode-exploration'
|
||||
];
|
||||
|
||||
export function isSupportedClient(uri: Uri): boolean {
|
||||
return (
|
||||
VALID_DESKTOP_CALLBACK_SCHEMES.includes(uri.scheme) ||
|
||||
// vscode.dev & insiders.vscode.dev
|
||||
/(?:^|\.)vscode\.dev$/.test(uri.authority) ||
|
||||
// github.dev & codespaces
|
||||
/(?:^|\.)github\.dev$/.test(uri.authority) ||
|
||||
// localhost
|
||||
/^localhost:\d+$/.test(uri.authority) ||
|
||||
// 127.0.0.1
|
||||
/^127\.0\.0\.1:\d+$/.test(uri.authority)
|
||||
);
|
||||
}
|
||||
@@ -5,14 +5,17 @@
|
||||
|
||||
import type { ILoopbackClient, ServerAuthorizationCodeResponse } from '@azure/msal-node';
|
||||
import type { UriEventHandler } from '../UriEventHandler';
|
||||
import { env, LogOutputChannel, Uri } from 'vscode';
|
||||
import { toPromise } from './async';
|
||||
import { Disposable, env, l10n, LogOutputChannel, Uri, window } from 'vscode';
|
||||
import { DeferredPromise, toPromise } from './async';
|
||||
import { isSupportedClient } from './env';
|
||||
|
||||
export interface ILoopbackClientAndOpener extends ILoopbackClient {
|
||||
openBrowser(url: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener {
|
||||
private _responseDeferred: DeferredPromise<ServerAuthorizationCodeResponse> | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _uriHandler: UriEventHandler,
|
||||
private readonly _redirectUri: string,
|
||||
@@ -20,17 +23,14 @@ export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener {
|
||||
) { }
|
||||
|
||||
async listenForAuthCode(): Promise<ServerAuthorizationCodeResponse> {
|
||||
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,
|
||||
};
|
||||
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.');
|
||||
}
|
||||
|
||||
getRedirectUri(): string {
|
||||
@@ -46,7 +46,93 @@ export class UriHandlerLoopbackClient implements ILoopbackClientAndOpener {
|
||||
async openBrowser(url: string): Promise<void> {
|
||||
const callbackUri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://vscode.microsoft-authentication`));
|
||||
|
||||
if (isSupportedClient(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(callbackUri.toString(true))}`);
|
||||
await env.openExternal(uri);
|
||||
}
|
||||
|
||||
private async _getCodeResponseFromUriHandler(): Promise<void> {
|
||||
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<void> {
|
||||
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<string | undefined>((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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user