mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-04-01 17:40:30 -04:00
Add MSAL Authentication Library support (#21024)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user