@microsoft/agents-hosting
Version:
Microsoft 365 Agents SDK for JavaScript
221 lines (204 loc) • 7.43 kB
text/typescript
/**
* 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
}
}