Skip to content
28 changes: 28 additions & 0 deletions packages/agents-hosting/src/auth/authConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ import objectPath from 'object-path'
const logger = debug('agents:authConfiguration')
const DEFAULT_CONNECTION = 'serviceConnection'

/**
* Supported authentication types for agent connections.
*/
export enum AuthType {
Certificate = 'Certificate',
CertificateSubjectName = 'CertificateSubjectName',
ClientSecret = 'ClientSecret',
UserManagedIdentity = 'UserManagedIdentity',
SystemManagedIdentity = 'SystemManagedIdentity',
FederatedCredentials = 'FederatedCredentials',
WorkloadIdentity = 'WorkloadIdentity'
}

/**
* Represents the authentication configuration.
*/
Expand Down Expand Up @@ -88,9 +101,20 @@ export interface AuthConfiguration {

/**
* The path to K8s provided token.
* @deprecated Use `authtype` set to `'WorkloadIdentity'` and `federatedtokenfile` instead.
*/
WIDAssertionFile?: string

/**
* The authentication type for the connection.
*/
authtype?: AuthType | string

/**
* The path to the federated token file used for Workload Identity authentication.
*/
federatedtokenfile?: string

/**
* The Azure region for ESTS-R regional token acquisition (e.g. 'westus', 'eastus').
* When set, MSAL routes token requests to the specified regional endpoint.
Expand Down Expand Up @@ -203,6 +227,8 @@ export const loadPrevAuthConfigFromEnv: () => AuthConfiguration = () => {
issuers: getDefaultIssuers(process.env.MicrosoftAppTenantId ?? '', authority),
altBlueprintConnectionName: process.env.altBlueprintConnectionName,
WIDAssertionFile: process.env.WIDAssertionFile,
authtype: process.env.authtype,
federatedtokenfile: process.env.federatedtokenfile,
azureRegion: process.env.azureRegion,
}
envConnections.connections.set(DEFAULT_CONNECTION, authConfig)
Expand Down Expand Up @@ -361,6 +387,8 @@ function buildLegacyAuthConfig (envPrefix: string = '', customConfig?: AuthConfi
issuers: customConfig?.issuers ?? getDefaultIssuers(tenantId as string, authority),
altBlueprintConnectionName: customConfig?.altBlueprintConnectionName ?? process.env[`${prefix}altBlueprintConnectionName`],
WIDAssertionFile: customConfig?.WIDAssertionFile ?? process.env[`${prefix}WIDAssertionFile`],
authtype: customConfig?.authtype ?? process.env[`${prefix}authtype`],
federatedtokenfile: customConfig?.federatedtokenfile ?? process.env[`${prefix}federatedtokenfile`],
azureRegion: customConfig?.azureRegion ?? process.env[`${prefix}azureRegion`]
}
}
Expand Down
19 changes: 11 additions & 8 deletions packages/agents-hosting/src/auth/msalConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Activity, RoleTypes } from '@microsoft/agents-activity'
import { debug } from '@microsoft/agents-telemetry'
import { AuthConfiguration, resolveAuthority } from './authConfiguration'
import { AuthConfiguration, AuthType, resolveAuthority } from './authConfiguration'
import { Connections } from './connections'
import { MsalTokenProvider } from './msalTokenProvider'
import { JwtPayload } from 'jsonwebtoken'
Expand Down Expand Up @@ -44,13 +44,16 @@ export class MsalConnectionManager implements Connections {

for (const [name, provider] of this._connections.entries()) {
const cfg = provider.connectionSettings
const authType = cfg?.certPemFile
? 'certificate'
: cfg?.clientSecret
? 'clientSecret'
: cfg?.WIDAssertionFile || cfg?.FICClientId
? 'workloadIdentity'
: 'none'
const authType = cfg?.authtype ??
(cfg?.certPemFile
? AuthType.Certificate
: cfg?.clientSecret
? AuthType.ClientSecret
: cfg?.WIDAssertionFile
? AuthType.WorkloadIdentity
: cfg?.FICClientId
? AuthType.FederatedCredentials
: 'none')
logger.debug('connection "%s" clientId=%s tenantId=%s authType=%s', name, cfg?.clientId ?? '<none>', cfg?.tenantId ?? '<none>', authType)
}

Expand Down
117 changes: 102 additions & 15 deletions packages/agents-hosting/src/auth/msalTokenProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { ConfidentialClientApplication, LogLevel, ManagedIdentityApplication, NodeSystemOptions } from '@azure/msal-node'
import axios from 'axios'
import { AuthConfiguration, resolveAuthority as resolveAuthorityUtil } from './authConfiguration'
import { AuthConfiguration, AuthType, resolveAuthority as resolveAuthorityUtil } from './authConfiguration'
import { AuthProvider } from './authProvider'
import { debug, trace } from '@microsoft/agents-telemetry'
import { v4 } from 'uuid'
Expand All @@ -15,6 +15,8 @@ import jwt from 'jsonwebtoken'
import fs from 'fs'
import crypto from 'crypto'
import { AuthenticationTraceDefinitions } from '../observability'
import { ExceptionHelper } from '@microsoft/agents-activity'
import { Errors } from '../errorHelper'

const audience = 'api://AzureADTokenExchange'
const logger = debug('agents:msal')
Expand Down Expand Up @@ -70,28 +72,71 @@ export class MsalTokenProvider implements AuthProvider {
}

let token
if (authConfig.WIDAssertionFile !== undefined) {
record({ method: 'wid' })
logger.debug('getAccessToken via WID clientId=%s scope=%s', authConfig.clientId, actualScope)
if (authConfig.authtype) {
record({ method: authConfig.authtype })
logger.debug(`getAccessToken via ${authConfig.authtype} clientId=${authConfig.clientId} scope=${actualScope}`)
switch (authConfig.authtype) {
case AuthType.WorkloadIdentity: {
const tokenFilePath = authConfig.federatedtokenfile ?? authConfig.WIDAssertionFile
if (!tokenFilePath) {
throw ExceptionHelper.generateException(Error, Errors.WorkloadIdentityTokenFileRequired)
}
token = await this.acquireAccessTokenViaWID(authConfig, actualScope)
break
}
case AuthType.FederatedCredentials:
if (!authConfig.FICClientId) {
throw ExceptionHelper.generateException(Error, Errors.FICClientIdRequired)
}
token = await this.acquireAccessTokenViaFIC(authConfig, actualScope)
break
case AuthType.ClientSecret:
if (!authConfig.clientSecret) {
throw ExceptionHelper.generateException(Error, Errors.ClientSecretRequired)
}
token = await this.acquireAccessTokenViaSecret(authConfig, actualScope)
break
case AuthType.Certificate:
case AuthType.CertificateSubjectName:
if (!authConfig.certPemFile || !authConfig.certKeyFile) {
throw ExceptionHelper.generateException(Error, Errors.CertificateFilesRequired)
}
token = await this.acquireTokenWithCertificate(authConfig, actualScope)
break
case AuthType.UserManagedIdentity:
if (!authConfig.clientId) {
throw ExceptionHelper.generateException(Error, Errors.ClientIdRequiredForUserManagedIdentity)
}
token = await this.acquireTokenWithUserAssignedIdentity(authConfig, actualScope)
break
case AuthType.SystemManagedIdentity:
token = await this.acquireTokenWithSystemAssignedIdentity(authConfig, actualScope)
Comment thread
ceciliaavila marked this conversation as resolved.
break
Comment thread
ceciliaavila marked this conversation as resolved.
default:
throw ExceptionHelper.generateException(Error, Errors.UnsupportedAuthType, undefined, { authType: authConfig.authtype })
}
} else if (authConfig.WIDAssertionFile !== undefined) {
record({ method: AuthType.WorkloadIdentity })
logger.debug('getAccessToken via method=%s clientId=%s scope=%s', AuthType.WorkloadIdentity, authConfig.clientId, actualScope)
token = await this.acquireAccessTokenViaWID(authConfig, actualScope)
} else if (authConfig.FICClientId !== undefined) {
record({ method: 'fic' })
logger.debug('getAccessToken via FIC clientId=%s scope=%s', authConfig.clientId, actualScope)
record({ method: AuthType.FederatedCredentials })
logger.debug('getAccessToken via method=%s clientId=%s scope=%s', AuthType.FederatedCredentials, authConfig.clientId, actualScope)
token = await this.acquireAccessTokenViaFIC(authConfig, actualScope)
} else if (authConfig.clientSecret !== undefined) {
record({ method: 'secret' })
logger.debug('getAccessToken via secret clientId=%s scope=%s', authConfig.clientId, actualScope)
record({ method: AuthType.ClientSecret })
logger.debug('getAccessToken via method=%s clientId=%s scope=%s', AuthType.ClientSecret, authConfig.clientId, actualScope)
token = await this.acquireAccessTokenViaSecret(authConfig, actualScope)
} else if (authConfig.certPemFile !== undefined &&
authConfig.certKeyFile !== undefined) {
record({ method: 'certificate' })
logger.debug('getAccessToken via certificate clientId=%s scope=%s', authConfig.clientId, actualScope)
record({ method: AuthType.Certificate })
logger.debug('getAccessToken via method=%s clientId=%s scope=%s', AuthType.Certificate, authConfig.clientId, actualScope)
token = await this.acquireTokenWithCertificate(authConfig, actualScope)
} else if (authConfig.clientSecret === undefined &&
authConfig.certPemFile === undefined &&
authConfig.certKeyFile === undefined) {
record({ method: 'managed_identity' })
logger.debug('getAccessToken via managed identity clientId=%s scope=%s', authConfig.clientId, actualScope)
record({ method: AuthType.UserManagedIdentity })
logger.debug('getAccessToken via method=%s clientId=%s scope=%s', AuthType.UserManagedIdentity, authConfig.clientId, actualScope)
token = await this.acquireTokenWithUserAssignedIdentity(authConfig, actualScope)
} else {
throw new Error('Invalid authConfig. ')
Expand Down Expand Up @@ -305,8 +350,33 @@ export class MsalTokenProvider implements AuthProvider {

let clientAssertion

if (this.connectionSettings.WIDAssertionFile !== undefined) {
clientAssertion = fs.readFileSync(this.connectionSettings.WIDAssertionFile as string, 'utf8')
if (this.connectionSettings.authtype) {
switch (this.connectionSettings.authtype) {
case AuthType.WorkloadIdentity: {
const tokenFilePath = this.connectionSettings.federatedtokenfile ?? this.connectionSettings.WIDAssertionFile
if (tokenFilePath === undefined) {
Comment thread
ceciliaavila marked this conversation as resolved.
throw ExceptionHelper.generateException(Error, Errors.WorkloadIdentityTokenFileRequired)
}
clientAssertion = fs.readFileSync(tokenFilePath as string, 'utf8')
break
}
case AuthType.FederatedCredentials:
if (!this.connectionSettings.FICClientId) {
throw ExceptionHelper.generateException(Error, Errors.FICClientIdRequired)
}
clientAssertion = await this.fetchExternalToken(this.connectionSettings.FICClientId as string)
break
case AuthType.Certificate:
case AuthType.CertificateSubjectName:
if (!this.connectionSettings.certPemFile || !this.connectionSettings.certKeyFile) {
throw ExceptionHelper.generateException(Error, Errors.CertificateFilesRequired)
}
clientAssertion = this.getAssertionFromCert(this.connectionSettings)
break
}
} else if (this.connectionSettings.WIDAssertionFile !== undefined) {
const tokenFilePath = this.connectionSettings.federatedtokenfile ?? this.connectionSettings.WIDAssertionFile
clientAssertion = fs.readFileSync(tokenFilePath as string, 'utf8')
} else if (this.connectionSettings.FICClientId !== undefined) {
clientAssertion = await this.fetchExternalToken(this.connectionSettings.FICClientId as string)
} else if (this.connectionSettings.certPemFile !== undefined &&
Expand Down Expand Up @@ -415,6 +485,22 @@ export class MsalTokenProvider implements AuthProvider {
return token?.accessToken
}

/**
* Acquires a token using a system-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 acquireTokenWithSystemAssignedIdentity (authConfig: AuthConfiguration, scope: string) {
const mia = new ManagedIdentityApplication({
system: this.sysOptions
})
const token = await mia.acquireToken({
resource: scope
})
return token?.accessToken
}

/**
* Acquires a token using a certificate.
* @param authConfig The authentication configuration.
Expand Down Expand Up @@ -519,7 +605,8 @@ export class MsalTokenProvider implements AuthProvider {
*/
private async acquireAccessTokenViaWID (authConfig: AuthConfiguration, scope: string) : Promise<string> {
const scopes = [`${scope}/.default`]
const clientAssertion = fs.readFileSync(authConfig.WIDAssertionFile as string, 'utf8')
const tokenFilePath = authConfig.federatedtokenfile ?? authConfig.WIDAssertionFile
const clientAssertion = fs.readFileSync(tokenFilePath as string, 'utf8')
const cca = new ConfidentialClientApplication({
auth: {
clientId: authConfig.clientId as string,
Expand Down
48 changes: 48 additions & 0 deletions packages/agents-hosting/src/errorHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,54 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
description: "The current token for the '{connectionName}' AzureBot connection is not exchangeable for an on-behalf-of flow. Ensure the base token audience is for the bot/resource app, such as an App ID URI like 'api://...' or otherwise includes the app's client id."
},

/**
* Error thrown when an unsupported authentication type is specified in authConfig.
*/
UnsupportedAuthType: {
code: -120593,
description: 'Unsupported authentication type: {authType}'
},

/**
* Error thrown when WorkloadIdentity authentication requires `federatedtokenfile` or the deprecated `WIDAssertionFile` to be configured.
*/
WorkloadIdentityTokenFileRequired: {
code: -120594,
description: 'WorkloadIdentity authentication requires `federatedtokenfile` or the deprecated `WIDAssertionFile` to be configured'
},

/**
* Error thrown when ClientSecret authentication is specified but `clientSecret` is not configured.
*/
ClientSecretRequired: {
code: -120595,
description: 'ClientSecret authentication requires `clientSecret` to be configured'
},

/**
* Error thrown when Certificate authentication is specified but `certPemFile` or `certKeyFile` is not configured.
*/
CertificateFilesRequired: {
code: -120596,
description: 'Certificate authentication requires both `certPemFile` and `certKeyFile` to be configured'
},

/**
* Error thrown when FederatedCredentials authentication is specified but `FICClientId` is not configured.
*/
FICClientIdRequired: {
code: -120597,
description: 'FederatedCredentials authentication requires `FICClientId` to be configured'
},

/**
* Error thrown when UserManagedIdentity authentication is specified but `clientId` is not configured.
*/
ClientIdRequiredForUserManagedIdentity: {
code: -120598,
description: 'UserManagedIdentity authentication requires `clientId` to be configured'
},

// ============================================================================
// Agent and Client Errors (-120600 to -120630)
// ============================================================================
Expand Down
Loading
Loading