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