This commit is contained in:
Will Lorey 2021-07-27 18:02:38 -07:00 коммит произвёл GitHub
Родитель 04dcc447d6
Коммит 1be4dcc354
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 426 добавлений и 330 удалений

Просмотреть файл

@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Environment } from "@azure/ms-rest-azure-env";
import { DeviceTokenCredentials as DeviceTokenCredentials2 } from '@azure/ms-rest-nodeauth';
import { TokenResponse } from "adal-node";
import { randomBytes } from "crypto";
import { ServerResponse } from "http";
import { DeviceTokenCredentials } from "ms-rest-azure";
import { env, ExtensionContext, OutputChannel, UIKind, window } from "vscode";
import { AzureAccount, AzureSession } from "../azure-account.api";
import { displayName, redirectUrlAAD, redirectUrlADFS } from "../constants";
import { ISubscriptionCache } from "./AzureLoginHelper";
import { getEnvironments } from "./environments";
import { getKey } from "./getKey";
import { CodeResult, createServer, createTerminateServer, RedirectResult, startServer } from './server';
export type AbstractLoginResult = TokenResponse[];
export type AbstractCredentials = DeviceTokenCredentials;
export type AbstractCredentials2 = DeviceTokenCredentials2;
export abstract class AuthProviderBase {
private terminateServer: (() => Promise<void>) | undefined;
protected outputChannel: OutputChannel;
constructor(context: ExtensionContext) {
this.outputChannel = window.createOutputChannel(displayName);
context.subscriptions.push(this.outputChannel);
}
public abstract loginWithoutLocalServer(clientId: string, environment: Environment, isAdfs: boolean, tenantId: string): Promise<AbstractLoginResult>;
public abstract loginWithAuthCode(code: string, redirectUrl: string, clientId: string, environment: Environment, tenantId: string): Promise<AbstractLoginResult>;
public abstract loginWithDeviceCode(environment: Environment, tenantId: string): Promise<AbstractLoginResult>;
public abstract loginSilent(environment: Environment, storedCreds: string, tenantId: string): Promise<AbstractLoginResult>;
public abstract getCredentials(environment: string, userId: string, tenantId: string): AbstractCredentials;
public abstract getCredentials2(environment: Environment, userId: string, tenantId: string): AbstractCredentials2;
public abstract updateSessions(environment: Environment, loginResult: AbstractLoginResult, sessions: AzureSession[]): Promise<void>;
public abstract clearTokenCache(): Promise<void>;
public abstract deleteRefreshTokens(): Promise<void>;
public async login(clientId: string, environment: Environment, isAdfs: boolean, tenantId: string, openUri: (url: string) => Promise<void>, redirectTimeout: () => Promise<void>): Promise<AbstractLoginResult> {
if (env.uiKind === UIKind.Web) {
return await this.loginWithoutLocalServer(clientId, environment, isAdfs, tenantId);
}
if (isAdfs && this.terminateServer) {
await this.terminateServer();
}
const nonce: string = randomBytes(16).toString('base64');
const { server, redirectPromise, codePromise } = createServer(nonce);
if (isAdfs) {
this.terminateServer = createTerminateServer(server);
}
try {
const port: number = await startServer(server, isAdfs);
await openUri(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const redirectTimer = setTimeout(() => redirectTimeout().catch(console.error), 10*1000);
const redirectResult: RedirectResult = await redirectPromise;
if ('err' in redirectResult) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { err, res } = redirectResult;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
res.end();
throw err;
}
clearTimeout(redirectTimer);
const host: string = redirectResult.req.headers.host || '';
const updatedPortStr: string = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1];
const updatedPort: number = updatedPortStr ? parseInt(updatedPortStr, 10) : port;
const state: string = `${updatedPort},${encodeURIComponent(nonce)}`;
const redirectUrl: string = isAdfs ? redirectUrlADFS : redirectUrlAAD;
const signInUrl: string = `${environment.activeDirectoryEndpointUrl}${isAdfs ? '' : `${tenantId}/`}oauth2/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&resource=${encodeURIComponent(environment.activeDirectoryResourceId)}&prompt=select_account`;
redirectResult.res.writeHead(302, { Location: signInUrl })
redirectResult.res.end();
const codeResult: CodeResult = await codePromise;
const serverResponse: ServerResponse = codeResult.res;
try {
if ('err' in codeResult) {
throw codeResult.err;
}
try {
return await this.loginWithAuthCode(codeResult.code, redirectUrl, clientId, environment, tenantId);
} finally {
serverResponse.writeHead(302, { Location: '/' });
serverResponse.end();
}
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
serverResponse.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
serverResponse.end();
throw err;
}
} finally {
setTimeout(() => {
server.close();
}, 5000);
}
}
public async initializeSessions(cache: ISubscriptionCache, api: AzureAccount): Promise<Record<string, AzureSession>> {
const sessions: Record<string, AzureSession> = {};
const environments: Environment[] = await getEnvironments();
for (const { session } of cache.subscriptions) {
const { environment, userId, tenantId } = session;
const key: string = getKey(environment, userId, tenantId);
const env: Environment | undefined = environments.find(e => e.name === environment);
if (!sessions[key] && env) {
sessions[key] = {
environment: env,
userId,
tenantId,
credentials: this.getCredentials(environment, userId, tenantId),
credentials2: this.getCredentials2(env, userId, tenantId)
};
api.sessions.push(sessions[key]);
}
}
return sessions;
}
}

Просмотреть файл

@ -5,34 +5,26 @@
import { SubscriptionClient, SubscriptionModels } from '@azure/arm-subscriptions';
import { Environment } from '@azure/ms-rest-azure-env';
import { DeviceTokenCredentials as DeviceTokenCredentials2, TokenCredentialsBase } from '@azure/ms-rest-nodeauth';
import { Logging, MemoryCache, TokenResponse } from 'adal-node';
import { DeviceTokenCredentials } from 'ms-rest-azure';
import { CancellationTokenSource, commands, ConfigurationTarget, Disposable, EventEmitter, ExtensionContext, MessageItem, OutputChannel, QuickPickItem, window, workspace, WorkspaceConfiguration } from 'vscode';
import { CancellationTokenSource, commands, ConfigurationTarget, Disposable, EventEmitter, ExtensionContext, MessageItem, QuickPickItem, window, workspace, WorkspaceConfiguration } from 'vscode';
import { AzureAccount, AzureLoginStatus, AzureResourceFilter, AzureSession, AzureSubscription } from '../azure-account.api';
import { createCloudConsole } from '../cloudConsole/cloudConsole';
import { azureCustomCloud, azurePPE, cacheKey, clientId, cloudSetting, commonTenantId, customCloudArmUrlSetting, displayName, extensionPrefix, resourceFilterSetting, staticEnvironments, tenantSetting } from '../constants';
import { azureCustomCloud, azurePPE, cacheKey, clientId, cloudSetting, commonTenantId, credentialsSection, customCloudArmUrlSetting, extensionPrefix, resourceFilterSetting, tenantSetting } from '../constants';
import { AzureLoginError, getErrorMessage } from '../errors';
import { TelemetryReporter } from '../telemetry';
import { listAll } from '../utils/arrayUtils';
import { KeyTar, tryGetKeyTar } from '../utils/keytar';
import { localize } from '../utils/localize';
import { openUri } from '../utils/openUri';
import { getSettingValue, getSettingWithPrefix } from '../utils/settingUtils';
import { delay } from '../utils/timeUtils';
import { AdalAuthProvider } from './adal/AdalAuthProvider';
import { AbstractLoginResult } from './AuthProviderBase';
import { getEnvironments, getSelectedEnvironment, isADFS } from './environments';
import { addFilter, getNewFilters, removeFilter } from './filters';
import { login } from './login';
import { loginWithDeviceCode } from './loginWithDeviceCode';
import { getKey } from './getKey';
import { checkRedirectServer } from './server';
import { addTokenToCache, clearTokenCache, deleteRefreshToken, getStoredCredentials, getTokenWithAuthorizationCode, ProxyTokenCache, storeRefreshToken, tokenFromRefreshToken, tokensFromToken } from './tokens';
import { waitUntilOnline } from './waitUntilOnline';
const staticEnvironmentNames: string[] = [
...staticEnvironments.map(environment => environment.name),
azureCustomCloud,
azurePPE
];
const environmentLabels: Record<string, string> = {
AzureCloud: localize('azure-account.azureCloud', 'Azure'),
AzureChinaCloud: localize('azure-account.azureChinaCloud', 'Azure China'),
@ -42,7 +34,8 @@ const environmentLabels: Record<string, string> = {
[azurePPE]: localize('azure-account.azurePPE', 'Azure PPE'),
};
const logVerbose: boolean = false;
const enableVerboseLogs: boolean = false;
const keytar: KeyTar | undefined = tryGetKeyTar();
interface IAzureAccountWriteable extends AzureAccount {
status: AzureLoginStatus;
@ -52,7 +45,7 @@ export interface ISubscriptionItem extends QuickPickItem {
subscription: AzureSubscription;
}
interface ISubscriptionCache {
export interface ISubscriptionCache {
subscriptions: {
session: {
environment: string;
@ -76,11 +69,11 @@ export class AzureLoginHelper {
private filtersTask: Promise<AzureResourceFilter[]> = Promise.resolve(<AzureResourceFilter[]>[]);
private onFiltersChanged: EventEmitter<void> = new EventEmitter<void>();
private tokenCache: MemoryCache = new MemoryCache();
private delayedTokenCache: ProxyTokenCache = new ProxyTokenCache(this.tokenCache);
private oldResourceFilter: string = '';
private doLogin: boolean = false;
private authProvider: AdalAuthProvider;
public api: AzureAccount = {
status: 'Initializing',
onStatusChanged: this.onStatusChanged.event,
@ -97,6 +90,8 @@ export class AzureLoginHelper {
};
constructor(private context: ExtensionContext, private reporter: TelemetryReporter) {
this.authProvider = new AdalAuthProvider(context, enableVerboseLogs);
context.subscriptions.push(commands.registerCommand('azure-account.login', () => this.login('login').catch(console.error)));
context.subscriptions.push(commands.registerCommand('azure-account.loginWithDeviceCode', () => this.login('loginWithDeviceCode').catch(console.error)));
context.subscriptions.push(commands.registerCommand('azure-account.logout', () => this.logout().catch(console.error)));
@ -117,23 +112,6 @@ export class AzureLoginHelper {
}));
this.initialize('activation', false, true)
.catch(console.error);
if (logVerbose) {
const outputChannel: OutputChannel = window.createOutputChannel(displayName);
context.subscriptions.push(outputChannel);
Logging.setLoggingOptions({
level: 3 /* Logging.LOGGING_LEVEL.VERBOSE */,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
log: (_level: any, message: any, error: any) => {
if (message) {
outputChannel.appendLine(message);
}
if (error) {
outputChannel.appendLine(error);
}
}
});
}
}
public async login(trigger: LoginTrigger): Promise<void> {
@ -166,16 +144,10 @@ export class AzureLoginHelper {
const isAdfs: boolean = isADFS(environment);
const useCodeFlow: boolean = trigger !== 'loginWithDeviceCode' && await checkRedirectServer(isAdfs);
path = useCodeFlow ? 'newLoginCodeFlow' : 'newLoginDeviceCode';
const tokenResponse: TokenResponse = useCodeFlow ?
await login(clientId, environment, isAdfs, tenantId, openUri, redirectTimeout) :
await loginWithDeviceCode(environment, tenantId);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const refreshToken: string = tokenResponse.refreshToken!;
const tokenResponses: TokenResponse[] = tenantId === commonTenantId ? await tokensFromToken(environment, tokenResponse) : [tokenResponse];
await storeRefreshToken(environment, refreshToken);
await this.updateSessions(environment, tokenResponses);
const loginResult: AbstractLoginResult = useCodeFlow ?
await this.authProvider.login(clientId, environment, isAdfs, tenantId, openUri, redirectTimeout) :
await this.authProvider.loginWithDeviceCode(environment, tenantId);
await this.updateSessions(environment, loginResult);
void this.sendLoginTelemetry(trigger, path, environmentName, 'success', undefined, true);
} catch (err) {
if (err instanceof AzureLoginError && err.reason) {
@ -217,11 +189,7 @@ export class AzureLoginHelper {
public async logout(): Promise<void> {
await this.api.waitForLogin();
// 'Azure' and 'AzureChina' are the old names for the 'AzureCloud' and 'AzureChinaCloud' environments
const allEnvironmentNames: string[] = staticEnvironmentNames.concat(['Azure', 'AzureChina', 'AzurePPE'])
for (const name of allEnvironmentNames) {
await deleteRefreshToken(name);
}
await this.authProvider.deleteRefreshTokens();
await this.clearSessions();
this.updateLoginStatus();
}
@ -274,56 +242,19 @@ export class AzureLoginHelper {
private async initialize(trigger: LoginTrigger, doLogin?: boolean, migrateToken?: boolean): Promise<void> {
let environmentName: string = 'uninitialized';
try {
const showTimingLogs: boolean = false;
const start: number = Date.now();
await this.loadSubscriptionCache();
showTimingLogs && console.log(`loadSubscriptionCache: ${(Date.now() - start) / 1000}s`);
const environment: Environment = await getSelectedEnvironment();
environmentName = environment.name;
const tenantId: string = getSettingValue(tenantSetting) || commonTenantId;
const storedCreds: string | undefined = await getStoredCredentials(environment, migrateToken);
showTimingLogs && console.log(`keytar: ${(Date.now() - start) / 1000}s`);
if (!storedCreds) {
throw new AzureLoginError(localize('azure-account.refreshTokenMissing', "Not signed in"));
}
await waitUntilOnline(environment, 5000);
this.beginLoggingIn();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let parsedCreds: any;
let tokenResponse: TokenResponse | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
parsedCreds = JSON.parse(storedCreds);
} catch {
tokenResponse = await tokenFromRefreshToken(environment, storedCreds, tenantId);
}
if (parsedCreds) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { redirectionUrl, code } = parsedCreds;
if (!redirectionUrl || !code) {
throw new AzureLoginError(localize('azure-account.malformedCredentials', "Stored credentials are invalid"));
}
tokenResponse = await getTokenWithAuthorizationCode(clientId, Environment.AzureCloud, redirectionUrl, tenantId, code);
}
if (!tokenResponse) {
throw new AzureLoginError(localize('azure-account.missingTokenResponse', "Using stored credentials failed"));
}
showTimingLogs && console.log(`tokenFromRefreshToken: ${(Date.now() - start) / 1000}s`);
// For testing
if (workspace.getConfiguration(extensionPrefix).get('testTokenFailure')) {
throw new AzureLoginError(localize('azure-account.testingAcquiringTokenFailed', "Testing: Acquiring token failed"));
}
const tokenResponses: TokenResponse[] = tenantId === commonTenantId ? await tokensFromToken(environment, tokenResponse) : [tokenResponse];
showTimingLogs && console.log(`tokensFromToken: ${(Date.now() - start) / 1000}s`);
await this.updateSessions(environment, tokenResponses);
showTimingLogs && console.log(`updateSessions: ${(Date.now() - start) / 1000}s`);
const loginResult: AbstractLoginResult = await this.authProvider.loginSilent(environment, storedCreds, tenantId);
await this.updateSessions(environment, loginResult);
void this.sendLoginTelemetry(trigger, 'tryExisting', environmentName, 'success', undefined, true);
} catch (err) {
await this.clearSessions(); // clear out cached data
@ -341,10 +272,10 @@ export class AzureLoginHelper {
}
private async loadSubscriptionCache(): Promise<void> {
const cache: ISubscriptionCache | undefined = this.context.globalState.get<ISubscriptionCache>(cacheKey);
const cache: ISubscriptionCache | undefined = this.context.globalState.get(cacheKey);
if (cache) {
(<IAzureAccountWriteable>this.api).status = 'LoggedIn';
const sessions: Record<string, AzureSession> = await this.initializeSessions(cache);
const sessions: Record<string, AzureSession> = await this.authProvider.initializeSessions(cache, this.api);
const subscriptions: AzureSubscription[] = this.initializeSubscriptions(cache, sessions);
this.initializeFilters(subscriptions);
}
@ -383,52 +314,13 @@ export class AzureLoginHelper {
}
}
private async initializeSessions(cache: ISubscriptionCache): Promise<Record<string, AzureSession>> {
const sessions: Record<string, AzureSession> = {};
for (const { session } of cache.subscriptions) {
const { environment, userId, tenantId } = session;
const key: string = `${environment} ${userId} ${tenantId}`;
const environments: Environment[] = await getEnvironments();
const env: Environment | undefined = environments.find(e => e.name === environment);
if (!sessions[key] && env) {
sessions[key] = {
environment: env,
userId,
tenantId,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
credentials: new DeviceTokenCredentials({ environment: (<any>Environment)[environment], username: userId, clientId, tokenCache: this.delayedTokenCache, domain: tenantId }),
credentials2: new DeviceTokenCredentials2(clientId, tenantId, userId, undefined, env, this.delayedTokenCache)
};
this.api.sessions.push(sessions[key]);
}
}
return sessions;
}
private async updateSessions(environment: Environment, tokenResponses: TokenResponse[]): Promise<void> {
await clearTokenCache(this.tokenCache);
for (const tokenResponse of tokenResponses) {
await addTokenToCache(environment, this.tokenCache, tokenResponse);
}
/* eslint-disable @typescript-eslint/no-non-null-assertion */
this.delayedTokenCache.initEnd!();
const sessions: AzureSession[] = this.api.sessions;
sessions.splice(0, sessions.length, ...tokenResponses.map<AzureSession>(tokenResponse => ({
environment,
userId: tokenResponse.userId!,
tenantId: tokenResponse.tenantId!,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
credentials: new DeviceTokenCredentials({ environment: (<any>environment), username: tokenResponse.userId, clientId, tokenCache: this.delayedTokenCache, domain: tokenResponse.tenantId }),
credentials2: new DeviceTokenCredentials2(clientId, tokenResponse.tenantId, tokenResponse.userId, undefined, environment, this.delayedTokenCache)
})));
private async updateSessions(environment: Environment, loginResult: AbstractLoginResult): Promise<void> {
await this.authProvider.updateSessions(environment, loginResult, this.api.sessions);
this.onSessionsChanged.fire();
/* eslint-enable @typescript-eslint/no-non-null-assertion */
}
private async clearSessions(): Promise<void> {
await clearTokenCache(this.tokenCache);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.delayedTokenCache.initEnd!();
await this.authProvider.clearTokenCache();
const sessions: AzureSession[] = this.api.sessions;
sessions.length = 0;
this.onSessionsChanged.fire();
@ -445,7 +337,7 @@ export class AzureLoginHelper {
private initializeSubscriptions(cache: ISubscriptionCache, sessions: Record<string, AzureSession>): AzureSubscription[] {
const subscriptions: AzureSubscription[] = cache.subscriptions.map<AzureSubscription>(({ session, subscription }) => {
const { environment, userId, tenantId } = session;
const key: string = `${environment} ${userId} ${tenantId}`;
const key: string = getKey(environment, userId, tenantId);
return {
session: sessions[key],
subscription
@ -528,8 +420,7 @@ export class AzureLoginHelper {
private async loadSubscriptions(): Promise<AzureSubscription[]> {
const lists: AzureSubscription[][] = await Promise.all(this.api.sessions.map(session => {
const credentials: TokenCredentialsBase = session.credentials2;
const client: SubscriptionClient = new SubscriptionClient(credentials, { baseUri: session.environment.resourceManagerEndpointUrl });
const client: SubscriptionClient = new SubscriptionClient(session.credentials2, { baseUri: session.environment.resourceManagerEndpointUrl });
return listAll(client.subscriptions, client.subscriptions.list())
.then(list => list.map(subscription => ({
session,
@ -635,3 +526,27 @@ function getCurrentTarget(config: { key: string; defaultValue?: unknown; globalV
}
return ConfigurationTarget.Global;
}
async function getStoredCredentials(environment: Environment, migrateToken?: boolean): Promise<string | undefined> {
if (!keytar) {
return undefined;
}
try {
if (migrateToken) {
const token = await keytar.getPassword('VSCode Public Azure', 'Refresh Token');
if (token) {
if (!await keytar.getPassword(credentialsSection, 'Azure')) {
await keytar.setPassword(credentialsSection, 'Azure', token);
}
await keytar.deletePassword('VSCode Public Azure', 'Refresh Token');
}
}
} catch {
// ignore
}
try {
return await keytar.getPassword(credentialsSection, environment.name) || undefined;
} catch {
// ignore
}
}

Просмотреть файл

@ -0,0 +1,200 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Environment } from "@azure/ms-rest-azure-env";
import { DeviceTokenCredentials as DeviceTokenCredentials2 } from '@azure/ms-rest-nodeauth';
import { Logging, MemoryCache, TokenResponse, UserCodeInfo } from "adal-node";
import { randomBytes } from "crypto";
import { DeviceTokenCredentials } from "ms-rest-azure";
import { Disposable, env, ExtensionContext, Uri, window } from "vscode";
import { AzureSession } from "../../azure-account.api";
import { azureCustomCloud, azurePPE, clientId, redirectUrlAAD, staticEnvironments } from "../../constants";
import { AzureLoginError } from "../../errors";
import { localize } from "../../utils/localize";
import { timeout } from "../../utils/timeUtils";
import { AbstractCredentials, AbstractCredentials2, AbstractLoginResult, AuthProviderBase } from "../AuthProviderBase";
import { getCallbackEnvironment, parseQuery, UriEventHandler } from "../login";
import { getUserCode, showDeviceCodeMessage } from "../loginWithDeviceCode";
import { addTokenToCache, clearTokenCache, deleteRefreshToken, getTokenResponse, getTokensFromToken, getTokenWithAuthorizationCode, ProxyTokenCache, storeRefreshToken, tokenFromRefreshToken } from "../tokens";
const staticEnvironmentNames: string[] = [
...staticEnvironments.map(environment => environment.name),
azureCustomCloud,
azurePPE
];
export class AdalAuthProvider extends AuthProviderBase {
private tokenCache: MemoryCache = new MemoryCache();
private delayedTokenCache: ProxyTokenCache = new ProxyTokenCache(this.tokenCache);
private handler: UriEventHandler = new UriEventHandler();
constructor(context: ExtensionContext, enableVerboseLogs: boolean) {
super(context);
window.registerUriHandler(this.handler);
Logging.setLoggingOptions({
level: enableVerboseLogs ?
3 /* Logging.LOGGING_LEVEL.VERBOSE */ :
0 /* Logging.LOGGING_LEVEL.ERROR */,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
log: (_level: any, message: any, error: any) => {
if (message) {
super.outputChannel.appendLine(message);
}
if (error) {
super.outputChannel.appendLine(error);
}
}
});
}
public async loginWithoutLocalServer(clientId: string, environment: Environment, isAdfs: boolean, tenantId: string): Promise<AbstractLoginResult> {
const callbackUri: Uri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://ms-vscode.azure-account`));
const nonce: string = randomBytes(16).toString('base64');
const port: string | number = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
const callbackEnvironment: string = getCallbackEnvironment(callbackUri);
const state: string = `${callbackEnvironment}${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
const signInUrl: string = `${environment.activeDirectoryEndpointUrl}${isAdfs ? '' : `${tenantId}/`}oauth2/authorize`;
let uri: Uri = Uri.parse(signInUrl);
uri = uri.with({
query: `response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${redirectUrlAAD}&state=${state}&resource=${environment.activeDirectoryResourceId}&prompt=select_account`
});
void env.openExternal(uri);
const timeoutPromise = new Promise((_resolve: (value: TokenResponse) => void, reject) => {
const wait = setTimeout(() => {
clearTimeout(wait);
reject('Login timed out.');
}, 1000 * 60 * 5)
});
const tokenResponse: TokenResponse = await Promise.race([this.exchangeCodeForToken(clientId, environment, tenantId, redirectUrlAAD, state), timeoutPromise]);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await storeRefreshToken(environment, tokenResponse.refreshToken!);
return getTokensFromToken(environment, tenantId, tokenResponse);
}
public async loginWithAuthCode(code: string, redirectUrl: string, clientId: string, environment: Environment, tenantId: string): Promise<AbstractLoginResult> {
const tokenResponse: TokenResponse = await getTokenWithAuthorizationCode(clientId, environment, redirectUrl, tenantId, code);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await storeRefreshToken(environment, tokenResponse.refreshToken!);
return getTokensFromToken(environment, tenantId, tokenResponse);
}
public async loginWithDeviceCode(environment: Environment, tenantId: string): Promise<AbstractLoginResult> {
const userCode: UserCodeInfo = await getUserCode(environment, tenantId);
const messageTask: Promise<void> = showDeviceCodeMessage(userCode);
const tokenResponseTask: Promise<TokenResponse> = getTokenResponse(environment, tenantId, userCode);
const tokenResponse: TokenResponse = await Promise.race([tokenResponseTask, messageTask.then(() => Promise.race([tokenResponseTask, timeout(3 * 60 * 1000)]))]); // 3 minutes
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await storeRefreshToken(environment, tokenResponse.refreshToken!);
return getTokensFromToken(environment, tenantId, tokenResponse);
}
public async loginSilent(environment: Environment, storedCreds: string, tenantId: string): Promise<AbstractLoginResult> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let parsedCreds: any;
let tokenResponse: TokenResponse | null = null;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
parsedCreds = JSON.parse(storedCreds);
} catch {
tokenResponse = await tokenFromRefreshToken(environment, storedCreds, tenantId)
}
if (parsedCreds) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { redirectionUrl, code } = parsedCreds;
if (!redirectionUrl || !code) {
throw new AzureLoginError(localize('azure-account.malformedCredentials', "Stored credentials are invalid"));
}
tokenResponse = await getTokenWithAuthorizationCode(clientId, Environment.AzureCloud, redirectionUrl, tenantId, code);
}
if (!tokenResponse) {
throw new AzureLoginError(localize('azure-account.missingTokenResponse', "Using stored credentials failed"));
}
return getTokensFromToken(environment, tenantId, tokenResponse);
}
public getCredentials(environment: string, userId: string, tenantId: string): AbstractCredentials {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
return new DeviceTokenCredentials({ environment: (<any>Environment)[environment], username: userId, clientId, tokenCache: this.delayedTokenCache, domain: tenantId });
}
public getCredentials2(environment: Environment, userId: string, tenantId: string): AbstractCredentials2 {
return new DeviceTokenCredentials2(clientId, tenantId, userId, undefined, environment, this.delayedTokenCache);
}
public async updateSessions(environment: Environment, loginResult: AbstractLoginResult, sessions: AzureSession[]): Promise<void> {
await clearTokenCache(this.tokenCache);
for (const tokenResponse of loginResult) {
await addTokenToCache(environment, this.tokenCache, tokenResponse);
}
/* eslint-disable @typescript-eslint/no-non-null-assertion */
this.delayedTokenCache.initEnd!();
sessions.splice(0, sessions.length, ...loginResult.map<AzureSession>(tokenResponse => ({
environment,
userId: tokenResponse.userId!,
tenantId: tokenResponse.tenantId!,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
credentials: this.getCredentials(<any>environment, tokenResponse.userId!, tokenResponse.tenantId!),
credentials2: this.getCredentials2(environment, tokenResponse.userId!, tokenResponse.tenantId!)
})));
/* eslint-enable @typescript-eslint/no-non-null-assertion */
}
public async clearTokenCache(): Promise<void> {
await clearTokenCache(this.tokenCache);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.delayedTokenCache.initEnd!();
}
public async deleteRefreshTokens(): Promise<void> {
// 'Azure' and 'AzureChina' are the old names for the 'AzureCloud' and 'AzureChinaCloud' environments
const allEnvironmentNames: string[] = staticEnvironmentNames.concat(['Azure', 'AzureChina', 'AzurePPE'])
for (const name of allEnvironmentNames) {
await deleteRefreshToken(name);
}
}
private async exchangeCodeForToken(clientId: string, environment: Environment, tenantId: string, callbackUri: string, state: string): Promise<TokenResponse> {
let uriEventListener: Disposable;
return new Promise((resolve: (value: TokenResponse) => void , reject) => {
uriEventListener = this.handler.event(async (uri: Uri) => {
try {
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
const query = parseQuery(uri);
const code = query.code;
// Workaround double encoding issues of state
if (query.state !== state && decodeURIComponent(query.state) !== state) {
throw new Error('State does not match.');
}
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
resolve(await getTokenWithAuthorizationCode(clientId, environment, callbackUri, tenantId, code));
} catch (err) {
reject(err);
}
});
}).then(result => {
uriEventListener.dispose()
return result;
}).catch(err => {
uriEventListener.dispose();
throw err;
});
}
}

8
src/login/getKey.ts Normal file
Просмотреть файл

@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function getKey(environment: string, userId: string, tenantId: string): string {
return `${environment} ${userId} ${tenantId}`;
}

145
src/login/login.ts Executable file → Normal file
Просмотреть файл

@ -1,31 +1,18 @@
//#!/usr/bin/env ts-node
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Environment } from '@azure/ms-rest-azure-env';
import { TokenResponse } from 'adal-node';
import * as crypto from 'crypto';
import { ServerResponse } from 'http';
import * as vscode from 'vscode';
import { redirectUrlAAD, redirectUrlADFS } from '../constants';
import { CodeResult, createServer, createTerminateServer, RedirectResult, startServer } from './server';
import { getTokenWithAuthorizationCode } from './tokens';
class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
public handleUri(uri: vscode.Uri) {
export class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
public handleUri(uri: vscode.Uri): void {
this.fire(uri);
}
}
const handler: UriEventHandler = new UriEventHandler();
vscode.window.registerUriHandler(handler);
let terminateServer: () => Promise<void>;
/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
function parseQuery(uri: vscode.Uri): any {
export function parseQuery(uri: vscode.Uri): any {
return uri.query.split('&').reduce((prev: any, current) => {
const queryString: string[] = current.split('=');
prev[queryString[0]] = queryString[1];
@ -34,36 +21,7 @@ function parseQuery(uri: vscode.Uri): any {
}
/* eslint-enable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
async function exchangeCodeForToken(clientId: string, environment: Environment, tenantId: string, callbackUri: string, state: string): Promise<TokenResponse> {
let uriEventListener: vscode.Disposable;
return new Promise((resolve: (value: TokenResponse) => void , reject) => {
uriEventListener = handler.event(async (uri: vscode.Uri) => {
try {
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
const query = parseQuery(uri);
const code = query.code;
// Workaround double encoding issues of state
if (query.state !== state && decodeURIComponent(query.state) !== state) {
throw new Error('State does not match.');
}
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
resolve(await getTokenWithAuthorizationCode(clientId, environment, callbackUri, tenantId, code));
} catch (err) {
reject(err);
}
});
}).then(result => {
uriEventListener.dispose()
return result;
}).catch(err => {
uriEventListener.dispose();
throw err;
});
}
function getCallbackEnvironment(callbackUri: vscode.Uri): string {
export function getCallbackEnvironment(callbackUri: vscode.Uri): string {
if (callbackUri.authority.endsWith('.workspaces.github.com') || callbackUri.authority.endsWith('.github.dev')) {
return `${callbackUri.authority},`;
}
@ -81,98 +39,3 @@ function getCallbackEnvironment(callbackUri: vscode.Uri): string {
return '';
}
}
async function loginWithoutLocalServer(clientId: string, environment: Environment, adfs: boolean, tenantId: string): Promise<TokenResponse> {
const callbackUri: vscode.Uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://ms-vscode.azure-account`));
const nonce: string = crypto.randomBytes(16).toString('base64');
const port: string | number = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
const callbackEnvironment: string = getCallbackEnvironment(callbackUri);
const state: string = `${callbackEnvironment}${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
const signInUrl: string = `${environment.activeDirectoryEndpointUrl}${adfs ? '' : `${tenantId}/`}oauth2/authorize`;
let uri: vscode.Uri = vscode.Uri.parse(signInUrl);
uri = uri.with({
query: `response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${redirectUrlAAD}&state=${state}&resource=${environment.activeDirectoryResourceId}&prompt=select_account`
});
void vscode.env.openExternal(uri);
const timeoutPromise = new Promise((_resolve: (value: TokenResponse) => void, reject) => {
const wait = setTimeout(() => {
clearTimeout(wait);
reject('Login timed out.');
}, 1000 * 60 * 5)
});
return Promise.race([exchangeCodeForToken(clientId, environment, tenantId, redirectUrlAAD, state), timeoutPromise]);
}
export async function login(clientId: string, environment: Environment, adfs: boolean, tenantId: string, openUri: (url: string) => Promise<void>, redirectTimeout: () => Promise<void>): Promise<TokenResponse> {
if (vscode.env.uiKind === vscode.UIKind.Web) {
return loginWithoutLocalServer(clientId, environment, adfs, tenantId);
}
if (adfs && terminateServer) {
await terminateServer();
}
const nonce: string = crypto.randomBytes(16).toString('base64');
const { server, redirectPromise, codePromise } = createServer(nonce);
if (adfs) {
terminateServer = createTerminateServer(server);
}
try {
const port: number = await startServer(server, adfs);
await openUri(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const redirectTimer = setTimeout(() => redirectTimeout().catch(console.error), 10*1000);
const redirectResult: RedirectResult = await redirectPromise;
if ('err' in redirectResult) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { err, res } = redirectResult;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
res.end();
throw err;
}
clearTimeout(redirectTimer);
const host: string = redirectResult.req.headers.host || '';
const updatedPortStr: string = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1];
const updatedPort: number = updatedPortStr ? parseInt(updatedPortStr, 10) : port;
const state: string = `${updatedPort},${encodeURIComponent(nonce)}`;
const redirectUrl: string = adfs ? redirectUrlADFS : redirectUrlAAD;
const signInUrl: string = `${environment.activeDirectoryEndpointUrl}${adfs ? '' : `${tenantId}/`}oauth2/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&resource=${encodeURIComponent(environment.activeDirectoryResourceId)}&prompt=select_account`;
redirectResult.res.writeHead(302, { Location: signInUrl })
redirectResult.res.end();
const codeResult: CodeResult = await codePromise;
const serverResponse: ServerResponse = codeResult.res;
try {
if ('err' in codeResult) {
throw codeResult.err;
}
const tokenResponse: TokenResponse = await getTokenWithAuthorizationCode(clientId, environment, redirectUrl, tenantId, codeResult.code);
serverResponse.writeHead(302, { Location: '/' });
serverResponse.end();
return tokenResponse;
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
serverResponse.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` });
serverResponse.end();
throw err;
}
} finally {
setTimeout(() => {
server.close();
}, 5000);
}
}
if (require.main === module) {
login('aebc6443-996d-45c2-90f0-388ff96faa56', Environment.AzureCloud, false, 'common', async uri => console.log(`Open: ${uri}`), async () => console.log('Browser did not connect to local server within 10 seconds.'))
.catch(console.error);
}

Просмотреть файл

@ -4,22 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { Environment } from "@azure/ms-rest-azure-env";
import { AuthenticationContext, MemoryCache, TokenResponse, UserCodeInfo } from "adal-node";
import { AuthenticationContext, MemoryCache } from "@azure/ms-rest-nodeauth/node_modules/adal-node";
import { UserCodeInfo } from "adal-node";
import { env, MessageItem, window } from "vscode";
import { clientId } from "../constants";
import { AzureLoginError } from "../errors";
import { localize } from "../utils/localize";
import { openUri } from "../utils/openUri";
import { timeout } from "../utils/timeUtils";
export async function loginWithDeviceCode(environment: Environment, tenantId: string): Promise<TokenResponse> {
const userCode: UserCodeInfo = await getUserCode(environment, tenantId);
const messageTask: Promise<void> = showDeviceCodeMessage(userCode);
const tokenResponseTask: Promise<TokenResponse> = getTokenResponse(environment, tenantId, userCode);
return Promise.race([tokenResponseTask, messageTask.then(() => Promise.race([tokenResponseTask, timeout(3 * 60 * 1000)]))]); // 3 minutes
}
async function showDeviceCodeMessage(userCode: UserCodeInfo): Promise<void> {
export async function showDeviceCodeMessage(userCode: UserCodeInfo): Promise<void> {
const copyAndOpen: MessageItem = { title: localize('azure-account.copyAndOpen', "Copy & Open") };
const response: MessageItem | undefined = await window.showInformationMessage(userCode.message, copyAndOpen);
if (response === copyAndOpen) {
@ -30,7 +23,7 @@ async function showDeviceCodeMessage(userCode: UserCodeInfo): Promise<void> {
}
}
async function getUserCode(environment: Environment, tenantId: string): Promise<UserCodeInfo> {
export async function getUserCode(environment: Environment, tenantId: string): Promise<UserCodeInfo> {
return new Promise<UserCodeInfo>((resolve, reject) => {
const cache: MemoryCache = new MemoryCache();
const context: AuthenticationContext = new AuthenticationContext(`${environment.activeDirectoryEndpointUrl}${tenantId}`, environment.validateAuthority, cache);
@ -43,19 +36,3 @@ async function getUserCode(environment: Environment, tenantId: string): Promise<
});
});
}
async function getTokenResponse(environment: Environment, tenantId: string, userCode: UserCodeInfo): Promise<TokenResponse> {
return new Promise<TokenResponse>((resolve, reject) => {
const tokenCache: MemoryCache = new MemoryCache();
const context: AuthenticationContext = new AuthenticationContext(`${environment.activeDirectoryEndpointUrl}${tenantId}`, environment.validateAuthority, tokenCache);
context.acquireTokenWithDeviceCode(`${environment.managementEndpointUrl}`, clientId, userCode, (err, tokenResponse) => {
if (err) {
reject(new AzureLoginError(localize('azure-account.tokenFailed', "Acquiring token with device code failed"), err));
} else if (tokenResponse.error) {
reject(new AzureLoginError(localize('azure-account.tokenFailed', "Acquiring token with device code failed"), tokenResponse));
} else {
resolve(<TokenResponse>tokenResponse);
}
});
});
}

Просмотреть файл

@ -6,8 +6,8 @@
import { SubscriptionClient, SubscriptionModels } from "@azure/arm-subscriptions";
import { Environment } from "@azure/ms-rest-azure-env";
import { DeviceTokenCredentials as DeviceTokenCredentials2 } from '@azure/ms-rest-nodeauth';
import { AuthenticationContext, MemoryCache, TokenResponse } from "adal-node";
import { clientId, credentialsSection } from "../constants";
import { AuthenticationContext, MemoryCache, TokenResponse, UserCodeInfo } from "adal-node";
import { clientId, commonTenantId, credentialsSection } from "../constants";
import { AzureLoginError } from "../errors";
import { listAll } from "../utils/arrayUtils";
import { tryGetKeyTar } from "../utils/keytar";
@ -47,30 +47,6 @@ export class ProxyTokenCache {
/* eslint-enable */
}
export async function getStoredCredentials(environment: Environment, migrateToken?: boolean): Promise<string | undefined> {
if (!keytar) {
return undefined;
}
try {
if (migrateToken) {
const token = await keytar.getPassword('VSCode Public Azure', 'Refresh Token');
if (token) {
if (!await keytar.getPassword(credentialsSection, 'Azure')) {
await keytar.setPassword(credentialsSection, 'Azure', token);
}
await keytar.deletePassword('VSCode Public Azure', 'Refresh Token');
}
}
} catch {
// ignore
}
try {
return await keytar.getPassword(credentialsSection, environment.name) || undefined;
} catch {
// ignore
}
}
export async function storeRefreshToken(environment: Environment, token: string): Promise<void> {
if (keytar) {
try {
@ -188,3 +164,23 @@ export async function getTokenWithAuthorizationCode(clientId: string, environmen
});
});
}
export async function getTokensFromToken(environment: Environment, tenantId: string, tokenResponse: TokenResponse): Promise<TokenResponse[]> {
return tenantId === commonTenantId ? await tokensFromToken(environment, tokenResponse) : [tokenResponse];
}
export async function getTokenResponse(environment: Environment, tenantId: string, userCode: UserCodeInfo): Promise<TokenResponse> {
return new Promise<TokenResponse>((resolve, reject) => {
const tokenCache: MemoryCache = new MemoryCache();
const context: AuthenticationContext = new AuthenticationContext(`${environment.activeDirectoryEndpointUrl}${tenantId}`, environment.validateAuthority, tokenCache);
context.acquireTokenWithDeviceCode(`${environment.managementEndpointUrl}`, clientId, userCode, (err, tokenResponse) => {
if (err) {
reject(new AzureLoginError(localize('azure-account.tokenFailed', "Acquiring token with device code failed"), err));
} else if (tokenResponse.error) {
reject(new AzureLoginError(localize('azure-account.tokenFailed', "Acquiring token with device code failed"), tokenResponse));
} else {
resolve(<TokenResponse>tokenResponse);
}
});
});
}