@microsoft/agents-hosting 1.6.0-beta.9.gce9d8facd2 → 1.7.0-beta.1.g8bcd4f11a7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +4 -9
- package/dist/src/agent-client/agentClient.js +2 -2
- package/dist/src/agent-client/agentClient.js.map +1 -1
- package/dist/src/agent-client/agentResponseHandler.js +2 -2
- package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
- package/dist/src/app/agentApplication.js +28 -2
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/agentApplicationOptions.d.ts +10 -0
- package/dist/src/app/attachmentDownloader.js +5 -10
- package/dist/src/app/attachmentDownloader.js.map +1 -1
- package/dist/src/app/auth/authorizationManager.d.ts +4 -0
- package/dist/src/app/auth/authorizationManager.js +94 -67
- package/dist/src/app/auth/authorizationManager.js.map +1 -1
- package/dist/src/app/auth/handlers/azureBotAuthorization.js +2 -2
- package/dist/src/app/auth/handlers/azureBotAuthorization.js.map +1 -1
- package/dist/src/app/proactive/createConversationOptionsBuilder.js +10 -6
- package/dist/src/app/proactive/createConversationOptionsBuilder.js.map +1 -1
- package/dist/src/app/proactive/proactive.d.ts +1 -0
- package/dist/src/app/proactive/proactive.js +30 -15
- package/dist/src/app/proactive/proactive.js.map +1 -1
- package/dist/src/app/streaming/streamingResponse.d.ts +11 -0
- package/dist/src/app/streaming/streamingResponse.js +21 -0
- package/dist/src/app/streaming/streamingResponse.js.map +1 -1
- package/dist/src/app/teamsAttachmentDownloader.js +6 -14
- package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
- package/dist/src/auth/MemoryCache.d.ts +2 -1
- package/dist/src/auth/MemoryCache.js +7 -1
- package/dist/src/auth/MemoryCache.js.map +1 -1
- package/dist/src/auth/authConfiguration.d.ts +2 -143
- package/dist/src/auth/authConfiguration.js +336 -257
- package/dist/src/auth/authConfiguration.js.map +1 -1
- package/dist/src/auth/authConstants.d.ts +7 -7
- package/dist/src/auth/authConstants.js.map +1 -1
- package/dist/src/auth/connectionManager.d.ts +92 -0
- package/dist/src/auth/{msalConnectionManager.js → connectionManager.js} +48 -33
- package/dist/src/auth/connectionManager.js.map +1 -0
- package/dist/src/auth/index.d.ts +5 -3
- package/dist/src/auth/index.js +5 -3
- package/dist/src/auth/index.js.map +1 -1
- package/dist/src/auth/jwt-middleware.d.ts +6 -0
- package/dist/src/auth/jwt-middleware.js +34 -2
- package/dist/src/auth/jwt-middleware.js.map +1 -1
- package/dist/src/auth/msal/msalConnectionManager.d.ts +17 -0
- package/dist/src/auth/msal/msalConnectionManager.js +50 -0
- package/dist/src/auth/msal/msalConnectionManager.js.map +1 -0
- package/dist/src/auth/msal/msalConnectionSettings.d.ts +68 -0
- package/dist/src/auth/msal/msalConnectionSettings.js +7 -0
- package/dist/src/auth/msal/msalConnectionSettings.js.map +1 -0
- package/dist/src/auth/{msalTokenCredential.d.ts → msal/msalTokenCredential.d.ts} +1 -1
- package/dist/src/auth/{msalTokenCredential.js → msal/msalTokenCredential.js} +2 -2
- package/dist/src/auth/msal/msalTokenCredential.js.map +1 -0
- package/dist/src/auth/{msalTokenProvider.d.ts → msal/msalTokenProvider.d.ts} +30 -3
- package/dist/src/auth/msal/msalTokenProvider.js +733 -0
- package/dist/src/auth/msal/msalTokenProvider.js.map +1 -0
- package/dist/src/auth/settings.d.ts +336 -0
- package/dist/src/auth/settings.js +202 -0
- package/dist/src/auth/settings.js.map +1 -0
- package/dist/src/auth/sidecar/sidecarAuthProvider.d.ts +74 -0
- package/dist/src/auth/sidecar/sidecarAuthProvider.js +181 -0
- package/dist/src/auth/sidecar/sidecarAuthProvider.js.map +1 -0
- package/dist/src/auth/sidecar/sidecarConnectionSettings.d.ts +65 -0
- package/dist/src/auth/sidecar/sidecarConnectionSettings.js +7 -0
- package/dist/src/auth/sidecar/sidecarConnectionSettings.js.map +1 -0
- package/dist/src/auth/sidecar/sidecarHttpClient.d.ts +95 -0
- package/dist/src/auth/sidecar/sidecarHttpClient.js +406 -0
- package/dist/src/auth/sidecar/sidecarHttpClient.js.map +1 -0
- package/dist/src/auth/sidecar/sidecarModels.d.ts +110 -0
- package/dist/src/auth/sidecar/sidecarModels.js +48 -0
- package/dist/src/auth/sidecar/sidecarModels.js.map +1 -0
- package/dist/src/auth/sidecar/sidecarTokenExpiry.d.ts +13 -0
- package/dist/src/auth/sidecar/sidecarTokenExpiry.js +39 -0
- package/dist/src/auth/sidecar/sidecarTokenExpiry.js.map +1 -0
- package/dist/src/cloudAdapter.d.ts +58 -1
- package/dist/src/cloudAdapter.js +233 -53
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/connector-client/connectorClient.d.ts +12 -8
- package/dist/src/connector-client/connectorClient.js +99 -82
- package/dist/src/connector-client/connectorClient.js.map +1 -1
- package/dist/src/errorHelper.js +181 -7
- package/dist/src/errorHelper.js.map +1 -1
- package/dist/src/getProductInfo.d.ts +23 -0
- package/dist/src/getProductInfo.js +71 -1
- package/dist/src/getProductInfo.js.map +1 -1
- package/dist/src/headerPropagation.d.ts +17 -2
- package/dist/src/headerPropagation.js +46 -24
- package/dist/src/headerPropagation.js.map +1 -1
- package/dist/src/httpClient.d.ts +60 -0
- package/dist/src/httpClient.js +173 -0
- package/dist/src/httpClient.js.map +1 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +8 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/oauth/userTokenClient.d.ts +17 -4
- package/dist/src/oauth/userTokenClient.js +119 -80
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/dist/src/observability/traces.d.ts +11 -4
- package/dist/src/observability/traces.js +39 -7
- package/dist/src/observability/traces.js.map +1 -1
- package/dist/src/utils/env.d.ts +50 -0
- package/dist/src/utils/env.js +113 -0
- package/dist/src/utils/env.js.map +1 -0
- package/dist/src/utils.d.ts +10 -0
- package/dist/src/utils.js +18 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +4 -9
- package/src/agent-client/agentClient.ts +2 -2
- package/src/agent-client/agentResponseHandler.ts +2 -2
- package/src/app/agentApplication.ts +30 -3
- package/src/app/agentApplicationOptions.ts +11 -0
- package/src/app/attachmentDownloader.ts +6 -8
- package/src/app/auth/authorizationManager.ts +84 -41
- package/src/app/auth/handlers/azureBotAuthorization.ts +2 -2
- package/src/app/proactive/createConversationOptionsBuilder.ts +8 -4
- package/src/app/proactive/proactive.ts +39 -23
- package/src/app/streaming/streamingResponse.ts +24 -1
- package/src/app/teamsAttachmentDownloader.ts +7 -11
- package/src/auth/MemoryCache.ts +7 -1
- package/src/auth/authConfiguration.ts +350 -356
- package/src/auth/authConstants.ts +7 -7
- package/src/auth/{msalConnectionManager.ts → connectionManager.ts} +65 -44
- package/src/auth/index.ts +5 -3
- package/src/auth/jwt-middleware.ts +32 -2
- package/src/auth/msal/msalConnectionManager.ts +55 -0
- package/src/auth/msal/msalConnectionSettings.ts +79 -0
- package/src/auth/{msalTokenCredential.ts → msal/msalTokenCredential.ts} +2 -1
- package/src/auth/msal/msalTokenProvider.ts +881 -0
- package/src/auth/settings.ts +378 -0
- package/src/auth/sidecar/sidecarAuthProvider.ts +234 -0
- package/src/auth/sidecar/sidecarConnectionSettings.ts +72 -0
- package/src/auth/sidecar/sidecarHttpClient.ts +443 -0
- package/src/auth/sidecar/sidecarModels.ts +153 -0
- package/src/auth/sidecar/sidecarTokenExpiry.ts +35 -0
- package/src/cloudAdapter.ts +292 -13
- package/src/connector-client/connectorClient.ts +116 -94
- package/src/errorHelper.ts +206 -7
- package/src/getProductInfo.ts +79 -1
- package/src/headerPropagation.ts +55 -23
- package/src/httpClient.ts +223 -0
- package/src/index.ts +6 -2
- package/src/oauth/userTokenClient.ts +137 -84
- package/src/observability/traces.ts +39 -8
- package/src/utils/env.ts +105 -0
- package/src/utils.ts +14 -0
- package/dist/src/auth/msalConnectionManager.d.ts +0 -64
- package/dist/src/auth/msalConnectionManager.js.map +0 -1
- package/dist/src/auth/msalTokenCredential.js.map +0 -1
- package/dist/src/auth/msalTokenProvider.js +0 -485
- package/dist/src/auth/msalTokenProvider.js.map +0 -1
- package/src/auth/msalTokenProvider.ts +0 -562
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AuthenticationResult, ConfidentialClientApplication, LogLevel, ManagedIdentityApplication, NodeSystemOptions } from '@azure/msal-node'
|
|
7
|
+
import { AuthConfiguration, AuthType, resolveAuthority, resolveAuthority as resolveAuthorityUtil, resolveAuthType } from '../authConfiguration'
|
|
8
|
+
import { AuthProvider } from '../authProvider'
|
|
9
|
+
import { debug, trace } from '@microsoft/agents-telemetry'
|
|
10
|
+
import { randomUUID } from 'crypto'
|
|
11
|
+
import { MemoryCache } from '../MemoryCache'
|
|
12
|
+
import jwt, { JwtPayload } from 'jsonwebtoken'
|
|
13
|
+
import fs from 'fs'
|
|
14
|
+
import crypto from 'crypto'
|
|
15
|
+
import { AuthenticationTraceDefinitions } from '../../observability'
|
|
16
|
+
import { ExceptionHelper } from '@microsoft/agents-activity'
|
|
17
|
+
import { Errors } from '../../errorHelper'
|
|
18
|
+
|
|
19
|
+
const audience = 'api://AzureADTokenExchange'
|
|
20
|
+
const logger = debug('agents:msal')
|
|
21
|
+
const agenticTokenRequestTimeoutMs = 30000
|
|
22
|
+
|
|
23
|
+
function isAbortError (error: unknown): error is Error {
|
|
24
|
+
return error instanceof Error && error.name === 'AbortError'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createTokenRequestTimeoutError (timeoutMs: number): Error {
|
|
28
|
+
return ExceptionHelper.generateException(Error, Errors.TokenRequestTimeout, undefined, { timeoutMs: timeoutMs.toString() })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Provides tokens using MSAL.
|
|
33
|
+
*/
|
|
34
|
+
export class MsalTokenProvider implements AuthProvider {
|
|
35
|
+
private static readonly _accessTokenCache = new MemoryCache<string>()
|
|
36
|
+
private static readonly _agenticTokenCache = new MemoryCache<string>()
|
|
37
|
+
private static readonly _confidentialClients = new Map<string, ConfidentialClientApplication>()
|
|
38
|
+
private static readonly _maxConfidentialClients = 100
|
|
39
|
+
public readonly connectionSettings?: AuthConfiguration
|
|
40
|
+
|
|
41
|
+
constructor (connectionSettings?: AuthConfiguration) {
|
|
42
|
+
this.connectionSettings = connectionSettings
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Clears process-wide auth caches.
|
|
47
|
+
*/
|
|
48
|
+
public static clearSharedCaches (): void {
|
|
49
|
+
MsalTokenProvider._accessTokenCache.clear()
|
|
50
|
+
MsalTokenProvider._agenticTokenCache.clear()
|
|
51
|
+
MsalTokenProvider._confidentialClients.clear()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private static cacheKey (...parts: Array<string | number | boolean | undefined | null>): string {
|
|
55
|
+
return JSON.stringify(parts.map(part => part ?? ''))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private static digest (value: string | Buffer | undefined): string {
|
|
59
|
+
return value ? crypto.createHash('sha256').update(value).digest('base64url') : ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private getOrCreateConfidentialClient (
|
|
63
|
+
cacheKey: string,
|
|
64
|
+
createClient: () => ConfidentialClientApplication
|
|
65
|
+
): ConfidentialClientApplication {
|
|
66
|
+
const existing = MsalTokenProvider._confidentialClients.get(cacheKey)
|
|
67
|
+
if (existing) {
|
|
68
|
+
MsalTokenProvider._confidentialClients.delete(cacheKey)
|
|
69
|
+
MsalTokenProvider._confidentialClients.set(cacheKey, existing)
|
|
70
|
+
return existing
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const created = createClient()
|
|
74
|
+
MsalTokenProvider._confidentialClients.set(cacheKey, created)
|
|
75
|
+
while (MsalTokenProvider._confidentialClients.size > MsalTokenProvider._maxConfidentialClients) {
|
|
76
|
+
const oldestKey = MsalTokenProvider._confidentialClients.keys().next().value
|
|
77
|
+
if (oldestKey === undefined) {
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
MsalTokenProvider._confidentialClients.delete(oldestKey)
|
|
81
|
+
}
|
|
82
|
+
return created
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private getFileCacheIdentity (path?: string): string {
|
|
86
|
+
if (!path) {
|
|
87
|
+
return ''
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const stat = fs.statSync(path)
|
|
92
|
+
return `${path}:${stat.size}:${stat.mtimeMs}`
|
|
93
|
+
} catch {
|
|
94
|
+
return path
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private getAccessTokenCacheKey (authConfig: AuthConfiguration, scope: string): string {
|
|
99
|
+
const authType = resolveAuthType(authConfig)
|
|
100
|
+
let authority = resolveAuthorityUtil(authConfig.authorityEndpoint ?? authConfig.authority, authConfig.tenantId)
|
|
101
|
+
let credentialIdentity = ''
|
|
102
|
+
|
|
103
|
+
switch (authType) {
|
|
104
|
+
case AuthType.ClientSecret:
|
|
105
|
+
credentialIdentity = MsalTokenProvider.digest(authConfig.clientSecret)
|
|
106
|
+
break
|
|
107
|
+
case AuthType.Certificate:
|
|
108
|
+
case AuthType.CertificateSubjectName:
|
|
109
|
+
credentialIdentity = MsalTokenProvider.cacheKey(
|
|
110
|
+
this.getFileCacheIdentity(authConfig.certPemFile),
|
|
111
|
+
this.getFileCacheIdentity(authConfig.certKeyFile),
|
|
112
|
+
authConfig.sendX5C
|
|
113
|
+
)
|
|
114
|
+
break
|
|
115
|
+
case AuthType.WorkloadIdentity:
|
|
116
|
+
authority = `https://login.microsoftonline.com/${authConfig.tenantId}`
|
|
117
|
+
credentialIdentity = this.getFileCacheIdentity(authConfig.federatedTokenFile ?? authConfig.WIDAssertionFile)
|
|
118
|
+
break
|
|
119
|
+
case AuthType.FederatedCredentials:
|
|
120
|
+
credentialIdentity = authConfig.federatedClientId ?? authConfig.FICClientId ?? ''
|
|
121
|
+
break
|
|
122
|
+
case AuthType.UserManagedIdentity:
|
|
123
|
+
authority = 'managed-identity'
|
|
124
|
+
credentialIdentity = authConfig.clientId ?? ''
|
|
125
|
+
break
|
|
126
|
+
case AuthType.SystemManagedIdentity:
|
|
127
|
+
authority = 'managed-identity'
|
|
128
|
+
credentialIdentity = 'system'
|
|
129
|
+
break
|
|
130
|
+
default:
|
|
131
|
+
credentialIdentity = 'unknown'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return MsalTokenProvider.cacheKey(
|
|
135
|
+
'access-token',
|
|
136
|
+
authType,
|
|
137
|
+
authority,
|
|
138
|
+
authConfig.clientId,
|
|
139
|
+
scope,
|
|
140
|
+
authConfig.azureRegion,
|
|
141
|
+
credentialIdentity
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private cacheAccessToken (cacheKey: string, token: string, expiresOn?: Date | null): void {
|
|
146
|
+
const ttlSeconds = this.getTokenCacheTtlSeconds(token, expiresOn)
|
|
147
|
+
if (ttlSeconds > 0) {
|
|
148
|
+
MsalTokenProvider._accessTokenCache.set(cacheKey, token, ttlSeconds)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private cacheAgenticToken (cacheKey: string, token: string, ttlSeconds: number): void {
|
|
153
|
+
const safeTtlSeconds = Math.floor(ttlSeconds) - 300
|
|
154
|
+
if (safeTtlSeconds > 0) {
|
|
155
|
+
MsalTokenProvider._agenticTokenCache.set(cacheKey, token, safeTtlSeconds)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private cacheAgenticAuthenticationResult (cacheKey: string, token: AuthenticationResult): void {
|
|
160
|
+
const ttlSeconds = this.getTokenCacheTtlSeconds(token.accessToken, token.expiresOn)
|
|
161
|
+
if (ttlSeconds > 0) {
|
|
162
|
+
MsalTokenProvider._agenticTokenCache.set(cacheKey, token.accessToken, ttlSeconds)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private getAgenticTokenCacheKey (
|
|
167
|
+
tenantId: string,
|
|
168
|
+
clientId: string,
|
|
169
|
+
scopes: string[],
|
|
170
|
+
tokenBodyParameters: { [key: string]: any }
|
|
171
|
+
): string {
|
|
172
|
+
const bodyKey = Object.keys(tokenBodyParameters)
|
|
173
|
+
.sort()
|
|
174
|
+
.filter(key => key !== 'user_federated_identity_credential')
|
|
175
|
+
.map(key => `${key}=${MsalTokenProvider.digest(String(tokenBodyParameters[key]))}`)
|
|
176
|
+
.join('&')
|
|
177
|
+
|
|
178
|
+
return MsalTokenProvider.cacheKey(
|
|
179
|
+
'agentic-token',
|
|
180
|
+
this.resolveAuthority(tenantId),
|
|
181
|
+
clientId,
|
|
182
|
+
scopes.join(' '),
|
|
183
|
+
bodyKey
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private getTokenCacheTtlSeconds (token: string, expiresOn?: Date | null): number {
|
|
188
|
+
const expiresAtMs = expiresOn?.getTime() ?? this.getJwtExpiresAtMs(token)
|
|
189
|
+
if (!expiresAtMs) {
|
|
190
|
+
return 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return Math.floor((expiresAtMs - Date.now()) / 1000) - 300
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private getJwtExpiresAtMs (token: string): number | undefined {
|
|
197
|
+
const payload = jwt.decode(token) as JwtPayload | string | null
|
|
198
|
+
if (!payload || typeof payload === 'string' || typeof payload.exp !== 'number') {
|
|
199
|
+
return undefined
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return payload.exp * 1000
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private getClientSecretClient (authConfig: AuthConfiguration): ConfidentialClientApplication {
|
|
206
|
+
const cacheKey = MsalTokenProvider.cacheKey(
|
|
207
|
+
'confidential-client',
|
|
208
|
+
AuthType.ClientSecret,
|
|
209
|
+
authConfig.clientId,
|
|
210
|
+
resolveAuthorityUtil(authConfig.authorityEndpoint ?? authConfig.authority, authConfig.tenantId),
|
|
211
|
+
MsalTokenProvider.digest(authConfig.clientSecret)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return this.getOrCreateConfidentialClient(cacheKey, () => new ConfidentialClientApplication({
|
|
215
|
+
auth: {
|
|
216
|
+
clientId: authConfig.clientId as string,
|
|
217
|
+
authority: resolveAuthorityUtil(authConfig.authorityEndpoint ?? authConfig.authority, authConfig.tenantId),
|
|
218
|
+
clientSecret: authConfig.clientSecret
|
|
219
|
+
},
|
|
220
|
+
system: this.sysOptions
|
|
221
|
+
}))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private getCertificateClient (authConfig: AuthConfiguration): ConfidentialClientApplication {
|
|
225
|
+
const cacheKey = MsalTokenProvider.cacheKey(
|
|
226
|
+
'confidential-client',
|
|
227
|
+
authConfig.authType ?? AuthType.Certificate,
|
|
228
|
+
authConfig.clientId,
|
|
229
|
+
resolveAuthorityUtil(authConfig.authorityEndpoint ?? authConfig.authority, authConfig.tenantId),
|
|
230
|
+
this.getFileCacheIdentity(authConfig.certPemFile),
|
|
231
|
+
this.getFileCacheIdentity(authConfig.certKeyFile),
|
|
232
|
+
authConfig.sendX5C
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return this.getOrCreateConfidentialClient(cacheKey, () => {
|
|
236
|
+
const privateKeySource = fs.readFileSync(authConfig.certKeyFile as string)
|
|
237
|
+
|
|
238
|
+
const privateKeyObject = crypto.createPrivateKey({
|
|
239
|
+
key: privateKeySource,
|
|
240
|
+
format: 'pem'
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const privateKey = privateKeyObject.export({
|
|
244
|
+
format: 'pem',
|
|
245
|
+
type: 'pkcs8'
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const pemFile = fs.readFileSync(authConfig.certPemFile as string)
|
|
249
|
+
const pubKeyObject = new crypto.X509Certificate(pemFile)
|
|
250
|
+
|
|
251
|
+
return new ConfidentialClientApplication({
|
|
252
|
+
auth: {
|
|
253
|
+
clientId: authConfig.clientId || '',
|
|
254
|
+
authority: resolveAuthority(authConfig.authorityEndpoint ?? authConfig.authority, authConfig.tenantId),
|
|
255
|
+
clientCertificate: {
|
|
256
|
+
privateKey: privateKey as string,
|
|
257
|
+
thumbprint: pubKeyObject.fingerprint.replaceAll(':', ''),
|
|
258
|
+
x5c: pemFile.toString()
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
system: this.sysOptions
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Gets an access token using the auth configuration from the MsalTokenProvider instance and the provided scope.
|
|
268
|
+
* @param scope The scope for the token.
|
|
269
|
+
* @returns A promise that resolves to the access token.
|
|
270
|
+
*/
|
|
271
|
+
public async getAccessToken (scope: string): Promise<string>
|
|
272
|
+
/**
|
|
273
|
+
* Gets an access token.
|
|
274
|
+
* @param authConfig The authentication configuration.
|
|
275
|
+
* @param scope The scope for the token.
|
|
276
|
+
* @returns A promise that resolves to the access token.
|
|
277
|
+
*/
|
|
278
|
+
public async getAccessToken (authConfig: AuthConfiguration, scope: string): Promise<string>
|
|
279
|
+
public async getAccessToken (authConfigOrScope: AuthConfiguration | string, scope?: string): Promise<string> {
|
|
280
|
+
return trace(AuthenticationTraceDefinitions.getAccessToken, async ({ record }) => {
|
|
281
|
+
let authConfig: AuthConfiguration
|
|
282
|
+
let actualScope: string
|
|
283
|
+
|
|
284
|
+
if (typeof authConfigOrScope === 'string') {
|
|
285
|
+
// Called as getAccessToken(scope)
|
|
286
|
+
if (!this.connectionSettings) {
|
|
287
|
+
throw new Error('Connection settings must be provided to constructor when calling getAccessToken(scope)')
|
|
288
|
+
}
|
|
289
|
+
authConfig = this.connectionSettings
|
|
290
|
+
actualScope = authConfigOrScope
|
|
291
|
+
} else {
|
|
292
|
+
// Called as getAccessToken(authConfig, scope)
|
|
293
|
+
authConfig = authConfigOrScope
|
|
294
|
+
actualScope = scope as string
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
record({ scope: actualScope })
|
|
298
|
+
|
|
299
|
+
if (!authConfig.clientId && process.env.NODE_ENV !== 'production') {
|
|
300
|
+
record({ method: 'unknown' })
|
|
301
|
+
return ''
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const accessTokenCacheKey = this.getAccessTokenCacheKey(authConfig, actualScope)
|
|
305
|
+
const cachedAccessToken = MsalTokenProvider._accessTokenCache.get(accessTokenCacheKey)
|
|
306
|
+
if (cachedAccessToken) {
|
|
307
|
+
logger.debug('getAccessToken cache hit clientId=%s scope=%s', authConfig.clientId, actualScope)
|
|
308
|
+
return cachedAccessToken
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let token
|
|
312
|
+
const authType = resolveAuthType(authConfig)
|
|
313
|
+
record({ method: authType })
|
|
314
|
+
logger.debug('getAccessToken via method=%s clientId=%s scope=%s', authType, authConfig.clientId, actualScope)
|
|
315
|
+
|
|
316
|
+
switch (authType) {
|
|
317
|
+
case AuthType.WorkloadIdentity: {
|
|
318
|
+
const tokenFilePath = authConfig.federatedTokenFile ?? authConfig.WIDAssertionFile
|
|
319
|
+
if (!tokenFilePath) {
|
|
320
|
+
throw ExceptionHelper.generateException(Error, Errors.WorkloadIdentityTokenFileRequired)
|
|
321
|
+
}
|
|
322
|
+
token = await this.acquireAccessTokenViaWID(authConfig, actualScope)
|
|
323
|
+
break
|
|
324
|
+
}
|
|
325
|
+
case AuthType.FederatedCredentials:
|
|
326
|
+
if (!authConfig.federatedClientId && !authConfig.FICClientId) {
|
|
327
|
+
throw ExceptionHelper.generateException(Error, Errors.FICClientIdRequired)
|
|
328
|
+
}
|
|
329
|
+
token = await this.acquireAccessTokenViaFIC(authConfig, actualScope)
|
|
330
|
+
break
|
|
331
|
+
case AuthType.ClientSecret:
|
|
332
|
+
if (!authConfig.clientSecret) {
|
|
333
|
+
throw ExceptionHelper.generateException(Error, Errors.ClientSecretRequired)
|
|
334
|
+
}
|
|
335
|
+
token = await this.acquireAccessTokenViaSecret(authConfig, actualScope)
|
|
336
|
+
break
|
|
337
|
+
case AuthType.Certificate:
|
|
338
|
+
case AuthType.CertificateSubjectName:
|
|
339
|
+
if (!authConfig.certPemFile || !authConfig.certKeyFile) {
|
|
340
|
+
throw ExceptionHelper.generateException(Error, Errors.CertificateFilesRequired)
|
|
341
|
+
}
|
|
342
|
+
token = await this.acquireTokenWithCertificate(authConfig, actualScope)
|
|
343
|
+
break
|
|
344
|
+
case AuthType.UserManagedIdentity:
|
|
345
|
+
if (!authConfig.clientId) {
|
|
346
|
+
throw ExceptionHelper.generateException(Error, Errors.ClientIdRequiredForUserManagedIdentity)
|
|
347
|
+
}
|
|
348
|
+
token = await this.acquireTokenWithUserAssignedIdentity(authConfig, actualScope)
|
|
349
|
+
break
|
|
350
|
+
case AuthType.SystemManagedIdentity:
|
|
351
|
+
token = await this.acquireTokenWithSystemAssignedIdentity(authConfig, actualScope)
|
|
352
|
+
break
|
|
353
|
+
default:
|
|
354
|
+
throw ExceptionHelper.generateException(Error, Errors.UnsupportedAuthType, undefined, { authType })
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (token === undefined) {
|
|
358
|
+
throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireToken)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.cacheAccessToken(accessTokenCacheKey, token)
|
|
362
|
+
return token
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
public async acquireTokenOnBehalfOf (scopes: string[], oboAssertion: string): Promise<string>
|
|
367
|
+
public async acquireTokenOnBehalfOf (authConfig: AuthConfiguration, scopes: string[], oboAssertion: string): Promise<string>
|
|
368
|
+
public async acquireTokenOnBehalfOf (
|
|
369
|
+
authConfigOrScopes: AuthConfiguration | string[],
|
|
370
|
+
scopesOrOboAssertion?: string[] | string,
|
|
371
|
+
oboAssertion?: string
|
|
372
|
+
): Promise<string> {
|
|
373
|
+
return trace(AuthenticationTraceDefinitions.acquireTokenOnBehalfOf, async ({ record }) => {
|
|
374
|
+
let authConfig: AuthConfiguration
|
|
375
|
+
let actualScopes: string[]
|
|
376
|
+
let actualOboAssertion: string
|
|
377
|
+
|
|
378
|
+
if (Array.isArray(authConfigOrScopes)) {
|
|
379
|
+
// Called as acquireTokenOnBehalfOf(scopes, oboAssertion)
|
|
380
|
+
if (!this.connectionSettings) {
|
|
381
|
+
throw new Error('Connection settings must be provided to constructor when calling acquireTokenOnBehalfOf(scopes, oboAssertion)')
|
|
382
|
+
}
|
|
383
|
+
authConfig = this.connectionSettings
|
|
384
|
+
actualScopes = authConfigOrScopes
|
|
385
|
+
actualOboAssertion = scopesOrOboAssertion as string
|
|
386
|
+
} else {
|
|
387
|
+
// Called as acquireTokenOnBehalfOf(authConfig, scopes, oboAssertion)
|
|
388
|
+
authConfig = authConfigOrScopes
|
|
389
|
+
actualScopes = scopesOrOboAssertion as string[]
|
|
390
|
+
actualOboAssertion = oboAssertion!
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
record({ scopes: actualScopes })
|
|
394
|
+
logger.debug('acquireTokenOnBehalfOf clientId=%s scopes=%o', authConfig.clientId, actualScopes)
|
|
395
|
+
|
|
396
|
+
const cca = this.getClientSecretClient(authConfig)
|
|
397
|
+
const token = await cca.acquireTokenOnBehalfOf({
|
|
398
|
+
oboAssertion: actualOboAssertion,
|
|
399
|
+
scopes: actualScopes
|
|
400
|
+
})
|
|
401
|
+
if (!token?.accessToken) {
|
|
402
|
+
throw new Error('Failed to acquire token on behalf of user')
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return token.accessToken
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
public async getAgenticInstanceToken (tenantId: string, agentAppInstanceId: string): Promise<string> {
|
|
410
|
+
return trace(AuthenticationTraceDefinitions.getAgenticInstanceToken, async ({ record }) => {
|
|
411
|
+
logger.debug('getAgenticInstanceToken tenantId=%s agentAppInstanceId=%s', tenantId, agentAppInstanceId)
|
|
412
|
+
record({ agenticInstanceId: agentAppInstanceId })
|
|
413
|
+
|
|
414
|
+
if (!this.connectionSettings) {
|
|
415
|
+
throw new Error('Connection settings must be provided when calling getAgenticInstanceToken')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const instanceTokenCacheKey = MsalTokenProvider.cacheKey(
|
|
419
|
+
'agentic-instance-token',
|
|
420
|
+
this.resolveAuthority(tenantId),
|
|
421
|
+
agentAppInstanceId,
|
|
422
|
+
this.connectionSettings.clientId,
|
|
423
|
+
this.connectionSettings.azureRegion
|
|
424
|
+
)
|
|
425
|
+
const cachedInstanceToken = MsalTokenProvider._agenticTokenCache.get(instanceTokenCacheKey)
|
|
426
|
+
if (cachedInstanceToken) {
|
|
427
|
+
return cachedInstanceToken
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const appToken = await this.getAgenticApplicationToken(tenantId, agentAppInstanceId)
|
|
431
|
+
const cca = new ConfidentialClientApplication({
|
|
432
|
+
auth: {
|
|
433
|
+
clientId: agentAppInstanceId,
|
|
434
|
+
clientAssertion: appToken,
|
|
435
|
+
authority: this.resolveAuthority(tenantId),
|
|
436
|
+
},
|
|
437
|
+
system: this.sysOptions
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
const token = await cca.acquireTokenByClientCredential({
|
|
441
|
+
scopes: ['api://AzureAdTokenExchange/.default'],
|
|
442
|
+
correlationId: randomUUID(),
|
|
443
|
+
azureRegion: this.connectionSettings?.azureRegion
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
if (!token?.accessToken) {
|
|
447
|
+
throw new Error(`Failed to acquire instance token for agent instance: ${agentAppInstanceId}`)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
this.cacheAgenticAuthenticationResult(instanceTokenCacheKey, token)
|
|
451
|
+
return token.accessToken
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* This method can optionally accept a tenant ID that overrides the tenant ID in the connection settings.
|
|
457
|
+
* The passed tenantId is always preferred over the configured tenantId when present.
|
|
458
|
+
* @param tenantId
|
|
459
|
+
* @returns
|
|
460
|
+
*/
|
|
461
|
+
private resolveAuthority (tenantId?: string) : string {
|
|
462
|
+
const { authorityEndpoint: configuredAuth, authority, tenantId: configuredTenantId } = this.connectionSettings ?? {}
|
|
463
|
+
|
|
464
|
+
if (!tenantId) {
|
|
465
|
+
// No agentic tenant override — delegate to shared utility
|
|
466
|
+
return resolveAuthorityUtil(configuredAuth ?? authority, configuredTenantId)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Agentic override: build a clean base using the override tenant, then replace any
|
|
470
|
+
// /common or GUID placeholder left in the authority (e.g. from a multi-tenant config)
|
|
471
|
+
const base = resolveAuthorityUtil(configuredAuth ?? authority, tenantId)
|
|
472
|
+
const guidPattern = /\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
|
|
473
|
+
|
|
474
|
+
if (base.endsWith('/common') || guidPattern.test(base)) {
|
|
475
|
+
return base.replace(
|
|
476
|
+
/\/(?:common|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(?=\/|$)/,
|
|
477
|
+
`/${tenantId}`
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return base
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Does a direct HTTP call to acquire a token for agentic scenarios - do not use this directly!
|
|
486
|
+
* This method will be removed once MSAL is updated with the necessary features.
|
|
487
|
+
* (This is required in order to pass additional parameters into the auth call)
|
|
488
|
+
* @param tenantId
|
|
489
|
+
* @param clientId
|
|
490
|
+
* @param clientAssertion
|
|
491
|
+
* @param scopes
|
|
492
|
+
* @param tokenBodyParameters
|
|
493
|
+
* @returns
|
|
494
|
+
*/
|
|
495
|
+
private async acquireTokenForAgenticScenarios (tenantId: string, clientId: string, clientAssertion: string | undefined, scopes: string[], tokenBodyParameters: { [key: string]: any }): Promise<string | null> {
|
|
496
|
+
if (!this.connectionSettings) {
|
|
497
|
+
throw new Error('Connection settings must be provided when calling getAgenticInstanceToken')
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
logger.debug('acquireTokenForAgenticScenarios clientId=%s tenantId=%s scopes=%o grant_type=%s', clientId, tenantId, scopes, tokenBodyParameters.grant_type)
|
|
501
|
+
// Check cache first
|
|
502
|
+
const cacheKey = this.getAgenticTokenCacheKey(tenantId, clientId, scopes, tokenBodyParameters)
|
|
503
|
+
if (MsalTokenProvider._agenticTokenCache.get(cacheKey)) {
|
|
504
|
+
return MsalTokenProvider._agenticTokenCache.get(cacheKey) as string
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const url = `${this.resolveAuthority(tenantId)}/oauth2/v2.0/token`
|
|
508
|
+
|
|
509
|
+
const data: { [key: string]: any } = {
|
|
510
|
+
client_id: clientId,
|
|
511
|
+
scope: scopes.join(' '),
|
|
512
|
+
...tokenBodyParameters
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (clientAssertion) {
|
|
516
|
+
data.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
|
|
517
|
+
data.client_assertion = clientAssertion
|
|
518
|
+
} else {
|
|
519
|
+
data.client_secret = this.connectionSettings.clientSecret
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (data.grant_type !== 'user_fic') {
|
|
523
|
+
data.client_info = '2'
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const controller = new AbortController()
|
|
527
|
+
const timeoutId = setTimeout(() => {
|
|
528
|
+
controller.abort(createTokenRequestTimeoutError(agenticTokenRequestTimeoutMs))
|
|
529
|
+
}, agenticTokenRequestTimeoutMs)
|
|
530
|
+
|
|
531
|
+
const token = await fetch(
|
|
532
|
+
url,
|
|
533
|
+
{
|
|
534
|
+
method: 'POST',
|
|
535
|
+
headers: {
|
|
536
|
+
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
|
|
537
|
+
},
|
|
538
|
+
body: new URLSearchParams(data as Record<string, string>).toString(),
|
|
539
|
+
signal: controller.signal
|
|
540
|
+
}
|
|
541
|
+
).then(async (response) => {
|
|
542
|
+
if (!response.ok) {
|
|
543
|
+
const errorBody = await response.text()
|
|
544
|
+
const error = new Error(`Token request failed with status ${response.status}: ${errorBody}`)
|
|
545
|
+
;(error as any).toJSON = () => ({ status: response.status, body: errorBody })
|
|
546
|
+
throw error
|
|
547
|
+
}
|
|
548
|
+
return response.json() as Promise<{ access_token: string, expires_in: number }>
|
|
549
|
+
}).catch((error) => {
|
|
550
|
+
const resolvedError = isAbortError(error)
|
|
551
|
+
? (controller.signal.reason instanceof Error ? controller.signal.reason : createTokenRequestTimeoutError(agenticTokenRequestTimeoutMs))
|
|
552
|
+
: error
|
|
553
|
+
logger.error('Error acquiring token: ', resolvedError.toJSON ? resolvedError.toJSON() : resolvedError)
|
|
554
|
+
throw resolvedError
|
|
555
|
+
}).finally(() => {
|
|
556
|
+
clearTimeout(timeoutId)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
// capture token, expire local cache 5 minutes early
|
|
560
|
+
this.cacheAgenticToken(cacheKey, token.access_token, token.expires_in)
|
|
561
|
+
return token.access_token
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
public async getAgenticUserToken (tenantId: string, agentAppInstanceId: string, agenticUserId: string, scopes: string[]): Promise<string> {
|
|
565
|
+
return trace(AuthenticationTraceDefinitions.getAgenticUserToken, async ({ record }) => {
|
|
566
|
+
logger.debug('getAgenticUserToken tenantId=%s agentAppInstanceId=%s scopes=%o', tenantId, agentAppInstanceId, scopes)
|
|
567
|
+
record({ agenticInstanceId: agentAppInstanceId, agenticUserId, scopes })
|
|
568
|
+
|
|
569
|
+
const userTokenParameters = {
|
|
570
|
+
user_id: agenticUserId,
|
|
571
|
+
grant_type: 'user_fic',
|
|
572
|
+
}
|
|
573
|
+
const userTokenCacheKey = this.getAgenticTokenCacheKey(tenantId, agentAppInstanceId, scopes, userTokenParameters)
|
|
574
|
+
const cachedUserToken = MsalTokenProvider._agenticTokenCache.get(userTokenCacheKey)
|
|
575
|
+
if (cachedUserToken) {
|
|
576
|
+
return cachedUserToken
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const agentToken = await this.getAgenticApplicationToken(tenantId, agentAppInstanceId)
|
|
580
|
+
const instanceToken = await this.getAgenticInstanceToken(tenantId, agentAppInstanceId)
|
|
581
|
+
|
|
582
|
+
const token = await this.acquireTokenForAgenticScenarios(tenantId, agentAppInstanceId, agentToken, scopes, {
|
|
583
|
+
...userTokenParameters,
|
|
584
|
+
user_federated_identity_credential: instanceToken,
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
if (!token) {
|
|
588
|
+
throw new Error(`Failed to acquire instance token for user token: ${agentAppInstanceId}`)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return token
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
public async getAgenticApplicationToken (tenantId: string, agentAppInstanceId: string): Promise<string> {
|
|
596
|
+
if (!this.connectionSettings?.clientId) {
|
|
597
|
+
throw new Error('Connection settings must be provided when calling getAgenticApplicationToken')
|
|
598
|
+
}
|
|
599
|
+
logger.debug('getAgenticApplicationToken clientId=%s tenantId=%s agentAppInstanceId=%s', this.connectionSettings.clientId, tenantId, agentAppInstanceId)
|
|
600
|
+
|
|
601
|
+
const authType = resolveAuthType(this.connectionSettings)
|
|
602
|
+
|
|
603
|
+
if (authType === AuthType.IdentityProxyManager) {
|
|
604
|
+
let resource: string
|
|
605
|
+
if (!this.connectionSettings.idpmResource) {
|
|
606
|
+
resource = 'api://AzureAdTokenExchange/.default'
|
|
607
|
+
} else if (!URL.canParse(this.connectionSettings.idpmResource)) {
|
|
608
|
+
throw new Error('idpmResource must be a valid absolute URI')
|
|
609
|
+
} else {
|
|
610
|
+
resource = this.connectionSettings.idpmResource
|
|
611
|
+
}
|
|
612
|
+
const msiApp = new ManagedIdentityApplication({
|
|
613
|
+
managedIdentityIdParams: {
|
|
614
|
+
userAssignedClientId: this.connectionSettings.clientId
|
|
615
|
+
},
|
|
616
|
+
system: this.sysOptions
|
|
617
|
+
})
|
|
618
|
+
const tokenResult = await msiApp.acquireToken({ resource })
|
|
619
|
+
if (!tokenResult?.accessToken) {
|
|
620
|
+
throw new Error(`Failed to acquire token via IdentityProxyManager for agent instance: ${agentAppInstanceId}`)
|
|
621
|
+
}
|
|
622
|
+
logger.debug('getAgenticApplicationToken via IdentityProxyManager clientId=%s resource=%s', this.connectionSettings.clientId, resource)
|
|
623
|
+
return tokenResult.accessToken
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let clientAssertion
|
|
627
|
+
|
|
628
|
+
switch (authType) {
|
|
629
|
+
case AuthType.WorkloadIdentity: {
|
|
630
|
+
const tokenFilePath = this.connectionSettings.federatedTokenFile ?? this.connectionSettings.WIDAssertionFile
|
|
631
|
+
if (tokenFilePath === undefined) {
|
|
632
|
+
throw ExceptionHelper.generateException(Error, Errors.WorkloadIdentityTokenFileRequired)
|
|
633
|
+
}
|
|
634
|
+
clientAssertion = fs.readFileSync(tokenFilePath as string, 'utf8')
|
|
635
|
+
break
|
|
636
|
+
}
|
|
637
|
+
case AuthType.FederatedCredentials:
|
|
638
|
+
if (!this.connectionSettings.federatedClientId && !this.connectionSettings.FICClientId) {
|
|
639
|
+
throw ExceptionHelper.generateException(Error, Errors.FICClientIdRequired)
|
|
640
|
+
}
|
|
641
|
+
clientAssertion = await this.fetchExternalToken(this.connectionSettings.federatedClientId as string || this.connectionSettings.FICClientId as string)
|
|
642
|
+
break
|
|
643
|
+
case AuthType.Certificate:
|
|
644
|
+
case AuthType.CertificateSubjectName:
|
|
645
|
+
if (!this.connectionSettings.certPemFile || !this.connectionSettings.certKeyFile) {
|
|
646
|
+
throw ExceptionHelper.generateException(Error, Errors.CertificateFilesRequired)
|
|
647
|
+
}
|
|
648
|
+
clientAssertion = this.getAssertionFromCert(this.connectionSettings)
|
|
649
|
+
break
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const token = await this.acquireTokenForAgenticScenarios(tenantId, this.connectionSettings.clientId, clientAssertion, ['api://AzureAdTokenExchange/.default'], {
|
|
653
|
+
grant_type: 'client_credentials',
|
|
654
|
+
fmi_path: agentAppInstanceId,
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
if (!token) {
|
|
658
|
+
throw new Error(`Failed to acquire token for agent instance: ${agentAppInstanceId}`)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return token
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private readonly sysOptions: NodeSystemOptions = {
|
|
665
|
+
loggerOptions: {
|
|
666
|
+
logLevel: LogLevel.Trace,
|
|
667
|
+
loggerCallback: (level, message, containsPii) => {
|
|
668
|
+
if (containsPii) {
|
|
669
|
+
return
|
|
670
|
+
}
|
|
671
|
+
switch (level) {
|
|
672
|
+
case LogLevel.Error:
|
|
673
|
+
logger.error(message)
|
|
674
|
+
return
|
|
675
|
+
case LogLevel.Info:
|
|
676
|
+
logger.debug(message)
|
|
677
|
+
return
|
|
678
|
+
case LogLevel.Warning:
|
|
679
|
+
if (!message.includes('Warning - No client info in response')) {
|
|
680
|
+
logger.warn(message)
|
|
681
|
+
}
|
|
682
|
+
return
|
|
683
|
+
case LogLevel.Verbose:
|
|
684
|
+
logger.debug(message)
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
piiLoggingEnabled: false
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Generates the client assertion using the provided certificate.
|
|
693
|
+
* @param authConfig The authentication configuration.
|
|
694
|
+
* @returns The client assertion.
|
|
695
|
+
*/
|
|
696
|
+
private getAssertionFromCert (authConfig: AuthConfiguration): string {
|
|
697
|
+
const base64url = (buf: Buffer) =>
|
|
698
|
+
buf.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
|
|
699
|
+
|
|
700
|
+
const privateKeyPem = fs.readFileSync(authConfig.certKeyFile as string)
|
|
701
|
+
|
|
702
|
+
const pemFile = fs.readFileSync(authConfig.certPemFile as string)
|
|
703
|
+
const pubKeyObject = new crypto.X509Certificate(pemFile)
|
|
704
|
+
|
|
705
|
+
const der = pubKeyObject.raw
|
|
706
|
+
const x5tS256 = base64url(crypto.createHash('sha256').update(der).digest())
|
|
707
|
+
|
|
708
|
+
let x5c
|
|
709
|
+
if (authConfig.sendX5C) {
|
|
710
|
+
x5c = pemFile.toString()
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const now = Math.floor(Date.now() / 1000)
|
|
714
|
+
const payload = {
|
|
715
|
+
aud: `${this.resolveAuthority(authConfig.tenantId)}/oauth2/v2.0/token`,
|
|
716
|
+
iss: authConfig.clientId,
|
|
717
|
+
sub: authConfig.clientId,
|
|
718
|
+
jti: randomUUID(),
|
|
719
|
+
nbf: now,
|
|
720
|
+
iat: now,
|
|
721
|
+
exp: now + 600, // 10 minutes
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return jwt.sign(
|
|
725
|
+
payload,
|
|
726
|
+
privateKeyPem,
|
|
727
|
+
{
|
|
728
|
+
algorithm: 'PS256',
|
|
729
|
+
header: { alg: 'PS256', typ: 'JWT', 'x5t#S256': x5tS256, x5c }
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Acquires a token using a user-assigned identity.
|
|
736
|
+
* @param authConfig The authentication configuration.
|
|
737
|
+
* @param scope The scope for the token.
|
|
738
|
+
* @returns A promise that resolves to the access token.
|
|
739
|
+
*/
|
|
740
|
+
private async acquireTokenWithUserAssignedIdentity (authConfig: AuthConfiguration, scope: string) {
|
|
741
|
+
const mia = new ManagedIdentityApplication({
|
|
742
|
+
managedIdentityIdParams: {
|
|
743
|
+
userAssignedClientId: authConfig.clientId || ''
|
|
744
|
+
},
|
|
745
|
+
system: this.sysOptions
|
|
746
|
+
})
|
|
747
|
+
const token = await mia.acquireToken({
|
|
748
|
+
resource: scope
|
|
749
|
+
})
|
|
750
|
+
return token?.accessToken
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Acquires a token using a system-assigned identity.
|
|
755
|
+
* @param authConfig The authentication configuration.
|
|
756
|
+
* @param scope The scope for the token.
|
|
757
|
+
* @returns A promise that resolves to the access token.
|
|
758
|
+
*/
|
|
759
|
+
private async acquireTokenWithSystemAssignedIdentity (authConfig: AuthConfiguration, scope: string) {
|
|
760
|
+
const mia = new ManagedIdentityApplication({
|
|
761
|
+
system: this.sysOptions
|
|
762
|
+
})
|
|
763
|
+
const token = await mia.acquireToken({
|
|
764
|
+
resource: scope
|
|
765
|
+
})
|
|
766
|
+
return token?.accessToken
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Acquires a token using a certificate.
|
|
771
|
+
* @param authConfig The authentication configuration.
|
|
772
|
+
* @param scope The scope for the token.
|
|
773
|
+
* @returns A promise that resolves to the access token.
|
|
774
|
+
*/
|
|
775
|
+
private async acquireTokenWithCertificate (authConfig: AuthConfiguration, scope: string) {
|
|
776
|
+
const cca = this.getCertificateClient(authConfig)
|
|
777
|
+
const token = await cca.acquireTokenByClientCredential({
|
|
778
|
+
scopes: [`${scope}/.default`],
|
|
779
|
+
correlationId: randomUUID(),
|
|
780
|
+
azureRegion: authConfig.azureRegion
|
|
781
|
+
})
|
|
782
|
+
if (!token?.accessToken) {
|
|
783
|
+
throw new Error('Failed to acquire token using certificate')
|
|
784
|
+
}
|
|
785
|
+
return token.accessToken
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Acquires a token using a client secret.
|
|
790
|
+
* @param authConfig The authentication configuration.
|
|
791
|
+
* @param scope The scope for the token.
|
|
792
|
+
* @returns A promise that resolves to the access token.
|
|
793
|
+
*/
|
|
794
|
+
private async acquireAccessTokenViaSecret (authConfig: AuthConfiguration, scope: string) {
|
|
795
|
+
const cca = this.getClientSecretClient(authConfig)
|
|
796
|
+
const token = await cca.acquireTokenByClientCredential({
|
|
797
|
+
scopes: [`${scope}/.default`],
|
|
798
|
+
correlationId: randomUUID(),
|
|
799
|
+
azureRegion: authConfig.azureRegion
|
|
800
|
+
})
|
|
801
|
+
if (!token?.accessToken) {
|
|
802
|
+
throw new Error('Failed to acquire token using client secret')
|
|
803
|
+
}
|
|
804
|
+
return token.accessToken
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Acquires a token using a FIC client assertion.
|
|
809
|
+
* @param authConfig The authentication configuration.
|
|
810
|
+
* @param scope The scope for the token.
|
|
811
|
+
* @returns A promise that resolves to the access token.
|
|
812
|
+
*/
|
|
813
|
+
private async acquireAccessTokenViaFIC (authConfig: AuthConfiguration, scope: string) : Promise<string> {
|
|
814
|
+
const scopes = [`${scope}/.default`]
|
|
815
|
+
const clientAssertion = await this.fetchExternalToken(authConfig.federatedClientId as string || authConfig.FICClientId as string)
|
|
816
|
+
const cca = new ConfidentialClientApplication({
|
|
817
|
+
auth: {
|
|
818
|
+
clientId: authConfig.clientId as string,
|
|
819
|
+
authority: `${authConfig.authorityEndpoint ?? authConfig.authority}/${authConfig.tenantId}`,
|
|
820
|
+
clientAssertion
|
|
821
|
+
},
|
|
822
|
+
system: this.sysOptions
|
|
823
|
+
})
|
|
824
|
+
const token = await cca.acquireTokenByClientCredential({ scopes, azureRegion: authConfig.azureRegion })
|
|
825
|
+
logger.debug('got token using FIC client assertion')
|
|
826
|
+
if (!token?.accessToken) {
|
|
827
|
+
throw new Error('Failed to acquire token using FIC client assertion')
|
|
828
|
+
}
|
|
829
|
+
return token.accessToken
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Acquires a token using a Workload Identity client assertion.
|
|
834
|
+
* @param authConfig The authentication configuration.
|
|
835
|
+
* @param scope The scope for the token.
|
|
836
|
+
* @returns A promise that resolves to the access token.
|
|
837
|
+
*/
|
|
838
|
+
private async acquireAccessTokenViaWID (authConfig: AuthConfiguration, scope: string) : Promise<string> {
|
|
839
|
+
const scopes = [`${scope}/.default`]
|
|
840
|
+
const tokenFilePath = authConfig.federatedTokenFile ?? authConfig.WIDAssertionFile
|
|
841
|
+
const clientAssertion = fs.readFileSync(tokenFilePath as string, 'utf8')
|
|
842
|
+
const cca = new ConfidentialClientApplication({
|
|
843
|
+
auth: {
|
|
844
|
+
clientId: authConfig.clientId as string,
|
|
845
|
+
authority: `https://login.microsoftonline.com/${authConfig.tenantId}`,
|
|
846
|
+
clientAssertion
|
|
847
|
+
},
|
|
848
|
+
system: this.sysOptions
|
|
849
|
+
})
|
|
850
|
+
const token = await cca.acquireTokenByClientCredential({ scopes, azureRegion: authConfig.azureRegion })
|
|
851
|
+
logger.debug('got token using WID client assertion')
|
|
852
|
+
if (!token?.accessToken) {
|
|
853
|
+
throw new Error('Failed to acquire token using WID client assertion')
|
|
854
|
+
}
|
|
855
|
+
return token.accessToken
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Fetches an external token.
|
|
860
|
+
* @param FICClientId The FIC client ID.
|
|
861
|
+
* @returns A promise that resolves to the external token.
|
|
862
|
+
*/
|
|
863
|
+
private async fetchExternalToken (FICClientId: string) : Promise<string> {
|
|
864
|
+
const managedIdentityClientAssertion = new ManagedIdentityApplication({
|
|
865
|
+
managedIdentityIdParams: {
|
|
866
|
+
userAssignedClientId: FICClientId
|
|
867
|
+
},
|
|
868
|
+
system: this.sysOptions
|
|
869
|
+
}
|
|
870
|
+
)
|
|
871
|
+
const response = await managedIdentityClientAssertion.acquireToken({
|
|
872
|
+
resource: audience,
|
|
873
|
+
forceRefresh: true
|
|
874
|
+
})
|
|
875
|
+
logger.debug('got token for FIC')
|
|
876
|
+
if (!response?.accessToken) {
|
|
877
|
+
throw new Error('Failed to acquire external token for FIC client assertion')
|
|
878
|
+
}
|
|
879
|
+
return response.accessToken
|
|
880
|
+
}
|
|
881
|
+
}
|