mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Introduce inbuilt MsalCachePlugin to replace msal-node-extensions (#21335)
This commit is contained in:
@@ -442,12 +442,11 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
authLibrary: this._authLibrary
|
||||
};
|
||||
|
||||
await this.saveToken(tenant, resource, accountKey, result);
|
||||
await this.saveTokenAdal(tenant, resource, accountKey, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public async getTenantsMsal(token: string): Promise<Tenant[]> {
|
||||
const tenantUri = url.resolve(this.metadata.settings.armResource.endpoint, 'tenants?api-version=2019-11-01');
|
||||
try {
|
||||
@@ -527,7 +526,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
//#endregion
|
||||
|
||||
//#region token management
|
||||
private async saveToken(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey, { accessToken, refreshToken, expiresOn }: OAuthTokenResponse) {
|
||||
private async saveTokenAdal(tenant: Tenant, resource: Resource, accountKey: azdata.AccountKey, { accessToken, refreshToken, expiresOn }: OAuthTokenResponse) {
|
||||
const msg = localize('azure.cacheErrorAdd', "Error when adding your account to the cache.");
|
||||
if (!tenant.id || !resource.id) {
|
||||
Logger.pii('Tenant ID or resource ID was undefined', [], [], tenant, resource);
|
||||
|
||||
@@ -7,18 +7,17 @@ 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 { MsalCachePluginProvider } from './utils/msalCachePlugin';
|
||||
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 { Configuration, PublicClientApplication } from '@azure/msal-node';
|
||||
import * as Constants from '../constants';
|
||||
import { Logger } from '../utils/Logger';
|
||||
import { ILoggerCallback, LogLevel as MsalLogLevel } from "@azure/msal-common";
|
||||
|
||||
let localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -34,12 +33,12 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
||||
private _accountDisposals: { [accountProviderId: string]: vscode.Disposable } = {};
|
||||
private _accountProviders: { [accountProviderId: string]: azdata.AccountProvider } = {};
|
||||
private _credentialProvider: azdata.CredentialProvider | undefined = undefined;
|
||||
private _cachePluginProvider: MsalCachePluginProvider | undefined = undefined;
|
||||
private _configChangePromiseChain: Thenable<void> = Promise.resolve();
|
||||
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,
|
||||
@@ -144,51 +143,38 @@ 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 tokenCacheKeyMsal = `azureTokenCacheMsal-${provider.metadata.id}`;
|
||||
try {
|
||||
if (!this._credentialProvider) {
|
||||
throw new Error('Credential provider not registered');
|
||||
}
|
||||
|
||||
// ADAL Token Cache
|
||||
let simpleTokenCache = new SimpleTokenCache(tokenCacheKey, this._userStoragePath, noSystemKeychain, this._credentialProvider);
|
||||
await simpleTokenCache.init();
|
||||
const cachePath = path.join(this._userStoragePath, Constants.ConfigFilePath);
|
||||
|
||||
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.');
|
||||
}
|
||||
// MSAL Cache Plugin
|
||||
this._cachePluginProvider = new MsalCachePluginProvider(tokenCacheKeyMsal, this._userStoragePath);
|
||||
|
||||
let persistenceCachePlugin: PersistenceCachePlugin = new PersistenceCachePlugin(
|
||||
this.persistence, {
|
||||
retryNumber: 500,
|
||||
retryDelay: 150
|
||||
});
|
||||
|
||||
const MSAL_CONFIG = {
|
||||
const msalConfiguration: Configuration = {
|
||||
auth: {
|
||||
clientId: provider.metadata.settings.clientId,
|
||||
redirect_uri: `${provider.metadata.settings.redirectUri}/redirect`
|
||||
authority: 'https://login.windows.net/common'
|
||||
},
|
||||
system: {
|
||||
loggerOptions: {
|
||||
loggerCallback: this.getLoggerCallback(),
|
||||
logLevel: MsalLogLevel.Trace,
|
||||
piiLoggingEnabled: true,
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
cachePlugin: persistenceCachePlugin
|
||||
cachePlugin: this._cachePluginProvider?.getCachePlugin()
|
||||
}
|
||||
}
|
||||
|
||||
this.clientApplication = new PublicClientApplication(MSAL_CONFIG);
|
||||
this.clientApplication = new PublicClientApplication(msalConfiguration);
|
||||
let accountProvider = new AzureAccountProvider(provider.metadata as AzureAccountProviderMetadata,
|
||||
simpleTokenCache, this._context, this.clientApplication, this._uriEventHandler, this._authLibrary, isSaw);
|
||||
this._accountProviders[provider.metadata.id] = accountProvider;
|
||||
@@ -198,6 +184,27 @@ export class AzureAccountProviderService implements vscode.Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private getLoggerCallback(): ILoggerCallback {
|
||||
return (level: number, message: string, containsPii: boolean) => {
|
||||
if (!containsPii) {
|
||||
switch (level) {
|
||||
case MsalLogLevel.Error:
|
||||
Logger.error(message);
|
||||
break;
|
||||
case MsalLogLevel.Info:
|
||||
Logger.info(message);
|
||||
break;
|
||||
case MsalLogLevel.Verbose:
|
||||
default:
|
||||
Logger.verbose(message);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
Logger.verbose(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async unregisterAccountProvider(provider: ProviderSettings): Promise<void> {
|
||||
try {
|
||||
this._accountDisposals[provider.metadata.id].dispose();
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as keytarType from 'keytar';
|
||||
import { join, parse } from 'path';
|
||||
import { FileDatabase } from './utils/fileDatabase';
|
||||
import * as azdata from 'azdata';
|
||||
import * as crypto from 'crypto';
|
||||
import { FileEncryptionHelper } from './utils/fileEncryptionHelper';
|
||||
|
||||
function getSystemKeytar(): Keytar | undefined {
|
||||
try {
|
||||
@@ -25,42 +25,8 @@ const separator = '§';
|
||||
|
||||
async function getFileKeytar(filePath: string, credentialService: azdata.CredentialProvider): Promise<Keytar | undefined> {
|
||||
const fileName = parse(filePath).base;
|
||||
const iv = await credentialService.readCredential(`${fileName}-iv`);
|
||||
const key = await credentialService.readCredential(`${fileName}-key`);
|
||||
let ivBuffer: Buffer;
|
||||
let keyBuffer: Buffer;
|
||||
if (!iv?.password || !key?.password) {
|
||||
ivBuffer = crypto.randomBytes(16);
|
||||
keyBuffer = crypto.randomBytes(32);
|
||||
try {
|
||||
await credentialService.saveCredential(`${fileName}-iv`, ivBuffer.toString('hex'));
|
||||
await credentialService.saveCredential(`${fileName}-key`, keyBuffer.toString('hex'));
|
||||
} catch (ex) {
|
||||
console.log(ex);
|
||||
}
|
||||
} else {
|
||||
ivBuffer = Buffer.from(iv.password, 'hex');
|
||||
keyBuffer = Buffer.from(key.password, 'hex');
|
||||
}
|
||||
|
||||
const fileSaver = async (content: string): Promise<string> => {
|
||||
const cipherIv = crypto.createCipheriv('aes-256-gcm', keyBuffer, ivBuffer);
|
||||
return `${cipherIv.update(content, 'utf8', 'hex')}${cipherIv.final('hex')}%${cipherIv.getAuthTag().toString('hex')}`;
|
||||
};
|
||||
|
||||
const fileOpener = async (content: string): Promise<string> => {
|
||||
const decipherIv = crypto.createDecipheriv('aes-256-gcm', keyBuffer, ivBuffer);
|
||||
|
||||
const split = content.split('%');
|
||||
if (split.length !== 2) {
|
||||
throw new Error('File didn\'t contain the auth tag.');
|
||||
}
|
||||
decipherIv.setAuthTag(Buffer.from(split[1], 'hex'));
|
||||
|
||||
return `${decipherIv.update(split[0], 'hex', 'utf8')}${decipherIv.final('utf8')}`;
|
||||
};
|
||||
|
||||
const db = new FileDatabase(filePath, fileOpener, fileSaver);
|
||||
const fileEncryptionHelper: FileEncryptionHelper = new FileEncryptionHelper(credentialService, fileName);
|
||||
const db = new FileDatabase(filePath, fileEncryptionHelper.fileOpener, fileEncryptionHelper.fileSaver);
|
||||
await db.initialize();
|
||||
|
||||
const fileKeytar: Keytar = {
|
||||
@@ -94,6 +60,7 @@ async function getFileKeytar(filePath: string, credentialService: azdata.Credent
|
||||
return fileKeytar;
|
||||
}
|
||||
|
||||
|
||||
export type Keytar = {
|
||||
getPassword: typeof keytarType['getPassword'];
|
||||
setPassword: typeof keytarType['setPassword'];
|
||||
@@ -110,9 +77,7 @@ export class SimpleTokenCache {
|
||||
private readonly userStoragePath: string,
|
||||
private readonly forceFileStorage: boolean = false,
|
||||
private readonly credentialService: azdata.CredentialProvider,
|
||||
) {
|
||||
|
||||
}
|
||||
) { }
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.serviceName = this.serviceName.replace(/-/g, '_');
|
||||
|
||||
@@ -88,7 +88,6 @@ export class FileDatabase {
|
||||
this.isDirty = true;
|
||||
}
|
||||
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
this.isInitialized = true;
|
||||
this.saveInterval = setInterval(() => this.save(), 20 * 1000);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as azdata from 'azdata';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export class FileEncryptionHelper {
|
||||
constructor(
|
||||
private _credentialService: azdata.CredentialProvider,
|
||||
private _fileName: string,
|
||||
) { }
|
||||
|
||||
private _ivBuffer: Buffer | undefined;
|
||||
private _keyBuffer: Buffer | undefined;
|
||||
|
||||
async init(): Promise<void> {
|
||||
const iv = await this._credentialService.readCredential(`${this._fileName}-iv`);
|
||||
const key = await this._credentialService.readCredential(`${this._fileName}-key`);
|
||||
if (!iv?.password || !key?.password) {
|
||||
this._ivBuffer = crypto.randomBytes(16);
|
||||
this._keyBuffer = crypto.randomBytes(32);
|
||||
try {
|
||||
await this._credentialService.saveCredential(`${this._fileName}-iv`, this._ivBuffer.toString('hex'));
|
||||
await this._credentialService.saveCredential(`${this._fileName}-key`, this._keyBuffer.toString('hex'));
|
||||
} catch (ex) {
|
||||
console.log(ex);
|
||||
}
|
||||
} else {
|
||||
this._ivBuffer = Buffer.from(iv.password, 'hex');
|
||||
this._keyBuffer = Buffer.from(key.password, 'hex');
|
||||
}
|
||||
}
|
||||
|
||||
fileSaver = async (content: string): Promise<string> => {
|
||||
if (!this._keyBuffer || !this._ivBuffer) {
|
||||
await this.init();
|
||||
}
|
||||
const cipherIv = crypto.createCipheriv('aes-256-gcm', this._keyBuffer!, this._ivBuffer!);
|
||||
return `${cipherIv.update(content, 'utf8', 'hex')}${cipherIv.final('hex')}%${cipherIv.getAuthTag().toString('hex')}`;
|
||||
};
|
||||
|
||||
fileOpener = async (content: string): Promise<string> => {
|
||||
if (!this._keyBuffer || !this._ivBuffer) {
|
||||
await this.init();
|
||||
}
|
||||
const decipherIv = crypto.createDecipheriv('aes-256-gcm', this._keyBuffer!, this._ivBuffer!);
|
||||
const split = content.split('%');
|
||||
if (split.length !== 2) {
|
||||
throw new Error('File didn\'t contain the auth tag.');
|
||||
}
|
||||
decipherIv.setAuthTag(Buffer.from(split[1], 'hex'));
|
||||
return `${decipherIv.update(split[0], 'hex', 'utf8')}${decipherIv.final('utf8')}`;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICachePlugin, TokenCacheContext } from '@azure/msal-node';
|
||||
import { constants, promises as fsPromises } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
|
||||
export class MsalCachePluginProvider {
|
||||
constructor(
|
||||
private readonly _serviceName: string,
|
||||
private readonly _msalFilePath: string
|
||||
) {
|
||||
this._msalFilePath = path.join(this._msalFilePath, this._serviceName);
|
||||
this._serviceName = this._serviceName.replace(/-/, '_');
|
||||
Logger.verbose(`MsalCachePluginProvider: Using cache path ${_msalFilePath} and serviceName ${_serviceName}`);
|
||||
}
|
||||
|
||||
public getCachePlugin(): ICachePlugin {
|
||||
const beforeCacheAccess = async (cacheContext: TokenCacheContext): Promise<void> => {
|
||||
let exists = true;
|
||||
try {
|
||||
await fsPromises.access(this._msalFilePath, constants.R_OK | constants.W_OK);
|
||||
} catch {
|
||||
exists = false;
|
||||
}
|
||||
if (exists) {
|
||||
try {
|
||||
const cache = await fsPromises.readFile(this._msalFilePath, { encoding: 'utf8' });
|
||||
cacheContext.tokenCache.deserialize(cache);
|
||||
Logger.verbose(`MsalCachePlugin: Token read from cache successfully.`);
|
||||
} catch (e) {
|
||||
Logger.error(`MsalCachePlugin: Failed to read from cache file. ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const afterCacheAccess = async (cacheContext: TokenCacheContext): Promise<void> => {
|
||||
if (cacheContext.cacheHasChanged) {
|
||||
try {
|
||||
const data = cacheContext.tokenCache.serialize();
|
||||
await fsPromises.writeFile(this._msalFilePath, data, { encoding: 'utf8' });
|
||||
Logger.verbose(`MsalCachePlugin: Token written to cache successfully.`);
|
||||
} catch (e) {
|
||||
Logger.error(`MsalCachePlugin: Failed to write to cache file. ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// This is an implementation of ICachePlugin that uses the beforeCacheAccess and afterCacheAccess callbacks to read and write to a file
|
||||
// Ref https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-node-migration#enable-token-caching
|
||||
// In future we should use msal-node-extensions to provide a secure storage of tokens, instead of implementing our own
|
||||
// However - as of now this library does not come with pre-compiled native libraries that causes runtime issues
|
||||
// Ref https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/3332
|
||||
return {
|
||||
beforeCacheAccess,
|
||||
afterCacheAccess,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -51,8 +51,6 @@ export const S256_CODE_CHALLENGE_METHOD = 'S256';
|
||||
|
||||
export const SELECT_ACCOUNT = 'select_account';
|
||||
|
||||
export const ConfigFilePath = './cache.json'
|
||||
|
||||
export const Saw = 'saw';
|
||||
|
||||
export const ViewType = 'view';
|
||||
|
||||
Reference in New Issue
Block a user