Add auth library abstraction (#297)
This commit is contained in:
Родитель
04dcc447d6
Коммит
1be4dcc354
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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}`;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче