Add MSAL Authentication Library support (#21024)

This commit is contained in:
Christopher Suh
2022-11-23 17:06:44 -05:00
committed by GitHub
parent fba47815e2
commit 86c3f315f2
32 changed files with 1502 additions and 320 deletions

View File

@@ -5,7 +5,6 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import {
@@ -15,51 +14,55 @@ import {
Resource,
Tenant
} from 'azurecore';
import { Deferred } from '../interfaces';
import * as url from 'url';
import * as Constants from '../../constants';
import { SimpleTokenCache } from '../simpleTokenCache';
import { MemoryDatabase } from '../utils/memoryDatabase';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { Logger } from '../../utils/Logger';
import * as qs from 'qs';
import { AzureAuthError } from './azureAuthError';
import { AccountInfo, AuthenticationResult, InteractionRequiredAuthError, PublicClientApplication } from '@azure/msal-node';
const localize = nls.loadMessageBundle();
export abstract class AzureAuth implements vscode.Disposable {
public static ACCOUNT_VERSION = '2.0';
protected readonly memdb = new MemoryDatabase<string>();
protected readonly WorkSchoolAccountType: string = 'work_school';
protected readonly MicrosoftAccountType: string = 'microsoft';
protected readonly loginEndpointUrl: string;
public readonly commonTenant: Tenant;
public readonly organizationTenant: Tenant;
protected readonly redirectUri: string;
protected readonly scopes: string[];
protected readonly scopesString: string;
protected readonly clientId: string;
protected readonly resources: Resource[];
private _authLibrary: string | undefined;
constructor(
protected readonly metadata: AzureAccountProviderMetadata,
protected readonly tokenCache: SimpleTokenCache,
protected readonly context: vscode.ExtensionContext,
protected clientApplication: PublicClientApplication,
protected readonly uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
protected readonly authType: AzureAuthType,
public readonly userFriendlyName: string
public readonly userFriendlyName: string,
public readonly authLibrary: string
) {
this._authLibrary = authLibrary;
this.loginEndpointUrl = this.metadata.settings.host;
this.commonTenant = {
id: 'common',
displayName: 'common',
};
this.organizationTenant = {
id: 'organizations',
displayName: 'organizations',
};
this.redirectUri = this.metadata.settings.redirectUri;
this.clientId = this.metadata.settings.clientId;
this.resources = [
this.metadata.settings.armResource,
this.metadata.settings.graphResource,
@@ -100,19 +103,39 @@ export abstract class AzureAuth implements vscode.Disposable {
if (!this.metadata.settings.microsoftResource) {
throw new Error(localize('noMicrosoftResource', "Provider '{0}' does not have a Microsoft resource endpoint defined.", this.metadata.displayName));
}
const result = await this.login(this.commonTenant, this.metadata.settings.microsoftResource);
loginComplete = result.authComplete;
if (!result?.response) {
Logger.error('Authentication failed');
return {
canceled: false
if (this._authLibrary === Constants.AuthLibrary.MSAL) {
const result = await this.loginMsal(this.organizationTenant, this.metadata.settings.microsoftResource);
loginComplete = result.authComplete;
if (!result?.response || !result.response?.account) {
Logger.error(`Authentication failed: ${loginComplete}`);
return {
canceled: false
};
}
const token: Token = {
token: result.response.accessToken,
key: result.response.account.homeAccountId,
tokenType: result.response.tokenType
};
const tokenClaims = <TokenClaims>result.response.idTokenClaims;
const account = await this.hydrateAccount(token, tokenClaims);
loginComplete?.resolve();
return account;
} else {// fallback to ADAL as default
const result = await this.loginAdal(this.commonTenant, this.metadata.settings.microsoftResource);
loginComplete = result.authComplete;
if (!result?.response) {
Logger.error('Authentication failed - no response');
return {
canceled: false
};
}
const account = await this.hydrateAccount(result.response.accessToken, result.response.tokenClaims);
loginComplete?.resolve();
return account;
}
const account = await this.hydrateAccount(result.response.accessToken, result.response.tokenClaims);
loginComplete?.resolve();
return account;
} catch (ex) {
Logger.error('Login failed');
Logger.error(`Login failed: ${ex}`);
if (ex instanceof AzureAuthError) {
if (loginComplete) {
loginComplete.reject(ex);
@@ -133,9 +156,9 @@ export abstract class AzureAuth implements vscode.Disposable {
}
}
public async refreshAccess(account: AzureAccount): Promise<AzureAccount> {
public async refreshAccessAdal(account: AzureAccount): Promise<AzureAccount> {
// Deprecated account - delete it.
if (account.key.accountVersion !== AzureAuth.ACCOUNT_VERSION) {
if (account.key.accountVersion !== Constants.AccountVersion) {
account.delete = true;
return account;
}
@@ -144,7 +167,7 @@ export abstract class AzureAuth implements vscode.Disposable {
// We want to return the one that owns the Azure account.
// Not doing so can result in token being issued for the wrong tenant
const tenant = account.properties.owningTenant;
const tokenResult = await this.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.MicrosoftResourceManagement);
const tokenResult = await this.getAccountSecurityTokenAdal(account, tenant.id, azdata.AzureResource.MicrosoftResourceManagement);
if (!tokenResult) {
account.isStale = true;
return account;
@@ -154,23 +177,28 @@ export abstract class AzureAuth implements vscode.Disposable {
} catch (ex) {
if (ex instanceof AzureAuthError) {
void vscode.window.showErrorMessage(ex.message);
Logger.error(ex.originalMessageAndException);
Logger.error(`Error refreshing access for account ${account.displayInfo.displayName}`, ex.originalMessageAndException);
} else {
Logger.error(ex);
}
Logger.error(ex);
account.isStale = true;
return account;
}
}
public async hydrateAccount(token: Token | AccessToken, tokenClaims: TokenClaims): Promise<AzureAccount> {
const tenants = await this.getTenants({ ...token });
const account = this.createAccount(tokenClaims, token.key, tenants);
let account: azdata.Account;
if (this._authLibrary === Constants.AuthLibrary.MSAL) {
const tenants = await this.getTenantsMsal(token.token);
account = this.createAccount(tokenClaims, token.key, tenants);
} else { // fallback to ADAL as default
const tenants = await this.getTenantsAdal({ ...token });
account = this.createAccount(tokenClaims, token.key, tenants);
}
return account;
}
public async getAccountSecurityToken(account: AzureAccount, tenantId: string, azureResource: azdata.AzureResource): Promise<Token | undefined> {
public async getAccountSecurityTokenAdal(account: AzureAccount, tenantId: string, azureResource: azdata.AzureResource): Promise<Token | undefined> {
if (account.isStale === true) {
Logger.error('Account was stale. No tokens being fetched.');
return undefined;
@@ -178,8 +206,7 @@ export abstract class AzureAuth implements vscode.Disposable {
const resource = this.resources.find(s => s.azureResourceId === azureResource);
if (!resource) {
Logger.error('Invalid resource, not fetching', azureResource);
Logger.error(`Unable to find Azure resource ${azureResource} for account ${account.displayInfo.userId} and tenant ${tenantId}`);
return undefined;
}
@@ -196,7 +223,7 @@ export abstract class AzureAuth implements vscode.Disposable {
throw new AzureAuthError(localize('azure.tenantNotFound', "Specified tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
}
const cachedTokens = await this.getSavedToken(tenant, resource, account.key);
const cachedTokens = await this.getSavedTokenAdal(tenant, resource, account.key);
// Let's check to see if we can just use the cached tokens to return to the user
if (cachedTokens?.accessToken) {
@@ -213,7 +240,7 @@ export abstract class AzureAuth implements vscode.Disposable {
const maxTolerance = 2 * 60; // two minutes
if (remainingTime < maxTolerance) {
const result = await this.refreshToken(tenant, resource, cachedTokens.refreshToken);
const result = await this.refreshTokenAdal(tenant, resource, cachedTokens.refreshToken);
if (result) {
accessToken = result.accessToken;
expiresOn = Number(result.expiresOn);
@@ -224,7 +251,7 @@ export abstract class AzureAuth implements vscode.Disposable {
return {
...accessToken,
expiresOn: expiresOn,
tokenType: 'Bearer'
tokenType: Constants.Bearer
};
}
}
@@ -234,7 +261,7 @@ export abstract class AzureAuth implements vscode.Disposable {
if (!this.metadata.settings.microsoftResource) {
throw new Error(localize('noMicrosoftResource', "Provider '{0}' does not have a Microsoft resource endpoint defined.", this.metadata.displayName));
}
const baseTokens = await this.getSavedToken(this.commonTenant, this.metadata.settings.microsoftResource, account.key);
const baseTokens = await this.getSavedTokenAdal(this.commonTenant, this.metadata.settings.microsoftResource, account.key);
if (!baseTokens) {
Logger.error('User had no base tokens for the basic resource registered. This should not happen and indicates something went wrong with the authentication cycle');
const msg = localize('azure.noBaseToken', 'Something failed with the authentication, or your tokens have been deleted from the system. Please try adding your account to Azure Data Studio again.');
@@ -242,12 +269,12 @@ export abstract class AzureAuth implements vscode.Disposable {
throw new AzureAuthError(msg, 'No base token found', undefined);
}
// Let's try to convert the access token type, worst case we'll have to prompt the user to do an interactive authentication.
const result = await this.refreshToken(tenant, resource, baseTokens.refreshToken);
const result = await this.refreshTokenAdal(tenant, resource, baseTokens.refreshToken);
if (result?.accessToken) {
return {
...result.accessToken,
expiresOn: Number(result.expiresOn),
tokenType: 'Bearer'
tokenType: Constants.Bearer
};
}
return undefined;
@@ -255,7 +282,9 @@ export abstract class AzureAuth implements vscode.Disposable {
protected abstract login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }>;
protected abstract loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }>;
protected abstract loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }>;
/**
* Refreshes a token, if a refreshToken is passed in then we use that. If it is not passed in then we will prompt the user for consent.
@@ -265,7 +294,7 @@ export abstract class AzureAuth implements vscode.Disposable {
* @returns The oauth token response or undefined. Undefined is returned when the user wants to ignore a tenant or chooses not to start the
* re-authentication process for their tenant.
*/
public async refreshToken(tenant: Tenant, resource: Resource, refreshToken: RefreshToken | undefined): Promise<OAuthTokenResponse | undefined> {
public async refreshTokenAdal(tenant: Tenant, resource: Resource, refreshToken: RefreshToken | undefined): Promise<OAuthTokenResponse | undefined> {
Logger.pii('Refreshing token', [{ name: 'token', objOrArray: refreshToken }], []);
if (refreshToken) {
const postData: RefreshTokenPostData = {
@@ -275,36 +304,96 @@ export abstract class AzureAuth implements vscode.Disposable {
tenant: tenant.id,
resource: resource.endpoint
};
return this.getToken(tenant, resource, postData);
return this.getTokenAdal(tenant, resource, postData);
}
return this.handleInteractionRequired(tenant, resource);
return this.handleInteractionRequiredAdal(tenant, resource);
}
public async getToken(tenant: Tenant, resource: Resource, postData: AuthorizationCodePostData | TokenPostData | RefreshTokenPostData): Promise<OAuthTokenResponse | undefined> {
Logger.verbose('Fetching token');
/**
* Gets the access token for the correct account and scope from the token cache, if the correct token doesn't exist in the token cache
* (i.e. expired token, wrong scope, etc.), sends a request for a new token using the refresh token
* @param accountId
* @param azureResource
* @returns The authentication result, including the access token
*/
public async getTokenMsal(accountId: string, azureResource: azdata.AzureResource, tenantId: string): Promise<AuthenticationResult | null> {
const cache = this.clientApplication.getTokenCache();
if (!cache) {
Logger.error('Error: Could not fetch token cache.');
return null;
}
const resource = this.resources.find(s => s.azureResourceId === azureResource);
if (!resource) {
Logger.error(`Error: Could not fetch the azure resource ${azureResource} `);
return null;
}
let account: AccountInfo | null;
// if the accountId is a home ID, it will include a "." character
if (accountId.includes(".")) {
account = await cache.getAccountByHomeId(accountId);
} else {
account = await cache.getAccountByLocalId(accountId);
}
if (!account) {
Logger.error('Error: Could not fetch account when acquiring token');
return null;
}
let newScope;
if (resource.azureResourceId === azdata.AzureResource.ResourceManagement) {
newScope = [`${resource?.endpoint}user_impersonation`];
} else {
newScope = [`${resource?.endpoint}.default`];
}
// construct request
// forceRefresh needs to be set true here in order to fetch the correct token, due to this issue
// https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/3687
const tokenRequest = {
account: account,
authority: `https://login.microsoftonline.com/${tenantId}`,
scopes: newScope,
forceRefresh: true
};
try {
return await this.clientApplication.acquireTokenSilent(tokenRequest);
} catch (e) {
Logger.error('Failed to acquireTokenSilent', e);
if (e instanceof InteractionRequiredAuthError) {
// build refresh token request
const tenant: Tenant = {
id: tenantId,
displayName: ''
};
return this.handleInteractionRequiredMsal(tenant, resource);
} else if (e.name === 'ClientAuthError') {
Logger.error(e.message);
}
Logger.error('Failed to silently acquire token, not InteractionRequiredAuthError');
return null;
}
}
public async getTokenAdal(tenant: Tenant, resource: Resource, postData: AuthorizationCodePostData | TokenPostData | RefreshTokenPostData): Promise<OAuthTokenResponse | undefined> {
Logger.verbose('Fetching token for tenant {0}', tenant.id);
const tokenUrl = `${this.loginEndpointUrl}${tenant.id}/oauth2/token`;
const response = await this.makePostRequest(tokenUrl, postData);
Logger.pii('Token: ', [{ name: 'access token', objOrArray: response.data }, { name: 'refresh token', objOrArray: response.data }],
[{ name: 'access token', value: response.data.access_token }, { name: 'refresh token', value: response.data.refresh_token }]);
if (response.data.error === 'interaction_required') {
return this.handleInteractionRequired(tenant, resource);
}
Logger.pii('Token: ', [{ name: 'access token', objOrArray: response.data }, { name: 'refresh token', objOrArray: response.data }], []);
if (response.data.error === 'interaction_required') {
return this.handleInteractionRequiredAdal(tenant, resource);
}
if (response.data.error) {
Logger.error('Response error!', response.data);
Logger.error(`Response returned error : ${response.data}`);
throw new AzureAuthError(localize('azure.responseError', "Token retrieval failed with an error. [Open developer tools]({0}) for more details.", 'command:workbench.action.toggleDevTools'), 'Token retrieval failed', undefined);
}
const accessTokenString = response.data.access_token;
const refreshTokenString = response.data.refresh_token;
const expiresOnString = response.data.expires_on;
return this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
return this.getTokenHelperAdal(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
}
public async getTokenHelper(tenant: Tenant, resource: Resource, accessTokenString: string, refreshTokenString: string, expiresOnString: string): Promise<OAuthTokenResponse> {
public async getTokenHelperAdal(tenant: Tenant, resource: Resource, accessTokenString: string, refreshTokenString: string, expiresOnString: string): Promise<OAuthTokenResponse> {
if (!accessTokenString) {
const msg = localize('azure.accessTokenEmpty', 'No access token returned from Microsoft OAuth');
throw new AzureAuthError(msg, 'Access token was empty', undefined);
@@ -349,7 +438,8 @@ export abstract class AzureAuth implements vscode.Disposable {
const accountKey: azdata.AccountKey = {
providerId: this.metadata.id,
accountId: userKey
accountId: userKey,
authLibrary: this._authLibrary
};
await this.saveToken(tenant, resource, accountKey, result);
@@ -358,19 +448,47 @@ export abstract class AzureAuth implements vscode.Disposable {
}
//#region tenant calls
public async getTenants(token: AccessToken): Promise<Tenant[]> {
interface TenantResponse { // https://docs.microsoft.com/en-us/rest/api/resources/tenants/list
id: string
tenantId: string
displayName?: string
tenantCategory?: string
}
public async getTenantsMsal(token: string): Promise<Tenant[]> {
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
try {
Logger.verbose('Fetching tenants', tenantUri);
Logger.verbose('Fetching tenants with uri {0}', tenantUri);
let tenantList: string[] = [];
const tenantResponse = await this.makeGetRequest(tenantUri, token);
const tenants: Tenant[] = tenantResponse.data.value.map((tenantInfo: TenantResponse) => {
if (tenantInfo.displayName) {
tenantList.push(tenantInfo.displayName);
} else {
tenantList.push(tenantInfo.tenantId);
Logger.info('Tenant display name found empty: {0}', tenantInfo.tenantId);
}
return {
id: tenantInfo.tenantId,
displayName: tenantInfo.displayName ? tenantInfo.displayName : tenantInfo.tenantId,
userId: token,
tenantCategory: tenantInfo.tenantCategory
} as Tenant;
});
Logger.verbose(`Tenants: ${tenantList}`);
const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === Constants.HomeCategory);
// remove home tenant from list of tenants
if (homeTenantIndex >= 0) {
const homeTenant = tenants.splice(homeTenantIndex, 1);
tenants.unshift(homeTenant[0]);
}
return tenants;
} catch (ex) {
Logger.error(`Error fetching tenants :${ex}`);
throw new Error('Error retrieving tenant information');
}
}
//#region tenant calls
public async getTenantsAdal(token: AccessToken): Promise<Tenant[]> {
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
try {
Logger.verbose('Fetching tenants with URI: {0}', tenantUri);
let tenantList: string[] = [];
const tenantResponse = await this.makeGetRequest(tenantUri, token.token);
if (tenantResponse.status !== 200) {
Logger.error(`Error with tenant response, status: ${tenantResponse.status} | status text: ${tenantResponse.statusText}`);
@@ -378,16 +496,22 @@ export abstract class AzureAuth implements vscode.Disposable {
throw new Error('Error with tenant response');
}
const tenants: Tenant[] = tenantResponse.data.value.map((tenantInfo: TenantResponse) => {
Logger.verbose(`Tenant: ${tenantInfo.displayName}`);
if (tenantInfo.displayName) {
tenantList.push(tenantInfo.displayName);
} else {
tenantList.push(tenantInfo.tenantId);
Logger.info('Tenant display name found empty: {0}', tenantInfo.tenantId);
}
return {
id: tenantInfo.tenantId,
displayName: tenantInfo.displayName ? tenantInfo.displayName : localize('azureWorkAccountDisplayName', "Work or school account"),
displayName: tenantInfo.displayName ? tenantInfo.displayName : tenantInfo.tenantId,
userId: token.key,
tenantCategory: tenantInfo.tenantCategory
} as Tenant;
});
const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === 'Home');
Logger.verbose(`Tenants: ${tenantList}`);
const homeTenantIndex = tenants.findIndex(tenant => tenant.tenantCategory === Constants.HomeCategory);
// remove home tenant from list of tenants
if (homeTenantIndex >= 0) {
const homeTenant = tenants.splice(homeTenantIndex, 1);
tenants.unshift(homeTenant[0]);
@@ -421,7 +545,7 @@ export abstract class AzureAuth implements vscode.Disposable {
}
}
public async getSavedToken(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | undefined, expiresOn: string } | undefined> {
public async getSavedTokenAdal(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | undefined, expiresOn: string } | undefined> {
const getMsg = localize('azure.cacheErrorGet', "Error when getting your account from the cache");
const parseMsg = localize('azure.cacheErrorParse', "Error when parsing your account from the cache");
@@ -464,12 +588,22 @@ export abstract class AzureAuth implements vscode.Disposable {
}
//#endregion
//#region interaction handling
public async handleInteractionRequired(tenant: Tenant, resource: Resource): Promise<OAuthTokenResponse | undefined> {
//#region interaction handling
public async handleInteractionRequiredMsal(tenant: Tenant, resource: Resource): Promise<AuthenticationResult | null> {
const shouldOpen = await this.askUserForInteraction(tenant, resource);
if (shouldOpen) {
const result = await this.login(tenant, resource);
const result = await this.loginMsal(tenant, resource);
result?.authComplete?.resolve();
return result?.response;
}
return null;
}
public async handleInteractionRequiredAdal(tenant: Tenant, resource: Resource): Promise<OAuthTokenResponse | undefined> {
const shouldOpen = await this.askUserForInteraction(tenant, resource);
if (shouldOpen) {
const result = await this.loginAdal(tenant, resource);
result?.authComplete?.resolve();
return result?.response;
}
@@ -487,13 +621,14 @@ export abstract class AzureAuth implements vscode.Disposable {
}
const getTenantConfigurationSet = (): Set<string> => {
const configuration = vscode.workspace.getConfiguration('azure.tenant.config');
const configuration = vscode.workspace.getConfiguration(Constants.AzureTenantConfigSection);
let values: string[] = configuration.get('filter') ?? [];
return new Set<string>(values);
};
// The user wants to ignore this tenant.
if (getTenantConfigurationSet().has(tenant.id)) {
Logger.info(`Tenant ${tenant.id} found in the ignore list, authentication will not be attempted.`);
return false;
}
@@ -544,22 +679,21 @@ export abstract class AzureAuth implements vscode.Disposable {
public createAccount(tokenClaims: TokenClaims, key: string, tenants: Tenant[]): AzureAccount {
Logger.verbose(`Token Claims: ${tokenClaims.name}`);
tenants.forEach((tenant) => {
Logger.verbose(
`Tenant ID: ${tenant.id}
Tenant Name: ${tenant.displayName}`);
Logger.verbose(`Tenant ID: ${tenant.id}, Tenant Name: ${tenant.displayName}`);
});
// Determine if this is a microsoft account
let accountIssuer = 'unknown';
if (tokenClaims.iss === 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/') {
accountIssuer = 'corp';
if (tokenClaims.iss === 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/' ||
tokenClaims.iss === 'https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0') {
accountIssuer = Constants.AccountIssuer.Corp;
}
if (tokenClaims?.idp === 'live.com') {
accountIssuer = 'msft';
accountIssuer = Constants.AccountIssuer.Msft;
}
const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name;
const email = tokenClaims.email ?? tokenClaims.unique_name;
const name = tokenClaims.name ?? tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.preferred_username;
const email = tokenClaims.email ?? tokenClaims.unique_name ?? tokenClaims.preferred_username;
// Read more about tid > https://learn.microsoft.com/azure/active-directory/develop/id-tokens
const owningTenant = tenants.find(t => t.id === tokenClaims.tid)
@@ -572,25 +706,26 @@ export abstract class AzureAuth implements vscode.Disposable {
let contextualDisplayName: string;
switch (accountIssuer) {
case 'corp':
case Constants.AccountIssuer.Corp:
contextualDisplayName = localize('azure.microsoftCorpAccount', "Microsoft Corp");
break;
case 'msft':
case Constants.AccountIssuer.Msft:
contextualDisplayName = localize('azure.microsoftAccountDisplayName', 'Microsoft Account');
break;
default:
contextualDisplayName = displayName;
}
let accountType = accountIssuer === 'msft'
? this.MicrosoftAccountType
: this.WorkSchoolAccountType;
let accountType = accountIssuer === Constants.AccountIssuer.Msft
? Constants.AccountType.Microsoft
: Constants.AccountType.WorkSchool;
const account = {
key: {
providerId: this.metadata.id,
accountId: key,
accountVersion: AzureAuth.ACCOUNT_VERSION,
accountVersion: Constants.AccountVersion,
authLibrary: this._authLibrary
},
name: displayName,
displayInfo: {
@@ -603,7 +738,7 @@ export abstract class AzureAuth implements vscode.Disposable {
},
properties: {
providerSettings: this.metadata,
isMsAccount: accountIssuer === 'msft',
isMsAccount: accountIssuer === Constants.AccountIssuer.Msft,
owningTenant: owningTenant,
tenants,
azureAuthType: this.authType
@@ -660,8 +795,10 @@ export abstract class AzureAuth implements vscode.Disposable {
protected toBase64UrlEncoding(base64string: string): string {
return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding
}
public async deleteAllCache(): Promise<void> {
public async deleteAllCacheMsal(): Promise<void> {
this.clientApplication.clearCache();
}
public async deleteAllCacheAdal(): Promise<void> {
const results = await this.tokenCache.findCredentials('');
for (let { account } of results) {
@@ -671,7 +808,13 @@ export abstract class AzureAuth implements vscode.Disposable {
public async clearCredentials(account: azdata.AccountKey): Promise<void> {
try {
return this.deleteAccountCache(account);
// remove account based on authLibrary field, accounts added before this field was present will default to
// ADAL method of account removal
if (account.authLibrary === Constants.AuthLibrary.MSAL) {
return this.deleteAccountCacheMsal(account);
} else { // fallback to ADAL by default
return this.deleteAccountCacheAdal(account);
}
} catch (ex) {
const msg = localize('azure.cacheErrrorRemove', "Error when removing your account from the cache.");
void vscode.window.showErrorMessage(msg);
@@ -679,9 +822,27 @@ export abstract class AzureAuth implements vscode.Disposable {
}
}
public async deleteAccountCache(account: azdata.AccountKey): Promise<void> {
const results = await this.tokenCache.findCredentials(account.accountId);
public async deleteAccountCacheMsal(account: azdata.AccountKey): Promise<void> {
const tokenCache = this.clientApplication.getTokenCache();
let msalAccount: AccountInfo | null;
// if the accountId is a home ID, it will include a "." character
if (account.accountId.includes(".")) {
msalAccount = await tokenCache.getAccountByHomeId(account.accountId);
} else {
msalAccount = await tokenCache.getAccountByLocalId(account.accountId);
}
if (!msalAccount) {
Logger.error(`MSAL: Unable to find account ${account.accountId} for removal`);
throw Error(`Unable to find account ${account.accountId}`);
}
await tokenCache.removeAccount(msalAccount);
}
public async deleteAccountCacheAdal(account: azdata.AccountKey): Promise<void> {
const results = await this.tokenCache.findCredentials(account.accountId);
if (!results) {
Logger.error('ADAL: Unable to find account for removal');
}
for (let { account } of results) {
await this.tokenCache.clearCredential(account);
}
@@ -722,6 +883,13 @@ export interface RefreshToken extends AccountKey {
key: string
}
export interface TenantResponse { // https://docs.microsoft.com/en-us/rest/api/resources/tenants/list
id: string
tenantId: string
displayName?: string
tenantCategory?: string
}
export interface MultiTenantTokenResponse {
[tenantId: string]: Token | undefined;
}

View File

@@ -12,11 +12,13 @@ import { SimpleTokenCache } from '../simpleTokenCache';
import { SimpleWebServer } from '../utils/simpleWebServer';
import { AzureAuthError } from './azureAuthError';
import { Logger } from '../../utils/Logger';
import * as Constants from '../../constants';
import * as nls from 'vscode-nls';
import * as path from 'path';
import * as http from 'http';
import * as qs from 'qs';
import { promises as fs } from 'fs';
import { PublicClientApplication, CryptoProvider, AuthorizationUrlRequest, AuthorizationCodeRequest, AuthenticationResult } from '@azure/msal-node';
const localize = nls.loadMessageBundle();
@@ -28,33 +30,43 @@ interface AuthCodeResponse {
interface CryptoValues {
nonce: string;
challengeMethod: string;
codeVerifier: string;
codeChallenge: string;
}
export class AzureAuthCodeGrant extends AzureAuth {
private static readonly USER_FRIENDLY_NAME: string = localize('azure.azureAuthCodeGrantName', 'Azure Auth Code Grant');
private cryptoProvider: CryptoProvider;
private pkceCodes: CryptoValues;
constructor(
metadata: AzureAccountProviderMetadata,
tokenCache: SimpleTokenCache,
context: vscode.ExtensionContext,
uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
clientApplication: PublicClientApplication,
authLibrary: string
) {
super(metadata, tokenCache, context, uriEventEmitter, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME);
super(metadata, tokenCache, context, clientApplication, uriEventEmitter, AzureAuthType.AuthCodeGrant, AzureAuthCodeGrant.USER_FRIENDLY_NAME, authLibrary);
this.cryptoProvider = new CryptoProvider();
this.pkceCodes = {
nonce: '',
challengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, // Use SHA256 as the challenge method
codeVerifier: '', // Generate a code verifier for the Auth Code Request first
codeChallenge: '', // Generate a code challenge from the previously generated code verifier
};
}
protected async login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }> {
protected async loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse | undefined, authComplete: Deferred<void, Error> }> {
let authCompleteDeferred: Deferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
let authResponse: AuthCodeResponse;
if (vscode.env.uiKind === vscode.UIKind.Web) {
authResponse = await this.loginWeb(tenant, resource);
authResponse = await this.loginWebAdal(tenant, resource);
} else {
authResponse = await this.loginDesktop(tenant, resource, authCompletePromise);
authResponse = await this.loginDesktopAdal(tenant, resource, authCompletePromise);
}
return {
@@ -63,6 +75,30 @@ export class AzureAuthCodeGrant extends AzureAuth {
};
}
protected async loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }> {
let authCompleteDeferred: Deferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
let authCodeRequest: AuthorizationCodeRequest;
if (vscode.env.uiKind === vscode.UIKind.Web) {
authCodeRequest = await this.loginWebMsal(tenant, resource);
} else {
authCodeRequest = await this.loginDesktopMsal(tenant, resource, authCompletePromise);
}
let result = await this.clientApplication.acquireTokenByCode(authCodeRequest);
if (!result) {
Logger.error('Failed to acquireTokenByCode');
Logger.error(`Auth Code Request: ${JSON.stringify(authCodeRequest)}`)
throw Error('Failed to fetch token using auth code');
} else {
return {
response: result,
authComplete: authCompleteDeferred!
};
}
}
/**
* Requests an OAuthTokenResponse from Microsoft OAuth
*
@@ -79,12 +115,47 @@ export class AzureAuthCodeGrant extends AzureAuth {
resource: resource.endpoint
};
return this.getToken(tenant, resource, postData);
return this.getTokenAdal(tenant, resource, postData);
}
private async loginWeb(tenant: Tenant, resource: Resource): Promise<AuthCodeResponse> {
private async loginWebMsal(tenant: Tenant, resource: Resource): Promise<AuthorizationCodeRequest> {
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://microsoft.azurecore`));
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValues();
await this.createCryptoValuesMsal();
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
const state = `${port},${encodeURIComponent(this.pkceCodes.nonce)},${encodeURIComponent(callbackUri.query)}`;
try {
let authUrlRequest: AuthorizationUrlRequest;
authUrlRequest = {
scopes: this.scopes,
redirectUri: this.redirectUri,
codeChallenge: this.pkceCodes.codeChallenge,
codeChallengeMethod: this.pkceCodes.challengeMethod,
prompt: Constants.SELECT_ACCOUNT,
state: state
};
let authCodeRequest: AuthorizationCodeRequest;
authCodeRequest = {
scopes: this.scopes,
redirectUri: this.redirectUri,
codeVerifier: this.pkceCodes.codeVerifier,
code: ''
};
let authCodeUrl = await this.clientApplication.getAuthCodeUrl(authUrlRequest);
await vscode.env.openExternal(vscode.Uri.parse(authCodeUrl));
const authCode = await this.handleWebResponse(state);
authCodeRequest.code = authCode;
return authCodeRequest;
} catch (e) {
Logger.error('MSAL: Error requesting auth code', e);
throw new AzureAuthError('error', 'Error requesting auth code', e);
}
}
private async loginWebAdal(tenant: Tenant, resource: Resource): Promise<AuthCodeResponse> {
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://microsoft.azurecore`));
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValuesAdal();
const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80);
const state = `${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`;
@@ -94,8 +165,8 @@ export class AzureAuthCodeGrant extends AzureAuth {
client_id: this.clientId,
redirect_uri: this.redirectUri,
state,
prompt: 'select_account',
code_challenge_method: 'S256',
prompt: Constants.SELECT_ACCOUNT,
code_challenge_method: Constants.S256_CODE_CHALLENGE_METHOD,
code_challenge: codeChallenge,
resource: resource.id
};
@@ -141,7 +212,7 @@ export class AzureAuthCodeGrant extends AzureAuth {
}, {});
}
private async loginDesktop(tenant: Tenant, resource: Resource, authCompletePromise: Promise<void>): Promise<AuthCodeResponse> {
private async loginDesktopMsal(tenant: Tenant, resource: Resource, authCompletePromise: Promise<void>): Promise<AuthorizationCodeRequest> {
const server = new SimpleWebServer();
let serverPort: string;
@@ -151,7 +222,56 @@ export class AzureAuthCodeGrant extends AzureAuth {
const msg = localize('azure.serverCouldNotStart', 'Server could not start. This could be a permissions error or an incompatibility on your system. You can try enabling device code authentication from settings.');
throw new AzureAuthError(msg, 'Server could not start', ex);
}
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValues();
await this.createCryptoValuesMsal();
const state = `${serverPort},${this.pkceCodes.nonce}`;
try {
let authUrlRequest: AuthorizationUrlRequest;
authUrlRequest = {
scopes: this.scopes,
redirectUri: `${this.redirectUri}:${serverPort}/redirect`,
codeChallenge: this.pkceCodes.codeChallenge,
codeChallengeMethod: this.pkceCodes.challengeMethod,
prompt: Constants.SELECT_ACCOUNT,
authority: `https://login.microsoftonline.com/${tenant.id}`,
state: state
};
let authCodeRequest: AuthorizationCodeRequest;
authCodeRequest = {
scopes: this.scopes,
redirectUri: `${this.redirectUri}:${serverPort}/redirect`,
codeVerifier: this.pkceCodes.codeVerifier,
authority: `https://login.microsoftonline.com/${tenant.id}`,
code: ''
};
let authCodeUrl = await this.clientApplication.getAuthCodeUrl(authUrlRequest);
await vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${serverPort}/signin?nonce=${encodeURIComponent(this.pkceCodes.nonce)}`));
const authCode = await this.addServerListeners(server, this.pkceCodes.nonce, authCodeUrl, authCompletePromise);
authCodeRequest.code = authCode;
return authCodeRequest;
}
catch (e) {
Logger.error('MSAL: Error requesting auth code', e);
throw new AzureAuthError('error', 'Error requesting auth code', e);
}
}
private async loginDesktopAdal(tenant: Tenant, resource: Resource, authCompletePromise: Promise<void>): Promise<AuthCodeResponse> {
const server = new SimpleWebServer();
let serverPort: string;
try {
serverPort = await server.startup();
} catch (ex) {
const msg = localize('azure.serverCouldNotStart', 'Server could not start. This could be a permissions error or an incompatibility on your system. You can try enabling device code authentication from settings.');
throw new AzureAuthError(msg, 'Server could not start', ex);
}
const { nonce, codeVerifier, codeChallenge } = this.createCryptoValuesAdal();
const state = `${serverPort},${encodeURIComponent(nonce)}`;
const loginQuery = {
response_type: 'code',
@@ -159,8 +279,8 @@ export class AzureAuthCodeGrant extends AzureAuth {
client_id: this.clientId,
redirect_uri: `${this.redirectUri}:${serverPort}/redirect`,
state,
prompt: 'select_account',
code_challenge_method: 'S256',
prompt: Constants.SELECT_ACCOUNT,
code_challenge_method: Constants.S256_CODE_CHALLENGE_METHOD,
code_challenge: codeChallenge,
resource: resource.endpoint
};
@@ -272,13 +392,21 @@ export class AzureAuthCodeGrant extends AzureAuth {
}
private createCryptoValues(): CryptoValues {
private createCryptoValuesAdal(): CryptoValues {
const nonce = crypto.randomBytes(16).toString('base64');
const codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
const codeChallenge = this.toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
const challengeMethod = '';
return {
nonce, codeVerifier, codeChallenge
nonce, challengeMethod, codeVerifier, codeChallenge
};
}
private async createCryptoValuesMsal(): Promise<void> {
this.pkceCodes.nonce = this.cryptoProvider.createNewGuid();
const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
this.pkceCodes.codeVerifier = verifier;
this.pkceCodes.codeChallenge = challenge;
}
}

View File

@@ -13,7 +13,6 @@ import {
DeviceCodeCheckPostData,
} from './azureAuth';
import {
AzureAccountProviderMetadata,
AzureAuthType,
@@ -21,8 +20,10 @@ import {
Resource
} from 'azurecore';
import { Deferred } from '../interfaces';
import { AuthenticationResult, DeviceCodeRequest, PublicClientApplication } from '@azure/msal-node';
import { SimpleTokenCache } from '../simpleTokenCache';
import { Logger } from '../../utils/Logger';
const localize = nls.loadMessageBundle();
interface DeviceCodeLogin { // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
@@ -50,12 +51,34 @@ export class AzureDeviceCode extends AzureAuth {
tokenCache: SimpleTokenCache,
context: vscode.ExtensionContext,
uriEventEmitter: vscode.EventEmitter<vscode.Uri>,
clientApplication: PublicClientApplication,
authLibrary: string
) {
super(metadata, tokenCache, context, uriEventEmitter, AzureAuthType.DeviceCode, AzureDeviceCode.USER_FRIENDLY_NAME);
super(metadata, tokenCache, context, clientApplication, uriEventEmitter, AzureAuthType.DeviceCode, AzureDeviceCode.USER_FRIENDLY_NAME, authLibrary);
this.pageTitle = localize('addAccount', "Add {0} account", this.metadata.displayName);
}
protected async login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void, Error> }> {
protected async loginMsal(tenant: Tenant, resource: Resource): Promise<{ response: AuthenticationResult | null, authComplete: Deferred<void, Error> }> {
let authCompleteDeferred: Deferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
const deviceCodeRequest: DeviceCodeRequest = {
scopes: this.scopes,
authority: `https://login.microsoftonline.com/${tenant.id}`,
deviceCodeCallback: async (response) => {
await azdata.accounts.beginAutoOAuthDeviceCode(this.metadata.id, this.pageTitle, response.message, response.userCode, response.verificationUri);
}
};
const authResult = await this.clientApplication.acquireTokenByDeviceCode(deviceCodeRequest);
this.closeOnceComplete(authCompletePromise).catch(Logger.error);
return {
response: authResult,
authComplete: authCompleteDeferred!
};
}
protected async loginAdal(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void, Error> }> {
let authCompleteDeferred: Deferred<void, Error>;
let authCompletePromise = new Promise<void>((resolve, reject) => authCompleteDeferred = { resolve, reject });
@@ -79,7 +102,7 @@ export class AzureDeviceCode extends AzureAuth {
const currentTime = new Date().getTime() / 1000;
const expiresOn = `${currentTime + finalDeviceLogin.expires_in}`;
const result = await this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOn);
const result = await this.getTokenHelperAdal(tenant, resource, accessTokenString, refreshTokenString, expiresOn);
this.closeOnceComplete(authCompletePromise).catch(Logger.error);
return {
@@ -93,7 +116,6 @@ export class AzureDeviceCode extends AzureAuth {
azdata.accounts.endAutoOAuthDeviceCode();
}
private setupPolling(info: DeviceCodeLogin): Promise<DeviceCodeLoginResult> {
const timeoutMessage = localize('azure.timeoutDeviceCode', 'Timed out when waiting for device code login.');
const fiveMinutes = 5 * 60 * 1000;
@@ -130,18 +152,15 @@ export class AzureDeviceCode extends AzureAuth {
};
const postResult = await this.makePostRequest(uri, postData);
const result: DeviceCodeLoginResult = postResult.data;
return result;
} catch (ex) {
console.log(ex);
console.log('Unexpected error making Azure auth request', 'azureCore.checkForResult', JSON.stringify(ex?.response?.data, undefined, 2));
Logger.error('Unexpected error making Azure auth request', 'azureCore.checkForResult', JSON.stringify(ex?.response?.data, undefined, 2));
throw new Error(msg);
}
}
public override async autoOAuthCancelled(): Promise<void> {
return azdata.accounts.endAutoOAuthDeviceCode();
}

View File

@@ -13,31 +13,37 @@ import {
AzureAccount
} from 'azurecore';
import { Deferred } from './interfaces';
import { PublicClientApplication } from '@azure/msal-node';
import { SimpleTokenCache } from './simpleTokenCache';
import { Logger } from '../utils/Logger';
import { MultiTenantTokenResponse, Token, AzureAuth } from './auths/azureAuth';
import { AzureAuthCodeGrant } from './auths/azureAuthCodeGrant';
import { AzureDeviceCode } from './auths/azureDeviceCode';
import { filterAccounts } from '../azureResource/utils';
import * as Constants from '../constants';
const localize = nls.loadMessageBundle();
export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disposable {
private static readonly CONFIGURATION_SECTION = 'accounts.azure.auth';
private readonly authMappings = new Map<AzureAuthType, AzureAuth>();
private initComplete!: Deferred<void, Error>;
private initCompletePromise: Promise<void> = new Promise<void>((resolve, reject) => this.initComplete = { resolve, reject });
public clientApplication: PublicClientApplication;
constructor(
metadata: AzureAccountProviderMetadata,
tokenCache: SimpleTokenCache,
context: vscode.ExtensionContext,
clientApplication: PublicClientApplication,
uriEventHandler: vscode.EventEmitter<vscode.Uri>,
private readonly authLibrary: string,
private readonly forceDeviceCode: boolean = false
) {
this.clientApplication = clientApplication;
vscode.workspace.onDidChangeConfiguration((changeEvent) => {
const impact = changeEvent.affectsConfiguration(AzureAccountProvider.CONFIGURATION_SECTION);
if (impact === true) {
const impactProvider = changeEvent.affectsConfiguration(Constants.AccountsAzureAuthSection);
if (impactProvider === true) {
this.handleAuthMapping(metadata, tokenCache, context, uriEventHandler);
}
});
@@ -50,25 +56,28 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
}
clearTokenCache(): Thenable<void> {
return this.getAuthMethod().deleteAllCache();
return this.authLibrary === Constants.AuthLibrary.MSAL
? this.getAuthMethod().deleteAllCacheMsal()
// fallback to ADAL as default
: this.getAuthMethod().deleteAllCacheAdal();
}
private handleAuthMapping(metadata: AzureAccountProviderMetadata, tokenCache: SimpleTokenCache, context: vscode.ExtensionContext, uriEventHandler: vscode.EventEmitter<vscode.Uri>) {
this.authMappings.forEach(m => m.dispose());
this.authMappings.clear();
const configuration = vscode.workspace.getConfiguration(AzureAccountProvider.CONFIGURATION_SECTION);
const codeGrantMethod: boolean = configuration.get<boolean>('codeGrant', false);
const deviceCodeMethod: boolean = configuration.get<boolean>('deviceCode', false);
const configuration = vscode.workspace.getConfiguration(Constants.AccountsAzureAuthSection);
const codeGrantMethod: boolean = configuration.get<boolean>(Constants.AuthType.CodeGrant, false);
const deviceCodeMethod: boolean = configuration.get<boolean>(Constants.AuthType.DeviceCode, false);
if (codeGrantMethod === true && !this.forceDeviceCode) {
this.authMappings.set(AzureAuthType.AuthCodeGrant, new AzureAuthCodeGrant(metadata, tokenCache, context, uriEventHandler));
this.authMappings.set(AzureAuthType.AuthCodeGrant, new AzureAuthCodeGrant(metadata, tokenCache, context, uriEventHandler, this.clientApplication, this.authLibrary));
}
if (deviceCodeMethod === true || this.forceDeviceCode) {
this.authMappings.set(AzureAuthType.DeviceCode, new AzureDeviceCode(metadata, tokenCache, context, uriEventHandler));
this.authMappings.set(AzureAuthType.DeviceCode, new AzureDeviceCode(metadata, tokenCache, context, uriEventHandler, this.clientApplication, this.authLibrary));
}
if (codeGrantMethod === false && deviceCodeMethod === false && !this.forceDeviceCode) {
Logger.error('Error: No authentication methods selected');
console.error('No authentication methods selected');
}
}
@@ -97,13 +106,19 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
private async _initialize(storedAccounts: AzureAccount[]): Promise<AzureAccount[]> {
const accounts: AzureAccount[] = [];
console.log(`Initializing stored accounts ${JSON.stringify(accounts)}`);
for (let account of storedAccounts) {
const updatedAccounts = filterAccounts(storedAccounts, this.authLibrary);
for (let account of updatedAccounts) {
const azureAuth = this.getAuthMethod(account);
if (!azureAuth) {
account.isStale = true;
accounts.push(account);
} else {
accounts.push(await azureAuth.refreshAccess(account));
account.isStale = false;
if (this.authLibrary === Constants.AuthLibrary.MSAL) {
accounts.push(account);
} else { // fallback to ADAL as default
accounts.push(await azureAuth.refreshAccessAdal(account));
}
}
}
this.initComplete.resolve();
@@ -123,7 +138,23 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
await this.initCompletePromise;
const azureAuth = this.getAuthMethod(account);
Logger.pii(`Getting account security token for ${JSON.stringify(account.key)} (tenant ${tenantId}). Auth Method = ${azureAuth.userFriendlyName}`, [], []);
return azureAuth?.getAccountSecurityToken(account, tenantId, resource);
if (this.authLibrary === Constants.AuthLibrary.MSAL) {
let authResult = await azureAuth?.getTokenMsal(account.key.accountId, resource, tenantId);
if (!authResult || !authResult.account || !authResult.account.idTokenClaims) {
Logger.error(`MSAL: getToken call failed`);
throw Error('Failed to get token');
} else {
const token: Token = {
key: authResult.account.homeAccountId,
token: authResult.accessToken,
tokenType: authResult.tokenType,
expiresOn: authResult.account.idTokenClaims.exp
};
return token;
}
} else { // fallback to ADAL as default
return azureAuth?.getAccountSecurityTokenAdal(account, tenantId, resource);
}
}
private async _getSecurityToken(account: AzureAccount, resource: azdata.AzureResource): Promise<MultiTenantTokenResponse | undefined> {
@@ -178,7 +209,6 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
return pick.azureAuth.startLogin();
}
refresh(account: AzureAccount): Thenable<AzureAccount | azdata.PromptFailedResult> {
return this._refresh(account);
}

View File

@@ -7,12 +7,18 @@ import * as azdata from 'azdata';
import * as events from 'events';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import * as os from 'os';
import { SimpleTokenCache } from './simpleTokenCache';
import providerSettings from './providerSettings';
import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider';
import { AzureAccountProviderMetadata } from 'azurecore';
import { ProviderSettings } from './interfaces';
import * as loc from '../localizedConstants';
import { PublicClientApplication } from '@azure/msal-node';
import { DataProtectionScope, PersistenceCachePlugin, FilePersistenceWithDataProtection, KeychainPersistence, LibSecretPersistence } from '@azure/msal-node-extensions';
import * as path from 'path';
import { Logger } from '../utils/Logger';
import * as Constants from '../constants';
let localize = nls.loadMessageBundle();
@@ -23,11 +29,6 @@ class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.
}
export class AzureAccountProviderService implements vscode.Disposable {
// CONSTANTS ///////////////////////////////////////////////////////////////
private static CommandClearTokenCache = 'accounts.clearTokenCache';
private static ConfigurationSection = 'accounts.azure.cloud';
private static CredentialNamespace = 'azureAccountProviderCredentials';
// MEMBER VARIABLES ////////////////////////////////////////////////////////
private _disposables: vscode.Disposable[] = [];
private _accountDisposals: { [accountProviderId: string]: vscode.Disposable } = {};
@@ -37,8 +38,12 @@ export class AzureAccountProviderService implements vscode.Disposable {
private _currentConfig: vscode.WorkspaceConfiguration | undefined = undefined;
private _event: events.EventEmitter = new events.EventEmitter();
private readonly _uriEventHandler: UriEventHandler = new UriEventHandler();
public clientApplication!: PublicClientApplication;
public persistence: FilePersistenceWithDataProtection | KeychainPersistence | LibSecretPersistence | undefined;
constructor(private _context: vscode.ExtensionContext, private _userStoragePath: string) {
constructor(private _context: vscode.ExtensionContext,
private _userStoragePath: string,
private _authLibrary: string) {
this._disposables.push(vscode.window.registerUriHandler(this._uriEventHandler));
}
@@ -47,17 +52,16 @@ export class AzureAccountProviderService implements vscode.Disposable {
let self = this;
// Register commands
this._context.subscriptions.push(vscode.commands.registerCommand(
AzureAccountProviderService.CommandClearTokenCache,
() => { self._event.emit(AzureAccountProviderService.CommandClearTokenCache); }
this._context.subscriptions.push(vscode.commands.registerCommand(Constants.AccountsClearTokenCacheCommand,
() => { self._event.emit(Constants.AccountsClearTokenCacheCommand); }
));
this._event.on(AzureAccountProviderService.CommandClearTokenCache, () => { void self.onClearTokenCache(); });
this._event.on(Constants.AccountsClearTokenCacheCommand, () => { void self.onClearTokenCache(); });
// 1) Get a credential provider
// 2a) Store the credential provider for use later
// 2b) Register the configuration change handler
// 2c) Perform an initial config change handling
return azdata.credentials.getProvider(AzureAccountProviderService.CredentialNamespace)
return azdata.credentials.getProvider(Constants.AzureAccountProviderCredentials)
.then(credProvider => {
this._credentialProvider = credProvider;
@@ -103,7 +107,7 @@ export class AzureAccountProviderService implements vscode.Disposable {
// Add a new change processing onto the existing promise change
await this._configChangePromiseChain;
// Grab the stored config and the latest config
let newConfig = vscode.workspace.getConfiguration(AzureAccountProviderService.ConfigurationSection);
let newConfig = vscode.workspace.getConfiguration(Constants.AccountsAzureCloudSection);
let oldConfig = this._currentConfig;
this._currentConfig = newConfig;
@@ -138,22 +142,58 @@ export class AzureAccountProviderService implements vscode.Disposable {
}
private async registerAccountProvider(provider: ProviderSettings): Promise<void> {
const isSaw: boolean = vscode.env.appName.toLowerCase().indexOf(Constants.Saw) > 0;
const noSystemKeychain = vscode.workspace.getConfiguration(Constants.AzureSection).get<boolean>(Constants.NoSystemKeyChainSection);
const platform = os.platform();
const tokenCacheKey = `azureTokenCache-${provider.metadata.id}`;
const lockOptions = {
retryNumber: 100,
retryDelay: 50
}
try {
const noSystemKeychain = vscode.workspace.getConfiguration('azure').get<boolean>('noSystemKeychain');
let tokenCacheKey = `azureTokenCache-${provider.metadata.id}`;
if (!this._credentialProvider) {
throw new Error('Credential provider not registered');
}
let simpleTokenCache = new SimpleTokenCache(tokenCacheKey, this._userStoragePath, noSystemKeychain, this._credentialProvider);
await simpleTokenCache.init();
const cachePath = path.join(this._userStoragePath, Constants.ConfigFilePath);
const isSaw: boolean = vscode.env.appName.toLowerCase().indexOf('saw') > 0;
let accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata, simpleTokenCache, this._context, this._uriEventHandler, isSaw);
switch (platform) {
case Constants.Platform.Windows:
const dataProtectionScope = DataProtectionScope.CurrentUser;
const optionalEntropy = "";
this.persistence = await FilePersistenceWithDataProtection.create(cachePath, dataProtectionScope, optionalEntropy);
break;
case Constants.Platform.Mac:
case Constants.Platform.Linux:
this.persistence = await KeychainPersistence.create(cachePath, Constants.ServiceName, Constants.Account);
break;
}
if (!this.persistence) {
Logger.error('Unable to intialize persistence for access token cache. Tokens will not persist in system memory for future use.');
throw new Error('Unable to intialize persistence for access token cache. Tokens will not persist in system memory for future use.');
}
let persistenceCachePlugin: PersistenceCachePlugin = new PersistenceCachePlugin(this.persistence, lockOptions); // or any of the other ones.
const MSAL_CONFIG = {
auth: {
clientId: provider.metadata.settings.clientId,
redirect_uri: `${provider.metadata.settings.redirectUri}/redirect`
},
cache: {
cachePlugin: persistenceCachePlugin
}
}
this.clientApplication = new PublicClientApplication(MSAL_CONFIG);
let accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata,
simpleTokenCache, this._context, this.clientApplication, this._uriEventHandler, this._authLibrary, isSaw);
this._accountProviders[provider.metadata.id] = accountProvider;
this._accountDisposals[provider.metadata.id] = azdata.accounts.registerAccountProvider(provider.metadata, accountProvider);
} catch (e) {
console.error(`Failed to register account provider: ${e}`);
console.error(`Failed to register account provider, isSaw: ${isSaw}: ${e}`);
}
}

View File

@@ -49,7 +49,7 @@ const publicAzureSettings: ProviderSettings = {
},
armResource: {
id: SettingIds.arm,
endpoint: 'https://management.azure.com',
endpoint: 'https://management.azure.com/',
azureResourceId: AzureResource.ResourceManagement
},
sqlResource: {