mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-21 09:08:53 +01:00
Add device code flow when not brokered (#270453)
fixes https://github.com/microsoft/vscode/issues/270452
This commit is contained in:
committed by
GitHub
parent
f0e1593e4f
commit
d751a3d55f
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { AccountInfo, AuthenticationResult, ClientAuthError, ClientAuthErrorCodes, ServerError, SilentFlowRequest } from '@azure/msal-node';
|
||||
import { AuthenticationChallenge, AuthenticationConstraint, AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, EventEmitter, ExtensionContext, ExtensionKind, l10n, LogOutputChannel, Uri, window } from 'vscode';
|
||||
import { AuthenticationChallenge, AuthenticationConstraint, AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, env, EventEmitter, ExtensionContext, ExtensionKind, l10n, LogOutputChannel, Uri, window } from 'vscode';
|
||||
import { Environment } from '@azure/ms-rest-azure-env';
|
||||
import { CachedPublicClientApplicationManager } from './publicClientCache';
|
||||
import { UriEventHandler } from '../UriEventHandler';
|
||||
@@ -16,7 +16,7 @@ import { IStoredSession } from '../AADHelper';
|
||||
import { ExtensionHost, getMsalFlows } from './flows';
|
||||
import { base64Decode } from './buffer';
|
||||
import { Config } from '../common/config';
|
||||
import { DEFAULT_REDIRECT_URI } from '../common/env';
|
||||
import { isSupportedClient } from '../common/env';
|
||||
|
||||
const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad';
|
||||
const MSA_PASSTHRU_TID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a';
|
||||
@@ -210,10 +210,12 @@ export class MsalAuthProvider implements AuthenticationProvider {
|
||||
};
|
||||
|
||||
const isNodeEnvironment = typeof process !== 'undefined' && typeof process?.versions?.node === 'string';
|
||||
const callbackUri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://vscode.microsoft-authentication`));
|
||||
const flows = getMsalFlows({
|
||||
extensionHost: isNodeEnvironment
|
||||
? this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote
|
||||
: ExtensionHost.WebWorker,
|
||||
supportedClient: isSupportedClient(callbackUri),
|
||||
isBrokerSupported: cachedPca.isBrokerAvailable
|
||||
});
|
||||
|
||||
@@ -235,7 +237,8 @@ export class MsalAuthProvider implements AuthenticationProvider {
|
||||
loginHint: options.account?.label,
|
||||
windowHandle: window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined,
|
||||
logger: this._logger,
|
||||
uriHandler: this._uriHandler
|
||||
uriHandler: this._uriHandler,
|
||||
callbackUri
|
||||
});
|
||||
|
||||
const session = this.sessionFromAuthenticationResult(result, scopeData.originalScopes);
|
||||
@@ -346,11 +349,13 @@ export class MsalAuthProvider implements AuthenticationProvider {
|
||||
};
|
||||
|
||||
const isNodeEnvironment = typeof process !== 'undefined' && typeof process?.versions?.node === 'string';
|
||||
const callbackUri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://vscode.microsoft-authentication`));
|
||||
const flows = getMsalFlows({
|
||||
extensionHost: isNodeEnvironment
|
||||
? this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote
|
||||
: ExtensionHost.WebWorker,
|
||||
isBrokerSupported: cachedPca.isBrokerAvailable
|
||||
isBrokerSupported: cachedPca.isBrokerAvailable,
|
||||
supportedClient: isSupportedClient(callbackUri)
|
||||
});
|
||||
|
||||
const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString();
|
||||
@@ -373,7 +378,8 @@ export class MsalAuthProvider implements AuthenticationProvider {
|
||||
windowHandle: window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined,
|
||||
logger: this._logger,
|
||||
uriHandler: this._uriHandler,
|
||||
claims: scopeData.claims
|
||||
claims: scopeData.claims,
|
||||
callbackUri
|
||||
};
|
||||
|
||||
const result = await flow.trigger(authRequest);
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { PublicClientApplication, AccountInfo, SilentFlowRequest, AuthenticationResult, InteractiveRequest, LogLevel, RefreshTokenRequest, BrokerOptions } from '@azure/msal-node';
|
||||
import { PublicClientApplication, AccountInfo, SilentFlowRequest, AuthenticationResult, InteractiveRequest, LogLevel, RefreshTokenRequest, BrokerOptions, DeviceCodeRequest } from '@azure/msal-node';
|
||||
import { NativeBrokerPlugin } from '@azure/msal-node-extensions';
|
||||
import { Disposable, SecretStorage, LogOutputChannel, window, ProgressLocation, l10n, EventEmitter, workspace } from 'vscode';
|
||||
import { raceCancellationAndTimeoutError } from '../common/async';
|
||||
import { Disposable, SecretStorage, LogOutputChannel, window, ProgressLocation, l10n, EventEmitter, workspace, env, Uri, UIKind } from 'vscode';
|
||||
import { DeferredPromise, raceCancellationAndTimeoutError } from '../common/async';
|
||||
import { SecretStorageCachePlugin } from '../common/cachePlugin';
|
||||
import { MsalLoggerOptions } from '../common/loggerOptions';
|
||||
import { ICachedPublicClientApplication } from '../common/publicClientCache';
|
||||
@@ -53,6 +53,8 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica
|
||||
let broker: BrokerOptions | undefined;
|
||||
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
||||
this._logger.info(`[${this._clientId}] Native Broker is only available on Windows and macOS`);
|
||||
} else if (env.uiKind === UIKind.Web) {
|
||||
this._logger.info(`[${this._clientId}] Native Broker is not available in web UI`);
|
||||
} else if (workspace.getConfiguration('microsoft-authentication').get<'msal' | 'msal-no-broker'>('implementation') === 'msal-no-broker') {
|
||||
this._logger.info(`[${this._clientId}] Native Broker disabled via settings`);
|
||||
} else {
|
||||
@@ -228,6 +230,81 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica
|
||||
return result;
|
||||
}
|
||||
|
||||
async acquireTokenByDeviceCode(request: Omit<DeviceCodeRequest, 'deviceCodeCallback'>): Promise<AuthenticationResult | null> {
|
||||
this._logger.debug(`[acquireTokenByDeviceCode] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}]`);
|
||||
const result = await this._sequencer.queue(async () => {
|
||||
const deferredPromise = new DeferredPromise<AuthenticationResult | null>();
|
||||
const result = await Promise.race([
|
||||
this._pca.acquireTokenByDeviceCode({
|
||||
...request,
|
||||
deviceCodeCallback: (response) => void this._deviceCodeCallback(response, deferredPromise)
|
||||
}),
|
||||
deferredPromise.p
|
||||
]);
|
||||
await deferredPromise.complete(result);
|
||||
// Force an update so that the account cache is updated.
|
||||
// TODO:@TylerLeonhardt The problem is, we use the sequencer for
|
||||
// change events but we _don't_ use it for the accounts cache.
|
||||
// We should probably use it for the accounts cache as well.
|
||||
await this._update();
|
||||
return result;
|
||||
});
|
||||
if (result) {
|
||||
if (this.isBrokerAvailable && result.account) {
|
||||
await this._accountAccess.setAllowedAccess(result.account, true);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _deviceCodeCallback(
|
||||
// MSAL doesn't expose this type...
|
||||
response: Parameters<DeviceCodeRequest['deviceCodeCallback']>[0],
|
||||
deferredPromise: DeferredPromise<AuthenticationResult | null>
|
||||
): Promise<void> {
|
||||
const button = l10n.t('Copy & Continue to Microsoft');
|
||||
const modalResult = await window.showInformationMessage(
|
||||
l10n.t({ message: 'Your Code: {0}', args: [response.userCode], comment: ['The {0} will be a code, e.g. 123-456'] }),
|
||||
{
|
||||
modal: true,
|
||||
detail: l10n.t('To finish authenticating, navigate to Microsoft and paste in the above one-time code.')
|
||||
}, button);
|
||||
|
||||
if (modalResult !== button) {
|
||||
this._logger.debug(`[deviceCodeCallback] [${this._clientId}] User cancelled the device code flow.`);
|
||||
deferredPromise.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
await env.clipboard.writeText(response.userCode);
|
||||
await env.openExternal(Uri.parse(response.verificationUri));
|
||||
await window.withProgress<void>({
|
||||
location: ProgressLocation.Notification,
|
||||
cancellable: true,
|
||||
title: l10n.t({
|
||||
message: 'Open [{0}]({0}) in a new tab and paste your one-time code: {1}',
|
||||
args: [response.verificationUri, response.userCode],
|
||||
comment: [
|
||||
'The [{0}]({0}) will be a url and the {1} will be a code, e.g. 123456',
|
||||
'{Locked="[{0}]({0})"}'
|
||||
]
|
||||
})
|
||||
}, async (_, token) => {
|
||||
const disposable = token.onCancellationRequested(() => {
|
||||
this._logger.debug(`[deviceCodeCallback] [${this._clientId}] Device code flow cancelled by user.`);
|
||||
deferredPromise.cancel();
|
||||
});
|
||||
try {
|
||||
await deferredPromise.p;
|
||||
this._logger.debug(`[deviceCodeCallback] [${this._clientId}] Device code flow completed successfully.`);
|
||||
} catch (error) {
|
||||
// Ignore errors here, they are handled at a higher scope
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeAccount(account: AccountInfo): Promise<void> {
|
||||
if (this.isBrokerAvailable) {
|
||||
return this._accountAccess.setAllowedAccess(account, false);
|
||||
|
||||
@@ -22,12 +22,15 @@ export const enum ExtensionHost {
|
||||
interface IMsalFlowOptions {
|
||||
supportsRemoteExtensionHost: boolean;
|
||||
supportsWebWorkerExtensionHost: boolean;
|
||||
supportsUnsupportedClient: boolean;
|
||||
supportsBroker: boolean;
|
||||
}
|
||||
|
||||
interface IMsalFlowTriggerOptions {
|
||||
cachedPca: ICachedPublicClientApplication;
|
||||
authority: string;
|
||||
scopes: string[];
|
||||
callbackUri: Uri;
|
||||
loginHint?: string;
|
||||
windowHandle?: Buffer;
|
||||
logger: LogOutputChannel;
|
||||
@@ -45,7 +48,9 @@ class DefaultLoopbackFlow implements IMsalFlow {
|
||||
label = 'default';
|
||||
options: IMsalFlowOptions = {
|
||||
supportsRemoteExtensionHost: false,
|
||||
supportsWebWorkerExtensionHost: false
|
||||
supportsWebWorkerExtensionHost: false,
|
||||
supportsUnsupportedClient: true,
|
||||
supportsBroker: true
|
||||
};
|
||||
|
||||
async trigger({ cachedPca, authority, scopes, claims, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise<AuthenticationResult> {
|
||||
@@ -73,12 +78,14 @@ class UrlHandlerFlow implements IMsalFlow {
|
||||
label = 'protocol handler';
|
||||
options: IMsalFlowOptions = {
|
||||
supportsRemoteExtensionHost: true,
|
||||
supportsWebWorkerExtensionHost: false
|
||||
supportsWebWorkerExtensionHost: false,
|
||||
supportsUnsupportedClient: false,
|
||||
supportsBroker: false
|
||||
};
|
||||
|
||||
async trigger({ cachedPca, authority, scopes, claims, loginHint, windowHandle, logger, uriHandler }: IMsalFlowTriggerOptions): Promise<AuthenticationResult> {
|
||||
async trigger({ cachedPca, authority, scopes, claims, loginHint, windowHandle, logger, uriHandler, callbackUri }: IMsalFlowTriggerOptions): Promise<AuthenticationResult> {
|
||||
logger.info('Trying protocol handler flow...');
|
||||
const loopbackClient = new UriHandlerLoopbackClient(uriHandler, DEFAULT_REDIRECT_URI, logger);
|
||||
const loopbackClient = new UriHandlerLoopbackClient(uriHandler, DEFAULT_REDIRECT_URI, callbackUri, logger);
|
||||
let redirectUri: string | undefined;
|
||||
if (cachedPca.isBrokerAvailable && process.platform === 'darwin') {
|
||||
redirectUri = Config.macOSBrokerRedirectUri;
|
||||
@@ -97,13 +104,34 @@ class UrlHandlerFlow implements IMsalFlow {
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceCodeFlow implements IMsalFlow {
|
||||
label = 'device code';
|
||||
options: IMsalFlowOptions = {
|
||||
supportsRemoteExtensionHost: true,
|
||||
supportsWebWorkerExtensionHost: false,
|
||||
supportsUnsupportedClient: true,
|
||||
supportsBroker: false
|
||||
};
|
||||
|
||||
async trigger({ cachedPca, authority, scopes, claims, logger }: IMsalFlowTriggerOptions): Promise<AuthenticationResult> {
|
||||
logger.info('Trying device code flow...');
|
||||
const result = await cachedPca.acquireTokenByDeviceCode({ scopes, authority, claims });
|
||||
if (!result) {
|
||||
throw new Error('Device code flow did not return a result');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const allFlows: IMsalFlow[] = [
|
||||
new DefaultLoopbackFlow(),
|
||||
new UrlHandlerFlow()
|
||||
new UrlHandlerFlow(),
|
||||
new DeviceCodeFlow()
|
||||
];
|
||||
|
||||
export interface IMsalFlowQuery {
|
||||
extensionHost: ExtensionHost;
|
||||
supportedClient: boolean;
|
||||
isBrokerSupported: boolean;
|
||||
}
|
||||
|
||||
@@ -119,12 +147,10 @@ export function getMsalFlows(query: IMsalFlowQuery): IMsalFlow[] {
|
||||
useFlow &&= flow.options.supportsWebWorkerExtensionHost;
|
||||
break;
|
||||
}
|
||||
useFlow &&= flow.options.supportsBroker || !query.isBrokerSupported;
|
||||
useFlow &&= flow.options.supportsUnsupportedClient || query.supportedClient;
|
||||
if (useFlow) {
|
||||
flows.push(flow);
|
||||
if (query.isBrokerSupported) {
|
||||
// If broker is supported, only use the first valid flow
|
||||
return flows;
|
||||
}
|
||||
}
|
||||
}
|
||||
return flows;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { getMsalFlows, ExtensionHost, IMsalFlowQuery } from '../flows';
|
||||
|
||||
suite('getMsalFlows', () => {
|
||||
test('should return all flows for local extension host with supported client and no broker', () => {
|
||||
const query: IMsalFlowQuery = {
|
||||
extensionHost: ExtensionHost.Local,
|
||||
supportedClient: true,
|
||||
isBrokerSupported: false
|
||||
};
|
||||
const flows = getMsalFlows(query);
|
||||
assert.strictEqual(flows.length, 3);
|
||||
assert.strictEqual(flows[0].label, 'default');
|
||||
assert.strictEqual(flows[1].label, 'protocol handler');
|
||||
assert.strictEqual(flows[2].label, 'device code');
|
||||
});
|
||||
|
||||
test('should return only default flow for local extension host with supported client and broker', () => {
|
||||
const query: IMsalFlowQuery = {
|
||||
extensionHost: ExtensionHost.Local,
|
||||
supportedClient: true,
|
||||
isBrokerSupported: true
|
||||
};
|
||||
const flows = getMsalFlows(query);
|
||||
assert.strictEqual(flows.length, 1);
|
||||
assert.strictEqual(flows[0].label, 'default');
|
||||
});
|
||||
|
||||
test('should return protocol handler and device code flows for remote extension host with supported client and no broker', () => {
|
||||
const query: IMsalFlowQuery = {
|
||||
extensionHost: ExtensionHost.Remote,
|
||||
supportedClient: true,
|
||||
isBrokerSupported: false
|
||||
};
|
||||
const flows = getMsalFlows(query);
|
||||
assert.strictEqual(flows.length, 2);
|
||||
assert.strictEqual(flows[0].label, 'protocol handler');
|
||||
assert.strictEqual(flows[1].label, 'device code');
|
||||
});
|
||||
|
||||
test('should return no flows for web worker extension host', () => {
|
||||
const query: IMsalFlowQuery = {
|
||||
extensionHost: ExtensionHost.WebWorker,
|
||||
supportedClient: true,
|
||||
isBrokerSupported: false
|
||||
};
|
||||
const flows = getMsalFlows(query);
|
||||
assert.strictEqual(flows.length, 0);
|
||||
});
|
||||
|
||||
test('should return only default and device code flows for local extension host with unsupported client and no broker', () => {
|
||||
const query: IMsalFlowQuery = {
|
||||
extensionHost: ExtensionHost.Local,
|
||||
supportedClient: false,
|
||||
isBrokerSupported: false
|
||||
};
|
||||
const flows = getMsalFlows(query);
|
||||
assert.strictEqual(flows.length, 2);
|
||||
assert.strictEqual(flows[0].label, 'default');
|
||||
assert.strictEqual(flows[1].label, 'device code');
|
||||
});
|
||||
|
||||
test('should return only device code flow for remote extension host with unsupported client and no broker', () => {
|
||||
const query: IMsalFlowQuery = {
|
||||
extensionHost: ExtensionHost.Remote,
|
||||
supportedClient: false,
|
||||
isBrokerSupported: false
|
||||
};
|
||||
const flows = getMsalFlows(query);
|
||||
assert.strictEqual(flows.length, 1);
|
||||
assert.strictEqual(flows[0].label, 'device code');
|
||||
});
|
||||
|
||||
test('should return default flow for local extension host with unsupported client and broker', () => {
|
||||
const query: IMsalFlowQuery = {
|
||||
extensionHost: ExtensionHost.Local,
|
||||
supportedClient: false,
|
||||
isBrokerSupported: true
|
||||
};
|
||||
const flows = getMsalFlows(query);
|
||||
assert.strictEqual(flows.length, 1);
|
||||
assert.strictEqual(flows[0].label, 'default');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user