UNPKG

@microsoft/agents-hosting

Version:

Microsoft 365 Agents SDK for JavaScript

385 lines 17.7 kB
"use strict"; /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MsalTokenProvider = void 0; const msal_node_1 = require("@azure/msal-node"); const axios_1 = __importDefault(require("axios")); const logger_1 = require("@microsoft/agents-activity/logger"); const uuid_1 = require("uuid"); const MemoryCache_1 = require("./MemoryCache"); const fs_1 = __importDefault(require("fs")); const crypto_1 = __importDefault(require("crypto")); const audience = 'api://AzureADTokenExchange'; const logger = (0, logger_1.debug)('agents:msal'); /** * Provides tokens using MSAL. */ class MsalTokenProvider { constructor(connectionSettings) { this.sysOptions = { loggerOptions: { logLevel: msal_node_1.LogLevel.Trace, loggerCallback: (level, message, containsPii) => { if (containsPii) { return; } switch (level) { case msal_node_1.LogLevel.Error: logger.error(message); return; case msal_node_1.LogLevel.Info: logger.debug(message); return; case msal_node_1.LogLevel.Warning: if (!message.includes('Warning - No client info in response')) { logger.warn(message); } return; case msal_node_1.LogLevel.Verbose: logger.debug(message); } }, piiLoggingEnabled: false } }; this._agenticTokenCache = new MemoryCache_1.MemoryCache(); this.connectionSettings = connectionSettings; } async getAccessToken(authConfigOrScope, scope) { let authConfig; let actualScope; if (typeof authConfigOrScope === 'string') { // Called as getAccessToken(scope) if (!this.connectionSettings) { throw new Error('Connection settings must be provided to constructor when calling getAccessToken(scope)'); } authConfig = this.connectionSettings; actualScope = authConfigOrScope; } else { // Called as getAccessToken(authConfig, scope) authConfig = authConfigOrScope; actualScope = scope; } if (!authConfig.clientId && process.env.NODE_ENV !== 'production') { return ''; } let token; if (authConfig.WIDAssertionFile !== undefined) { token = await this.acquireAccessTokenViaWID(authConfig, actualScope); } else if (authConfig.FICClientId !== undefined) { token = await this.acquireAccessTokenViaFIC(authConfig, actualScope); } else if (authConfig.clientSecret !== undefined) { token = await this.acquireAccessTokenViaSecret(authConfig, actualScope); } else if (authConfig.certPemFile !== undefined && authConfig.certKeyFile !== undefined) { token = await this.acquireTokenWithCertificate(authConfig, actualScope); } else if (authConfig.clientSecret === undefined && authConfig.certPemFile === undefined && authConfig.certKeyFile === undefined) { token = await this.acquireTokenWithUserAssignedIdentity(authConfig, actualScope); } else { throw new Error('Invalid authConfig. '); } if (token === undefined) { throw new Error('Failed to acquire token'); } return token; } async acquireTokenOnBehalfOf(authConfigOrScopes, scopesOrOboAssertion, oboAssertion) { let authConfig; let actualScopes; let actualOboAssertion; if (Array.isArray(authConfigOrScopes)) { // Called as acquireTokenOnBehalfOf(scopes, oboAssertion) if (!this.connectionSettings) { throw new Error('Connection settings must be provided to constructor when calling acquireTokenOnBehalfOf(scopes, oboAssertion)'); } authConfig = this.connectionSettings; actualScopes = authConfigOrScopes; actualOboAssertion = scopesOrOboAssertion; } else { // Called as acquireTokenOnBehalfOf(authConfig, scopes, oboAssertion) authConfig = authConfigOrScopes; actualScopes = scopesOrOboAssertion; actualOboAssertion = oboAssertion; } const cca = new msal_node_1.ConfidentialClientApplication({ auth: { clientId: authConfig.clientId, authority: `${authConfig.authority}/${authConfig.tenantId || 'botframework.com'}`, clientSecret: authConfig.clientSecret }, system: this.sysOptions }); const token = await cca.acquireTokenOnBehalfOf({ oboAssertion: actualOboAssertion, scopes: actualScopes }); return token === null || token === void 0 ? void 0 : token.accessToken; } async getAgenticInstanceToken(tenantId, agentAppInstanceId) { logger.debug('Getting agentic instance token'); if (!this.connectionSettings) { throw new Error('Connection settings must be provided when calling getAgenticInstanceToken'); } const appToken = await this.getAgenticApplicationToken(tenantId, agentAppInstanceId); const cca = new msal_node_1.ConfidentialClientApplication({ auth: { clientId: agentAppInstanceId, clientAssertion: appToken, authority: this.resolveAuthority(tenantId), }, system: this.sysOptions }); const token = await cca.acquireTokenByClientCredential({ scopes: ['api://AzureAdTokenExchange/.default'], correlationId: (0, uuid_1.v4)() }); if (!(token === null || token === void 0 ? void 0 : token.accessToken)) { throw new Error(`Failed to acquire instance token for agent instance: ${agentAppInstanceId}`); } return token.accessToken; } /** * This method can optionally accept a tenant ID that overrides the tenant ID in the connection settings, if the connection settings authority contains "common". * @param tenantId * @returns */ resolveAuthority(tenantId) { var _a, _b, _c, _d, _e, _f, _g, _h; // if for some reason the agentic tenant ID is not in the message, fall back to the original configured auth settings if (!tenantId) { return ((_a = this.connectionSettings) === null || _a === void 0 ? void 0 : _a.authority) ? `${this.connectionSettings.authority}/${(_b = this.connectionSettings) === null || _b === void 0 ? void 0 : _b.tenantId}` : `https://login.microsoftonline.com/${((_c = this.connectionSettings) === null || _c === void 0 ? void 0 : _c.tenantId) || 'botframework.com'}`; } if (((_d = this.connectionSettings) === null || _d === void 0 ? void 0 : _d.tenantId) === 'common') { return ((_e = this.connectionSettings) === null || _e === void 0 ? void 0 : _e.authority) ? `${this.connectionSettings.authority}/${tenantId}` : `https://login.microsoftonline.com/${tenantId}`; } else { return ((_f = this.connectionSettings) === null || _f === void 0 ? void 0 : _f.authority) ? `${this.connectionSettings.authority}/${(_g = this.connectionSettings) === null || _g === void 0 ? void 0 : _g.tenantId}` : `https://login.microsoftonline.com/${((_h = this.connectionSettings) === null || _h === void 0 ? void 0 : _h.tenantId) || 'botframework.com'}`; } } /** * Does a direct HTTP call to acquire a token for agentic scenarios - do not use this directly! * This method will be removed once MSAL is updated with the necessary features. * (This is required in order to pass additional parameters into the auth call) * @param tenantId * @param clientId * @param clientAssertion * @param scopes * @param tokenBodyParameters * @returns */ async acquireTokenByForAgenticScenarios(tenantId, clientId, clientAssertion, scopes, tokenBodyParameters) { if (!this.connectionSettings) { throw new Error('Connection settings must be provided when calling getAgenticInstanceToken'); } // Check cache first const cacheKey = `${clientId}/${Object.keys(tokenBodyParameters).map(key => key !== 'user_federated_identity_credential' ? `${key}=${tokenBodyParameters[key]}` : '').join('&')}/${scopes.join(';')}`; if (this._agenticTokenCache.get(cacheKey)) { return this._agenticTokenCache.get(cacheKey); } const url = `${this.resolveAuthority(tenantId)}/oauth2/v2.0/token`; const data = { client_id: clientId, scope: scopes.join(' '), ...tokenBodyParameters }; if (clientAssertion) { data.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'; data.client_assertion = clientAssertion; } else { data.client_secret = this.connectionSettings.clientSecret; } const token = await axios_1.default.post(url, data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' } }).catch((error) => { logger.error('Error acquiring token: ', error.toJSON()); throw error; }); // capture token, expire local cache 5 minutes early this._agenticTokenCache.set(cacheKey, token.data.access_token, token.data.expires_in - 300); return token.data.access_token; } async getAgenticUserToken(tenantId, agentAppInstanceId, agenticUserId, scopes) { logger.debug('Getting agentic user token'); const agentToken = await this.getAgenticApplicationToken(tenantId, agentAppInstanceId); const instanceToken = await this.getAgenticInstanceToken(tenantId, agentAppInstanceId); const token = await this.acquireTokenByForAgenticScenarios(tenantId, agentToken, instanceToken, scopes, { user_id: agenticUserId, user_federated_identity_credential: instanceToken, grant_type: 'user_fic', }); if (!token) { throw new Error(`Failed to acquire instance token for user token: ${agentAppInstanceId}`); } return token; } async getAgenticApplicationToken(tenantId, agentAppInstanceId) { var _a; if (!((_a = this.connectionSettings) === null || _a === void 0 ? void 0 : _a.clientId)) { throw new Error('Connection settings must be provided when calling getAgenticApplicationToken'); } logger.debug('Getting agentic application token'); const token = await this.acquireTokenByForAgenticScenarios(tenantId, this.connectionSettings.clientId, undefined, ['api://AzureAdTokenExchange/.default'], { grant_type: 'client_credentials', fmi_path: agentAppInstanceId, }); if (!token) { throw new Error(`Failed to acquire token for agent instance: ${agentAppInstanceId}`); } return token; } /** * Acquires a token using a user-assigned identity. * @param authConfig The authentication configuration. * @param scope The scope for the token. * @returns A promise that resolves to the access token. */ async acquireTokenWithUserAssignedIdentity(authConfig, scope) { const mia = new msal_node_1.ManagedIdentityApplication({ managedIdentityIdParams: { userAssignedClientId: authConfig.clientId || '' }, system: this.sysOptions }); const token = await mia.acquireToken({ resource: scope }); return token === null || token === void 0 ? void 0 : token.accessToken; } /** * Acquires a token using a certificate. * @param authConfig The authentication configuration. * @param scope The scope for the token. * @returns A promise that resolves to the access token. */ async acquireTokenWithCertificate(authConfig, scope) { const privateKeySource = fs_1.default.readFileSync(authConfig.certKeyFile); const privateKeyObject = crypto_1.default.createPrivateKey({ key: privateKeySource, format: 'pem' }); const privateKey = privateKeyObject.export({ format: 'pem', type: 'pkcs8' }); const pubKeyObject = new crypto_1.default.X509Certificate(fs_1.default.readFileSync(authConfig.certPemFile)); const cca = new msal_node_1.ConfidentialClientApplication({ auth: { clientId: authConfig.clientId || '', authority: `${authConfig.authority}/${authConfig.tenantId || 'botframework.com'}`, clientCertificate: { privateKey: privateKey, thumbprint: pubKeyObject.fingerprint.replaceAll(':', ''), x5c: Buffer.from(authConfig.certPemFile, 'base64').toString() } }, system: this.sysOptions }); const token = await cca.acquireTokenByClientCredential({ scopes: [`${scope}/.default`], correlationId: (0, uuid_1.v4)() }); return token === null || token === void 0 ? void 0 : token.accessToken; } /** * Acquires a token using a client secret. * @param authConfig The authentication configuration. * @param scope The scope for the token. * @returns A promise that resolves to the access token. */ async acquireAccessTokenViaSecret(authConfig, scope) { const cca = new msal_node_1.ConfidentialClientApplication({ auth: { clientId: authConfig.clientId, authority: `${authConfig.authority}/${authConfig.tenantId || 'botframework.com'}`, clientSecret: authConfig.clientSecret }, system: this.sysOptions }); const token = await cca.acquireTokenByClientCredential({ scopes: [`${scope}/.default`], correlationId: (0, uuid_1.v4)() }); return token === null || token === void 0 ? void 0 : token.accessToken; } /** * Acquires a token using a FIC client assertion. * @param authConfig The authentication configuration. * @param scope The scope for the token. * @returns A promise that resolves to the access token. */ async acquireAccessTokenViaFIC(authConfig, scope) { const scopes = [`${scope}/.default`]; const clientAssertion = await this.fetchExternalToken(authConfig.FICClientId); const cca = new msal_node_1.ConfidentialClientApplication({ auth: { clientId: authConfig.clientId, authority: `${authConfig.authority}/${authConfig.tenantId}`, clientAssertion }, system: this.sysOptions }); const token = await cca.acquireTokenByClientCredential({ scopes }); logger.debug('got token using FIC client assertion'); return token === null || token === void 0 ? void 0 : token.accessToken; } /** * Acquires a token using a Workload Identity client assertion. * @param authConfig The authentication configuration. * @param scope The scope for the token. * @returns A promise that resolves to the access token. */ async acquireAccessTokenViaWID(authConfig, scope) { const scopes = [`${scope}/.default`]; const clientAssertion = fs_1.default.readFileSync(authConfig.WIDAssertionFile, 'utf8'); const cca = new msal_node_1.ConfidentialClientApplication({ auth: { clientId: authConfig.clientId, authority: `https://login.microsoftonline.com/${authConfig.tenantId}`, clientAssertion }, system: this.sysOptions }); const token = await cca.acquireTokenByClientCredential({ scopes }); logger.info('got token using WID client assertion'); return token === null || token === void 0 ? void 0 : token.accessToken; } /** * Fetches an external token. * @param FICClientId The FIC client ID. * @returns A promise that resolves to the external token. */ async fetchExternalToken(FICClientId) { const managedIdentityClientAssertion = new msal_node_1.ManagedIdentityApplication({ managedIdentityIdParams: { userAssignedClientId: FICClientId }, system: this.sysOptions }); const response = await managedIdentityClientAssertion.acquireToken({ resource: audience, forceRefresh: true }); logger.debug('got token for FIC'); return response.accessToken; } } exports.MsalTokenProvider = MsalTokenProvider; //# sourceMappingURL=msalTokenProvider.js.map