@microsoft/agents-hosting
Version:
Microsoft 365 Agents SDK for JavaScript
355 lines (312 loc) • 11.7 kB
text/typescript
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { debug } from '@microsoft/agents-activity/logger'
import { ConnectionMapItem } from './msalConnectionManager'
import objectPath from 'object-path'
const logger = debug('agents:authConfiguration')
const DEFAULT_CONNECTION = 'serviceConnection'
/**
* Represents the authentication configuration.
*/
export interface AuthConfiguration {
/**
* The tenant ID for the authentication configuration.
*/
tenantId?: string
/**
* The client ID for the authentication configuration. Required in production.
*/
clientId?: string
/**
* The client secret for the authentication configuration.
*/
clientSecret?: string
/**
* The path to the certificate PEM file.
*/
certPemFile?: string
/**
* The path to the certificate key file.
*/
certKeyFile?: string
/**
* A list of valid issuers for the authentication configuration.
*/
issuers?: string[]
/**
* The connection name for the authentication configuration.
*/
connectionName?: string
/**
* The FIC (First-Party Integration Channel) client ID.
*/
FICClientId?: string,
/**
* Entra Authentication Endpoint to use.
*
* @remarks
* If not populated the Entra Public Cloud endpoint is assumed.
* This example of Public Cloud Endpoint is https://login.microsoftonline.com
* see also https://learn.microsoft.com/entra/identity-platform/authentication-national-cloud
*/
authority?: string
scope?: string
/**
* A map of connection names to their respective authentication configurations.
*/
connections?: Map<string, AuthConfiguration>
/**
* A list of connection map items to map service URLs to connection names.
*/
connectionsMap?: ConnectionMapItem[],
/**
* An optional alternative blueprint Connection name used when constructing a connector client.
*/
altBlueprintConnectionName?: string
/**
* The path to K8s provided token.
*/
WIDAssertionFile?: string
}
/**
* Loads the authentication configuration from environment variables.
*
* @returns The authentication configuration.
* @throws Will throw an error if clientId is not provided in production.
*
* @remarks
* - `clientId` is required
*
* @example
* ```
* tenantId=your-tenant-id
* clientId=your-client-id
* clientSecret=your-client-secret
*
* certPemFile=your-cert-pem-file
* certKeyFile=your-cert-key-file
*
* FICClientId=your-FIC-client-id
*
* connectionName=your-connection-name
* authority=your-authority-endpoint
* ```
*
*/
export const loadAuthConfigFromEnv = (cnxName?: string): AuthConfiguration => {
const envConnections = loadConnectionsMapFromEnv()
let authConfig: AuthConfiguration
if (envConnections.connectionsMap.length === 0) {
// No connections provided, we need to populate the connections map with the old config settings
authConfig = buildLegacyAuthConfig(cnxName)
envConnections.connections.set(DEFAULT_CONNECTION, authConfig)
envConnections.connectionsMap.push({
serviceUrl: '*',
connection: DEFAULT_CONNECTION,
})
} else {
// There are connections provided, use the default or specified connection
if (cnxName) {
const entry = envConnections.connections.get(cnxName)
if (entry) {
authConfig = entry
} else {
throw new Error(`Connection "${cnxName}" not found in environment.`)
}
} else {
const defaultItem = envConnections.connectionsMap.find((item) => item.serviceUrl === '*')
const defaultConn = defaultItem ? envConnections.connections.get(defaultItem.connection) : undefined
if (!defaultConn) {
throw new Error('No default connection found in environment connections.')
}
authConfig = defaultConn
}
authConfig.authority ??= 'https://login.microsoftonline.com'
authConfig.issuers ??= getDefaultIssuers(authConfig.tenantId ?? '', authConfig.authority)
}
return {
...authConfig,
...envConnections,
}
}
/**
* Loads the agent authentication configuration from previous version environment variables.
*
* @returns The agent authentication configuration.
* @throws Will throw an error if MicrosoftAppId is not provided in production.
*
* @example
* ```
* MicrosoftAppId=your-client-id
* MicrosoftAppPassword=your-client-secret
* MicrosoftAppTenantId=your-tenant-id
* ```
*
*/
export const loadPrevAuthConfigFromEnv: () => AuthConfiguration = () => {
const envConnections = loadConnectionsMapFromEnv()
let authConfig: AuthConfiguration = {}
if (envConnections.connectionsMap.length === 0) {
// No connections provided, we need to populate the connection map with the old config settings
if (process.env.MicrosoftAppId === undefined && process.env.NODE_ENV === 'production') {
throw new Error('ClientId required in production')
}
const authority = process.env.authorityEndpoint ?? 'https://login.microsoftonline.com'
authConfig = {
tenantId: process.env.MicrosoftAppTenantId,
clientId: process.env.MicrosoftAppId,
clientSecret: process.env.MicrosoftAppPassword,
certPemFile: process.env.certPemFile,
certKeyFile: process.env.certKeyFile,
connectionName: process.env.connectionName,
FICClientId: process.env.MicrosoftAppClientId,
authority,
scope: process.env.scope,
issuers: getDefaultIssuers(process.env.MicrosoftAppTenantId ?? '', authority),
altBlueprintConnectionName: process.env.altBlueprintConnectionName,
WIDAssertionFile: process.env.WIDAssertionFile,
}
envConnections.connections.set(DEFAULT_CONNECTION, authConfig)
envConnections.connectionsMap.push({
serviceUrl: '*',
connection: DEFAULT_CONNECTION,
})
} else {
// There are connections provided, use the default one.
const defaultItem = envConnections.connectionsMap.find((item) => item.serviceUrl === '*')
const defaultConn = defaultItem ? envConnections.connections.get(defaultItem.connection) : undefined
if (!defaultConn) {
throw new Error('No default connection found in environment connections.')
}
authConfig = defaultConn
}
authConfig.authority ??= 'https://login.microsoftonline.com'
authConfig.issuers ??= getDefaultIssuers(authConfig.tenantId ?? '', authConfig.authority)
return { ...authConfig, ...envConnections }
}
function loadConnectionsMapFromEnv () {
const envVars = process.env
const connectionsObj: Record<string, any> = {}
const connectionsMap: ConnectionMapItem[] = []
const CONNECTIONS_PREFIX = 'connections__'
const CONNECTIONS_MAP_PREFIX = 'connectionsMap__'
for (const [key, value] of Object.entries(envVars)) {
if (key.startsWith(CONNECTIONS_PREFIX)) {
// Convert to dot notation
let path = key.substring(CONNECTIONS_PREFIX.length).replace(/__/g, '.')
// Remove ".settings." from the path
path = path.replace('.settings.', '.')
objectPath.set(connectionsObj, path, value)
} else if (key.startsWith(CONNECTIONS_MAP_PREFIX)) {
const path = key.substring(CONNECTIONS_MAP_PREFIX.length).replace(/__/g, '.')
objectPath.set(connectionsMap, path, value)
}
}
// Convert connectionsObj to Map<string, AuthConfiguration>
const connections: Map<string, AuthConfiguration> = new Map(Object.entries(connectionsObj))
if (connections.size === 0) {
logger.warn('No connections found in configuration.')
}
if (connectionsMap.length === 0) {
logger.warn('No connections map found in configuration.')
if (connections.size > 0) {
const firstEntry = connections.entries().next().value
if (firstEntry) {
const [firstKey] = firstEntry
// Provide a default connection map if none is specified
connectionsMap.push({
serviceUrl: '*',
connection: firstKey,
})
}
}
}
return {
connections,
connectionsMap,
}
}
/**
* Loads the authentication configuration from the provided config or from the environment variables
* providing default values for authority and issuers.
*
* @returns The authentication configuration.
* @throws Will throw an error if clientId is not provided in production.
*
* @example
* ```
* tenantId=your-tenant-id
* clientId=your-client-id
* clientSecret=your-client-secret
*
* certPemFile=your-cert-pem-file
* certKeyFile=your-cert-key-file
*
* FICClientId=your-FIC-client-id
*
* connectionName=your-connection-name
* authority=your-authority-endpoint
* ```
*
*/
export function getAuthConfigWithDefaults (config?: AuthConfiguration): AuthConfiguration {
if (!config) return loadAuthConfigFromEnv()
const providedConnections = config.connections && config.connectionsMap
? { connections: config.connections, connectionsMap: config.connectionsMap }
: undefined
const connections = providedConnections ?? loadConnectionsMapFromEnv()
let mergedConfig: AuthConfiguration
if (connections && connections.connectionsMap?.length === 0) {
// No connections provided, we need to populate the connections map with the old config settings
mergedConfig = buildLegacyAuthConfig(undefined, config)
connections.connections?.set(DEFAULT_CONNECTION, mergedConfig)
connections.connectionsMap.push({ serviceUrl: '*', connection: DEFAULT_CONNECTION })
} else {
// There are connections provided, use the default connection
const defaultItem = connections.connectionsMap?.find((item) => item.serviceUrl === '*')
const defaultConn = defaultItem ? connections.connections?.get(defaultItem.connection) : undefined
if (!defaultConn) {
throw new Error('No default connection found in environment connections.')
}
mergedConfig = buildLegacyAuthConfig(undefined, defaultConn)
}
return {
...mergedConfig,
...connections,
}
}
function buildLegacyAuthConfig (envPrefix: string = '', customConfig?: AuthConfiguration): AuthConfiguration {
const prefix = envPrefix ? `${envPrefix}_` : ''
const authority = customConfig?.authority ?? process.env[`${prefix}authorityEndpoint`] ?? 'https://login.microsoftonline.com'
const clientId = customConfig?.clientId ?? process.env[`${prefix}clientId`]
if (!clientId && !envPrefix && process.env.NODE_ENV === 'production') {
throw new Error('ClientId required in production')
}
if (!clientId && envPrefix) {
throw new Error(`ClientId not found for connection: ${envPrefix}`)
}
const tenantId = customConfig?.tenantId ?? process.env[`${prefix}tenantId`]
return {
tenantId,
clientId: clientId!,
clientSecret: customConfig?.clientSecret ?? process.env[`${prefix}clientSecret`],
certPemFile: customConfig?.certPemFile ?? process.env[`${prefix}certPemFile`],
certKeyFile: customConfig?.certKeyFile ?? process.env[`${prefix}certKeyFile`],
connectionName: customConfig?.connectionName ?? process.env[`${prefix}connectionName`],
FICClientId: customConfig?.FICClientId ?? process.env[`${prefix}FICClientId`],
authority,
scope: customConfig?.scope ?? process.env[`${prefix}scope`],
issuers: customConfig?.issuers ?? getDefaultIssuers(tenantId as string, authority),
altBlueprintConnectionName: customConfig?.altBlueprintConnectionName ?? process.env[`${prefix}altBlueprintConnectionName`],
WIDAssertionFile: customConfig?.WIDAssertionFile ?? process.env[`${prefix}WIDAssertionFile`]
}
}
function getDefaultIssuers (tenantId: string, authority: string) : string[] {
return [
'https://api.botframework.com',
`https://sts.windows.net/${tenantId}/`,
`${authority}/${tenantId}/v2.0`
]
}