UNPKG

@microsoft/agents-hosting

Version:

Microsoft 365 Agents SDK for JavaScript

221 lines (204 loc) 7.43 kB
/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { ConfidentialClientApplication, LogLevel, ManagedIdentityApplication, NodeSystemOptions } from '@azure/msal-node' import { AuthConfiguration } from './authConfiguration' import { AuthProvider } from './authProvider' import { debug } from '@microsoft/agents-activity/logger' import { v4 } from 'uuid' import fs from 'fs' import crypto from 'crypto' const audience = 'api://AzureADTokenExchange' const logger = debug('agents:msal') /** * Provides tokens using MSAL. */ export class MsalTokenProvider implements AuthProvider { /** * Gets an access token. * @param authConfig The authentication configuration. * @param scope The scope for the token. * @returns A promise that resolves to the access token. */ public async getAccessToken (authConfig: AuthConfiguration, scope: string): Promise<string> { if (!authConfig.clientId && process.env.NODE_ENV !== 'production') { return '' } let token if (authConfig.FICClientId !== undefined) { token = await this.acquireAccessTokenViaFIC(authConfig, scope) } else if (authConfig.clientSecret !== undefined) { token = await this.acquireAccessTokenViaSecret(authConfig, scope) } else if (authConfig.certPemFile !== undefined && authConfig.certKeyFile !== undefined) { token = await this.acquireTokenWithCertificate(authConfig, scope) } else if (authConfig.clientSecret === undefined && authConfig.certPemFile === undefined && authConfig.certKeyFile === undefined) { token = await this.acquireTokenWithUserAssignedIdentity(authConfig, scope) } else { throw new Error('Invalid authConfig. ') } if (token === undefined) { throw new Error('Failed to acquire token') } return token } public async acquireTokenOnBehalfOf (authConfig: AuthConfiguration, scopes: string[], oboAssertion: string): Promise<string> { const cca = new ConfidentialClientApplication({ auth: { clientId: authConfig.clientId as string, authority: `${authConfig.authority}/${authConfig.tenantId || 'botframework.com'}`, clientSecret: authConfig.clientSecret }, system: this.sysOptions }) const token = await cca.acquireTokenOnBehalfOf({ oboAssertion, scopes }) return token?.accessToken as string } private readonly sysOptions: NodeSystemOptions = { loggerOptions: { logLevel: LogLevel.Trace, loggerCallback: (level, message, containsPii) => { if (containsPii) { return } switch (level) { case LogLevel.Error: logger.error(message) return case LogLevel.Info: logger.debug(message) return case LogLevel.Warning: if (!message.includes('Warning - No client info in response')) { logger.warn(message) } return case LogLevel.Verbose: logger.debug(message) } }, piiLoggingEnabled: false } } /** * 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. */ private async acquireTokenWithUserAssignedIdentity (authConfig: AuthConfiguration, scope: string) { const mia = new ManagedIdentityApplication({ managedIdentityIdParams: { userAssignedClientId: authConfig.clientId || '' }, system: this.sysOptions }) const token = await mia.acquireToken({ resource: scope }) return 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. */ private async acquireTokenWithCertificate (authConfig: AuthConfiguration, scope: string) { const privateKeySource = fs.readFileSync(authConfig.certKeyFile as string) const privateKeyObject = crypto.createPrivateKey({ key: privateKeySource, format: 'pem' }) const privateKey = privateKeyObject.export({ format: 'pem', type: 'pkcs8' }) const pubKeyObject = new crypto.X509Certificate(fs.readFileSync(authConfig.certPemFile as string)) const cca = new ConfidentialClientApplication({ auth: { clientId: authConfig.clientId || '', authority: `${authConfig.authority}/${authConfig.tenantId || 'botframework.com'}`, clientCertificate: { privateKey: privateKey as string, thumbprint: pubKeyObject.fingerprint.replaceAll(':', ''), x5c: Buffer.from(authConfig.certPemFile as string, 'base64').toString() } }, system: this.sysOptions }) const token = await cca.acquireTokenByClientCredential({ scopes: [`${scope}/.default`], correlationId: v4() }) return token?.accessToken as string } /** * 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. */ private async acquireAccessTokenViaSecret (authConfig: AuthConfiguration, scope: string) { const cca = new ConfidentialClientApplication({ auth: { clientId: authConfig.clientId as string, authority: `${authConfig.authority}/${authConfig.tenantId || 'botframework.com'}`, clientSecret: authConfig.clientSecret }, system: this.sysOptions }) const token = await cca.acquireTokenByClientCredential({ scopes: [`${scope}/.default`], correlationId: v4() }) return token?.accessToken as string } /** * 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. */ private async acquireAccessTokenViaFIC (authConfig: AuthConfiguration, scope: string) : Promise<string> { const scopes = [`${scope}/.default`] const clientAssertion = await this.fetchExternalToken(authConfig.FICClientId as string) const cca = new ConfidentialClientApplication({ auth: { clientId: authConfig.clientId as string, 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?.accessToken as string } /** * Fetches an external token. * @param FICClientId The FIC client ID. * @returns A promise that resolves to the external token. */ private async fetchExternalToken (FICClientId: string) : Promise<string> { const managedIdentityClientAssertion = new 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 } }