@microsoft/agents-hosting 1.7.1 → 1.8.0-beta.34.g5d543f96c0
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/README.md +64 -2
- package/dist/package.json +3 -3
- package/dist/src/agent-client/agentClient.d.ts +18 -6
- package/dist/src/agent-client/agentClient.js +34 -18
- package/dist/src/agent-client/agentClient.js.map +1 -1
- package/dist/src/agent-client/agentResponseHandler.d.ts +14 -9
- package/dist/src/agent-client/agentResponseHandler.js +10 -5
- package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
- package/dist/src/agent-client/createAgentResponseHandler.d.ts +7 -2
- package/dist/src/agent-client/createAgentResponseHandler.js +55 -8
- package/dist/src/agent-client/createAgentResponseHandler.js.map +1 -1
- package/dist/src/app/adaptiveCards/activityValueParsers.d.ts +1 -4
- package/dist/src/app/adaptiveCards/activityValueParsers.js +2 -6
- package/dist/src/app/adaptiveCards/activityValueParsers.js.map +1 -1
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.d.ts +18 -0
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +53 -23
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
- package/dist/src/app/agentApplication.d.ts +7 -0
- package/dist/src/app/agentApplication.js +130 -70
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/attachmentDownloader.d.ts +4 -1
- package/dist/src/app/attachmentDownloader.js +27 -5
- package/dist/src/app/attachmentDownloader.js.map +1 -1
- package/dist/src/app/auth/handlers/agenticAuthorization.d.ts +1 -1
- package/dist/src/app/auth/handlers/azureBotAuthorization.d.ts +1 -1
- package/dist/src/app/auth/types.d.ts +3 -4
- package/dist/src/app/index.d.ts +2 -0
- package/dist/src/app/index.js.map +1 -1
- package/dist/src/app/streaming/streamingResponse.js +2 -9
- package/dist/src/app/streaming/streamingResponse.js.map +1 -1
- package/dist/src/app/teamsAttachmentDownloader.d.ts +3 -1
- package/dist/src/app/teamsAttachmentDownloader.js +12 -1
- package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
- package/dist/src/auth/authConfiguration.js +12 -0
- package/dist/src/auth/authConfiguration.js.map +1 -1
- package/dist/src/auth/index.d.ts +3 -1
- package/dist/src/auth/index.js +3 -1
- 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 +263 -13
- package/dist/src/auth/jwt-middleware.js.map +1 -1
- package/dist/src/auth/msal/msalConnectionSettings.d.ts +8 -0
- package/dist/src/auth/msal/msalHttpRetryHandlerHelper.d.ts +34 -0
- package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js +126 -0
- package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js.map +1 -0
- package/dist/src/auth/msal/msalTokenProvider.d.ts +1 -0
- package/dist/src/auth/msal/msalTokenProvider.js +23 -14
- package/dist/src/auth/msal/msalTokenProvider.js.map +1 -1
- package/dist/src/auth/settings.d.ts +18 -0
- package/dist/src/auth/settings.js +8 -2
- package/dist/src/auth/settings.js.map +1 -1
- package/dist/src/cloudAdapter.d.ts +29 -2
- package/dist/src/cloudAdapter.js +44 -2
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/errorHelper.js +137 -101
- package/dist/src/errorHelper.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/invoke/adaptiveCardInvokeValue.d.ts +5 -0
- package/dist/src/oauth/userTokenClient.d.ts +0 -1
- package/dist/src/oauth/userTokenClient.js +0 -1
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/dist/src/observability/traces.js +3 -2
- package/dist/src/observability/traces.js.map +1 -1
- package/dist/src/outboundHostValidator.d.ts +47 -0
- package/dist/src/outboundHostValidator.js +149 -0
- package/dist/src/outboundHostValidator.js.map +1 -0
- package/dist/src/state/agentState.d.ts +5 -0
- package/dist/src/state/agentState.js +16 -3
- package/dist/src/state/agentState.js.map +1 -1
- package/package.json +3 -3
- package/src/agent-client/agentClient.ts +46 -23
- package/src/agent-client/agentResponseHandler.ts +14 -9
- package/src/agent-client/createAgentResponseHandler.ts +70 -9
- package/src/app/adaptiveCards/activityValueParsers.ts +3 -10
- package/src/app/adaptiveCards/adaptiveCardsActions.ts +52 -24
- package/src/app/agentApplication.ts +143 -67
- package/src/app/attachmentDownloader.ts +25 -5
- package/src/app/auth/handlers/agenticAuthorization.ts +1 -1
- package/src/app/auth/handlers/azureBotAuthorization.ts +1 -1
- package/src/app/auth/types.ts +3 -3
- package/src/app/index.ts +8 -0
- package/src/app/streaming/streamingResponse.ts +2 -8
- package/src/app/teamsAttachmentDownloader.ts +11 -1
- package/src/auth/authConfiguration.ts +12 -0
- package/src/auth/index.ts +3 -1
- package/src/auth/jwt-middleware.ts +298 -14
- package/src/auth/msal/msalConnectionSettings.ts +10 -0
- package/src/auth/msal/msalHttpRetryHandlerHelper.ts +161 -0
- package/src/auth/msal/msalTokenProvider.ts +32 -14
- package/src/auth/settings.ts +18 -3
- package/src/cloudAdapter.ts +56 -3
- package/src/errorHelper.ts +155 -114
- package/src/index.ts +1 -0
- package/src/invoke/adaptiveCardInvokeValue.ts +5 -0
- package/src/oauth/userTokenClient.ts +0 -1
- package/src/observability/traces.ts +2 -1
- package/src/outboundHostValidator.ts +172 -0
- package/src/state/agentState.ts +24 -6
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { INetworkModule, NetworkRequestOptions, NetworkResponse } from '@azure/msal-node'
|
|
7
|
+
import { ExceptionHelper } from '@microsoft/agents-activity'
|
|
8
|
+
import { debug } from '@microsoft/agents-telemetry'
|
|
9
|
+
import { Errors } from '../../errorHelper'
|
|
10
|
+
|
|
11
|
+
const logger = debug('agents:msal')
|
|
12
|
+
const requestTimeoutStatus = 408
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The default number of retries made for an MSAL HTTP request.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_MSAL_RETRY_COUNT = 2
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Converts a configured MSAL retry count to its effective runtime value.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeMsalRetryCount (retryCount: number | undefined): number {
|
|
23
|
+
if (!Number.isFinite(retryCount)) {
|
|
24
|
+
return DEFAULT_MSAL_RETRY_COUNT
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return Math.max(0, Math.floor(retryCount as number))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* MSAL network client that retries responses with HTTP status 408 (Request Timeout).
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* `maxRetryCount` is the number of retries after the initial request. Responses with
|
|
35
|
+
* any other status are returned immediately and retries have no additional delay.
|
|
36
|
+
*/
|
|
37
|
+
export class MsalHttpRetryHandlerHelper implements INetworkModule {
|
|
38
|
+
private readonly maxRetryCount: number
|
|
39
|
+
|
|
40
|
+
public constructor (
|
|
41
|
+
private readonly networkClient: INetworkModule = new MsalHttpClient(),
|
|
42
|
+
maxRetryCount: number = DEFAULT_MSAL_RETRY_COUNT
|
|
43
|
+
) {
|
|
44
|
+
this.maxRetryCount = normalizeMsalRetryCount(maxRetryCount)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Sends an HTTP GET request, retrying only Request Timeout responses.
|
|
49
|
+
*/
|
|
50
|
+
public async sendGetRequestAsync<T> (
|
|
51
|
+
url: string,
|
|
52
|
+
options?: NetworkRequestOptions,
|
|
53
|
+
timeout?: number
|
|
54
|
+
): Promise<NetworkResponse<T>> {
|
|
55
|
+
return this.sendWithRetries(() => this.networkClient.sendGetRequestAsync<T>(url, options, timeout))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sends an HTTP POST request, retrying only Request Timeout responses.
|
|
60
|
+
*/
|
|
61
|
+
public async sendPostRequestAsync<T> (
|
|
62
|
+
url: string,
|
|
63
|
+
options?: NetworkRequestOptions
|
|
64
|
+
): Promise<NetworkResponse<T>> {
|
|
65
|
+
return this.sendWithRetries(() => this.networkClient.sendPostRequestAsync<T>(url, options))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private async sendWithRetries<T> (send: () => Promise<NetworkResponse<T>>): Promise<NetworkResponse<T>> {
|
|
69
|
+
let response = await send()
|
|
70
|
+
let attempt
|
|
71
|
+
for (attempt = 0; response.status === requestTimeoutStatus && attempt < this.maxRetryCount; attempt++) {
|
|
72
|
+
logger.debug('MSAL retry on request timeout (retry %d/%d)', attempt + 1, this.maxRetryCount)
|
|
73
|
+
response = await send()
|
|
74
|
+
}
|
|
75
|
+
logger.debug('MSAL request completed with status %d after %d attempt(s)', response.status, attempt + 1)
|
|
76
|
+
return response
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default MSAL-compatible network implementation. MSAL Node does not export its
|
|
82
|
+
* built-in HTTP client, so the retry handler uses the same native fetch contract.
|
|
83
|
+
*/
|
|
84
|
+
class MsalHttpClient implements INetworkModule {
|
|
85
|
+
public async sendGetRequestAsync<T> (
|
|
86
|
+
url: string,
|
|
87
|
+
options?: NetworkRequestOptions,
|
|
88
|
+
timeout?: number
|
|
89
|
+
): Promise<NetworkResponse<T>> {
|
|
90
|
+
return this.sendRequest<T>(url, 'GET', options, timeout)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async sendPostRequestAsync<T> (
|
|
94
|
+
url: string,
|
|
95
|
+
options?: NetworkRequestOptions
|
|
96
|
+
): Promise<NetworkResponse<T>> {
|
|
97
|
+
return this.sendRequest<T>(url, 'POST', options)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async sendRequest<T> (
|
|
101
|
+
url: string,
|
|
102
|
+
method: 'GET' | 'POST',
|
|
103
|
+
options?: NetworkRequestOptions,
|
|
104
|
+
timeout?: number
|
|
105
|
+
): Promise<NetworkResponse<T>> {
|
|
106
|
+
const controller = new AbortController()
|
|
107
|
+
const timeoutId = timeout
|
|
108
|
+
? setTimeout(() => controller.abort(), timeout)
|
|
109
|
+
: undefined
|
|
110
|
+
|
|
111
|
+
let response: Response
|
|
112
|
+
try {
|
|
113
|
+
response = await fetch(url, {
|
|
114
|
+
method,
|
|
115
|
+
headers: options?.headers,
|
|
116
|
+
body: method === 'POST' ? options?.body : undefined,
|
|
117
|
+
signal: controller.signal
|
|
118
|
+
})
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const innerException = error instanceof Error ? error : undefined
|
|
121
|
+
if (innerException?.name === 'AbortError') {
|
|
122
|
+
throw ExceptionHelper.generateException(Error, Errors.TokenRequestTimeout, innerException, { timeoutMs: String(timeout) })
|
|
123
|
+
}
|
|
124
|
+
throw ExceptionHelper.generateException(Error, Errors.MsalHttpRequestFailed, innerException, {
|
|
125
|
+
message: innerException?.message ?? 'unknown'
|
|
126
|
+
})
|
|
127
|
+
} finally {
|
|
128
|
+
if (timeoutId) {
|
|
129
|
+
clearTimeout(timeoutId)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const headers: Record<string, string> = {}
|
|
134
|
+
response.headers.forEach((value, key) => {
|
|
135
|
+
headers[key] = value
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
headers,
|
|
140
|
+
body: await this.parseBody<T>(response),
|
|
141
|
+
status: response.status
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async parseBody<T> (response: Response): Promise<T> {
|
|
146
|
+
const text = await response.text()
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(text) as T
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (response.status === requestTimeoutStatus) {
|
|
152
|
+
return {} as T
|
|
153
|
+
}
|
|
154
|
+
throw ExceptionHelper.generateException(
|
|
155
|
+
Error,
|
|
156
|
+
Errors.MsalResponseUnparsable,
|
|
157
|
+
error instanceof Error ? error : undefined
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -15,6 +15,7 @@ import crypto from 'crypto'
|
|
|
15
15
|
import { AuthenticationTraceDefinitions } from '../../observability'
|
|
16
16
|
import { ExceptionHelper } from '@microsoft/agents-activity'
|
|
17
17
|
import { Errors } from '../../errorHelper'
|
|
18
|
+
import { MsalHttpRetryHandlerHelper, normalizeMsalRetryCount } from './msalHttpRetryHandlerHelper'
|
|
18
19
|
|
|
19
20
|
const audience = 'api://AzureADTokenExchange'
|
|
20
21
|
const logger = debug('agents:msal')
|
|
@@ -203,12 +204,14 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
private getClientSecretClient (authConfig: AuthConfiguration): ConfidentialClientApplication {
|
|
207
|
+
const retryCount = normalizeMsalRetryCount(authConfig.msalRetryCount)
|
|
206
208
|
const cacheKey = MsalTokenProvider.cacheKey(
|
|
207
209
|
'confidential-client',
|
|
208
210
|
AuthType.ClientSecret,
|
|
209
211
|
authConfig.clientId,
|
|
210
212
|
resolveAuthorityUtil(authConfig.authorityEndpoint ?? authConfig.authority, authConfig.tenantId),
|
|
211
|
-
MsalTokenProvider.digest(authConfig.clientSecret)
|
|
213
|
+
MsalTokenProvider.digest(authConfig.clientSecret),
|
|
214
|
+
retryCount
|
|
212
215
|
)
|
|
213
216
|
|
|
214
217
|
return this.getOrCreateConfidentialClient(cacheKey, () => new ConfidentialClientApplication({
|
|
@@ -217,11 +220,12 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
217
220
|
authority: resolveAuthorityUtil(authConfig.authorityEndpoint ?? authConfig.authority, authConfig.tenantId),
|
|
218
221
|
clientSecret: authConfig.clientSecret
|
|
219
222
|
},
|
|
220
|
-
system: this.
|
|
223
|
+
system: this.getSystemOptions(authConfig)
|
|
221
224
|
}))
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
private getCertificateClient (authConfig: AuthConfiguration): ConfidentialClientApplication {
|
|
228
|
+
const retryCount = normalizeMsalRetryCount(authConfig.msalRetryCount)
|
|
225
229
|
const cacheKey = MsalTokenProvider.cacheKey(
|
|
226
230
|
'confidential-client',
|
|
227
231
|
authConfig.authType ?? AuthType.Certificate,
|
|
@@ -229,7 +233,8 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
229
233
|
resolveAuthorityUtil(authConfig.authorityEndpoint ?? authConfig.authority, authConfig.tenantId),
|
|
230
234
|
this.getFileCacheIdentity(authConfig.certPemFile),
|
|
231
235
|
this.getFileCacheIdentity(authConfig.certKeyFile),
|
|
232
|
-
authConfig.sendX5C
|
|
236
|
+
authConfig.sendX5C,
|
|
237
|
+
retryCount
|
|
233
238
|
)
|
|
234
239
|
|
|
235
240
|
return this.getOrCreateConfidentialClient(cacheKey, () => {
|
|
@@ -258,7 +263,7 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
258
263
|
x5c: pemFile.toString()
|
|
259
264
|
}
|
|
260
265
|
},
|
|
261
|
-
system: this.
|
|
266
|
+
system: this.getSystemOptions(authConfig)
|
|
262
267
|
})
|
|
263
268
|
})
|
|
264
269
|
}
|
|
@@ -434,7 +439,7 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
434
439
|
clientAssertion: appToken,
|
|
435
440
|
authority: this.resolveAuthority(tenantId),
|
|
436
441
|
},
|
|
437
|
-
system: this.
|
|
442
|
+
system: this.getSystemOptions(this.connectionSettings)
|
|
438
443
|
})
|
|
439
444
|
|
|
440
445
|
const token = await cca.acquireTokenByClientCredential({
|
|
@@ -613,7 +618,7 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
613
618
|
managedIdentityIdParams: {
|
|
614
619
|
userAssignedClientId: this.connectionSettings.clientId
|
|
615
620
|
},
|
|
616
|
-
system: this.
|
|
621
|
+
system: this.getSystemOptions(this.connectionSettings)
|
|
617
622
|
})
|
|
618
623
|
const tokenResult = await msiApp.acquireToken({ resource })
|
|
619
624
|
if (!tokenResult?.accessToken) {
|
|
@@ -638,7 +643,10 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
638
643
|
if (!this.connectionSettings.federatedClientId && !this.connectionSettings.FICClientId) {
|
|
639
644
|
throw ExceptionHelper.generateException(Error, Errors.FICClientIdRequired)
|
|
640
645
|
}
|
|
641
|
-
clientAssertion = await this.fetchExternalToken(
|
|
646
|
+
clientAssertion = await this.fetchExternalToken(
|
|
647
|
+
this.connectionSettings.federatedClientId as string || this.connectionSettings.FICClientId as string,
|
|
648
|
+
this.connectionSettings
|
|
649
|
+
)
|
|
642
650
|
break
|
|
643
651
|
case AuthType.Certificate:
|
|
644
652
|
case AuthType.CertificateSubjectName:
|
|
@@ -688,6 +696,13 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
688
696
|
}
|
|
689
697
|
}
|
|
690
698
|
|
|
699
|
+
private getSystemOptions (authConfig?: AuthConfiguration): NodeSystemOptions {
|
|
700
|
+
return {
|
|
701
|
+
...this.sysOptions,
|
|
702
|
+
networkClient: new MsalHttpRetryHandlerHelper(undefined, normalizeMsalRetryCount(authConfig?.msalRetryCount))
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
691
706
|
/**
|
|
692
707
|
* Generates the client assertion using the provided certificate.
|
|
693
708
|
* @param authConfig The authentication configuration.
|
|
@@ -742,7 +757,7 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
742
757
|
managedIdentityIdParams: {
|
|
743
758
|
userAssignedClientId: authConfig.clientId || ''
|
|
744
759
|
},
|
|
745
|
-
system: this.
|
|
760
|
+
system: this.getSystemOptions(authConfig)
|
|
746
761
|
})
|
|
747
762
|
const token = await mia.acquireToken({
|
|
748
763
|
resource: scope
|
|
@@ -758,7 +773,7 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
758
773
|
*/
|
|
759
774
|
private async acquireTokenWithSystemAssignedIdentity (authConfig: AuthConfiguration, scope: string) {
|
|
760
775
|
const mia = new ManagedIdentityApplication({
|
|
761
|
-
system: this.
|
|
776
|
+
system: this.getSystemOptions(authConfig)
|
|
762
777
|
})
|
|
763
778
|
const token = await mia.acquireToken({
|
|
764
779
|
resource: scope
|
|
@@ -812,14 +827,17 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
812
827
|
*/
|
|
813
828
|
private async acquireAccessTokenViaFIC (authConfig: AuthConfiguration, scope: string) : Promise<string> {
|
|
814
829
|
const scopes = [`${scope}/.default`]
|
|
815
|
-
const clientAssertion = await this.fetchExternalToken(
|
|
830
|
+
const clientAssertion = await this.fetchExternalToken(
|
|
831
|
+
authConfig.federatedClientId as string || authConfig.FICClientId as string,
|
|
832
|
+
authConfig
|
|
833
|
+
)
|
|
816
834
|
const cca = new ConfidentialClientApplication({
|
|
817
835
|
auth: {
|
|
818
836
|
clientId: authConfig.clientId as string,
|
|
819
837
|
authority: `${authConfig.authorityEndpoint ?? authConfig.authority}/${authConfig.tenantId}`,
|
|
820
838
|
clientAssertion
|
|
821
839
|
},
|
|
822
|
-
system: this.
|
|
840
|
+
system: this.getSystemOptions(authConfig)
|
|
823
841
|
})
|
|
824
842
|
const token = await cca.acquireTokenByClientCredential({ scopes, azureRegion: authConfig.azureRegion })
|
|
825
843
|
logger.debug('got token using FIC client assertion')
|
|
@@ -845,7 +863,7 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
845
863
|
authority: `https://login.microsoftonline.com/${authConfig.tenantId}`,
|
|
846
864
|
clientAssertion
|
|
847
865
|
},
|
|
848
|
-
system: this.
|
|
866
|
+
system: this.getSystemOptions(authConfig)
|
|
849
867
|
})
|
|
850
868
|
const token = await cca.acquireTokenByClientCredential({ scopes, azureRegion: authConfig.azureRegion })
|
|
851
869
|
logger.debug('got token using WID client assertion')
|
|
@@ -860,12 +878,12 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
860
878
|
* @param FICClientId The FIC client ID.
|
|
861
879
|
* @returns A promise that resolves to the external token.
|
|
862
880
|
*/
|
|
863
|
-
private async fetchExternalToken (FICClientId: string) : Promise<string> {
|
|
881
|
+
private async fetchExternalToken (FICClientId: string, authConfig?: AuthConfiguration) : Promise<string> {
|
|
864
882
|
const managedIdentityClientAssertion = new ManagedIdentityApplication({
|
|
865
883
|
managedIdentityIdParams: {
|
|
866
884
|
userAssignedClientId: FICClientId
|
|
867
885
|
},
|
|
868
|
-
system: this.
|
|
886
|
+
system: this.getSystemOptions(authConfig ?? this.connectionSettings)
|
|
869
887
|
}
|
|
870
888
|
)
|
|
871
889
|
const response = await managedIdentityClientAssertion.acquireToken({
|
package/src/auth/settings.ts
CHANGED
|
@@ -170,6 +170,12 @@ export function resolveAuthority (authority?: string, tenantId?: string): string
|
|
|
170
170
|
return `${base}/${tenantId ?? 'botframework.com'}`
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function getEffectiveTenant (tenantId: string | undefined, authority: string): string | undefined {
|
|
174
|
+
const resolvedAuthority = new URL(trimTrailingSlashes(authority))
|
|
175
|
+
const embeddedTenant = resolvedAuthority.pathname.split('/').filter(Boolean).pop()
|
|
176
|
+
return embeddedTenant ?? tenantId
|
|
177
|
+
}
|
|
178
|
+
|
|
173
179
|
function trimTrailingSlashes (value: string): string {
|
|
174
180
|
let end = value.length
|
|
175
181
|
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
@@ -179,14 +185,14 @@ function trimTrailingSlashes (value: string): string {
|
|
|
179
185
|
return end === value.length ? value : value.slice(0, end)
|
|
180
186
|
}
|
|
181
187
|
|
|
182
|
-
function getDefaultIssuers (tenantId: string, authority: string) : string[] {
|
|
188
|
+
export function getDefaultIssuers (tenantId: string, authority: string) : string[] {
|
|
183
189
|
// Convert empty string to undefined so resolveAuthority applies its 'botframework.com' default
|
|
184
|
-
const t = tenantId || undefined
|
|
190
|
+
const t = getEffectiveTenant(tenantId || undefined, authority)
|
|
185
191
|
if (!t) {
|
|
186
192
|
logger.warn('tenantId is not configured, defaulting to botframework.com')
|
|
187
193
|
}
|
|
188
194
|
return [
|
|
189
|
-
'https://api.botframework.com',
|
|
195
|
+
/login\.microsoftonline\.us/i.test(authority) ? 'https://api.botframework.us' : 'https://api.botframework.com',
|
|
190
196
|
`${resolveAuthority('https://sts.windows.net', t)}/`,
|
|
191
197
|
`${resolveAuthority(authority, t)}/v2.0`
|
|
192
198
|
]
|
|
@@ -223,6 +229,15 @@ export interface ConnectionSettingsBase {
|
|
|
223
229
|
*/
|
|
224
230
|
issuers?: string[]
|
|
225
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Whether to validate the token issuer against {@link issuers}.
|
|
234
|
+
*
|
|
235
|
+
* @remarks
|
|
236
|
+
* Disabled by default for backward compatibility. Tenant-to-issuer binding is always applied
|
|
237
|
+
* independently when both claims contain comparable tenant GUIDs.
|
|
238
|
+
*/
|
|
239
|
+
validateIssuer?: boolean
|
|
240
|
+
|
|
226
241
|
/**
|
|
227
242
|
* The connection name for the authentication configuration.
|
|
228
243
|
*/
|
package/src/cloudAdapter.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { AgentHandler, INVOKE_RESPONSE_KEY } from './activityHandler'
|
|
|
7
7
|
import { BaseAdapter } from './baseAdapter'
|
|
8
8
|
import { TurnContext } from './turnContext'
|
|
9
9
|
import { Request } from './auth/request'
|
|
10
|
-
import { WebResponse } from './interfaces/webResponse'
|
|
10
|
+
import { NextFunction, WebResponse } from './interfaces/webResponse'
|
|
11
11
|
import { ConnectorClient } from './connector-client/connectorClient'
|
|
12
12
|
import { AuthConfiguration, getAuthConfigWithDefaults } from './auth/authConfiguration'
|
|
13
13
|
import { AuthProvider } from './auth/authProvider'
|
|
@@ -32,6 +32,8 @@ import { parseBooleanEnv, suggestClosest } from './utils/env'
|
|
|
32
32
|
import { trace } from '@microsoft/agents-telemetry'
|
|
33
33
|
import { AdapterTraceDefinitions } from './observability'
|
|
34
34
|
import { applyAgenticHeaders } from './getProductInfo'
|
|
35
|
+
import { createOutboundHostValidator, OutboundUrlPolicy } from './outboundHostValidator'
|
|
36
|
+
import { authorizeJWT } from './auth/jwt-middleware'
|
|
35
37
|
|
|
36
38
|
const logger = debug('agents:cloud-adapter')
|
|
37
39
|
|
|
@@ -80,6 +82,9 @@ export interface CloudAdapterOptions {
|
|
|
80
82
|
* for one service URL tries to send activities targeting a different one.
|
|
81
83
|
*
|
|
82
84
|
* Env var: `CloudAdapterOptions__validateServiceUrl`.
|
|
85
|
+
*
|
|
86
|
+
* @deprecated Configure {@link OutboundUrlPolicy} instead. This option
|
|
87
|
+
* remains available for backward compatibility with claim-only validation.
|
|
83
88
|
*/
|
|
84
89
|
validateServiceUrl?: boolean
|
|
85
90
|
}
|
|
@@ -238,6 +243,7 @@ function truncateActivityForLog (activity: unknown, max = 1024): string {
|
|
|
238
243
|
|
|
239
244
|
export class CloudAdapter extends BaseAdapter {
|
|
240
245
|
protected readonly authConfig: AuthConfiguration
|
|
246
|
+
private readonly jwtMiddleware: ReturnType<typeof authorizeJWT>
|
|
241
247
|
protected _agentName?: string
|
|
242
248
|
|
|
243
249
|
/**
|
|
@@ -246,6 +252,7 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
246
252
|
connectionManager: Connections
|
|
247
253
|
|
|
248
254
|
private readonly _options: Required<CloudAdapterOptions>
|
|
255
|
+
private readonly _hostValidator: OutboundUrlPolicy
|
|
249
256
|
|
|
250
257
|
/**
|
|
251
258
|
* Creates an instance of CloudAdapter.
|
|
@@ -253,12 +260,17 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
253
260
|
* @param authProvider - No longer used.
|
|
254
261
|
* @param userTokenClient - No longer used.
|
|
255
262
|
* @param options - Optional runtime behavior overrides. See {@link CloudAdapterOptions}.
|
|
263
|
+
* @param outboundHostValidator - Optional policy for validating outbound `Activity.serviceUrl` hosts.
|
|
264
|
+
* When omitted, the policy is loaded from `OutboundHostValidator__*` environment variables
|
|
265
|
+
* and remains disabled by default.
|
|
256
266
|
*/
|
|
257
|
-
constructor (authConfig?: AuthConfiguration, authProvider?: AuthProvider, userTokenClient?: UserTokenClient, options?: CloudAdapterOptions) {
|
|
267
|
+
constructor (authConfig?: AuthConfiguration, authProvider?: AuthProvider, userTokenClient?: UserTokenClient, options?: CloudAdapterOptions, outboundHostValidator?: OutboundUrlPolicy) {
|
|
258
268
|
super()
|
|
259
269
|
this.authConfig = authConfig = getAuthConfigWithDefaults(authConfig)
|
|
270
|
+
this.jwtMiddleware = authorizeJWT(authConfig)
|
|
260
271
|
this.connectionManager = new MsalConnectionManager(undefined, undefined, authConfig)
|
|
261
272
|
this._options = resolveCloudAdapterOptions(options)
|
|
273
|
+
this._hostValidator = outboundHostValidator ?? createOutboundHostValidator()
|
|
262
274
|
|
|
263
275
|
// Install a CloudAdapter-aware default `onTurnError` that honors
|
|
264
276
|
// `emitStackTrace`. The base class default only logs the message; we
|
|
@@ -321,6 +333,8 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
321
333
|
identity: JwtPayload,
|
|
322
334
|
headers?: HeaderPropagationCollection
|
|
323
335
|
): Promise<ConnectorClient> {
|
|
336
|
+
this.ensureOutboundServiceUrlAllowed(serviceUrl)
|
|
337
|
+
|
|
324
338
|
return trace(AdapterTraceDefinitions.createConnectorClient, async ({ record }) => {
|
|
325
339
|
record({ serviceUrl, scopes: [scope] })
|
|
326
340
|
|
|
@@ -349,6 +363,8 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
349
363
|
identity: JwtPayload,
|
|
350
364
|
activity: Activity,
|
|
351
365
|
headers?: HeaderPropagationCollection) {
|
|
366
|
+
this.ensureOutboundServiceUrlAllowed(activity.serviceUrl)
|
|
367
|
+
|
|
352
368
|
return trace(AdapterTraceDefinitions.createConnectorClient, async ({ record }) => {
|
|
353
369
|
if (!identity?.aud) {
|
|
354
370
|
// anonymous
|
|
@@ -425,6 +441,24 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
425
441
|
} as JwtPayload
|
|
426
442
|
}
|
|
427
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Authorizes an incoming web request using this adapter's resolved authentication configuration.
|
|
446
|
+
* @param req The incoming request.
|
|
447
|
+
* @param res The outgoing response.
|
|
448
|
+
* @param next Callback invoked after successful authorization.
|
|
449
|
+
*/
|
|
450
|
+
public async authorizeRequest (req: Request, res: WebResponse, next: NextFunction): Promise<void> {
|
|
451
|
+
await this.jwtMiddleware(req, res, next)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Gets the client ID used by this adapter's default authentication connection.
|
|
456
|
+
* @returns The configured client ID, or `undefined` for anonymous development configuration.
|
|
457
|
+
*/
|
|
458
|
+
public getClientId (): string | undefined {
|
|
459
|
+
return this.authConfig.clientId
|
|
460
|
+
}
|
|
461
|
+
|
|
428
462
|
/**
|
|
429
463
|
* Sets the agent name for M365 agent header propagation.
|
|
430
464
|
* @param agentName The human-friendly agent name to set for header propagation.
|
|
@@ -692,6 +726,8 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
692
726
|
* should be rejected with a 400.
|
|
693
727
|
*/
|
|
694
728
|
private validateServiceUrl (identity: JwtPayload | undefined, activity: Activity): boolean {
|
|
729
|
+
if (!this.isOutboundServiceUrlAllowed(activity.serviceUrl)) return false
|
|
730
|
+
|
|
695
731
|
if (!identity) return true
|
|
696
732
|
if (!activity.serviceUrl) return true
|
|
697
733
|
|
|
@@ -709,7 +745,7 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
709
745
|
|
|
710
746
|
const safeClaim = sanitizeForLog(claimValue)
|
|
711
747
|
const safeServiceUrl = sanitizeForLog(activity.serviceUrl)
|
|
712
|
-
if (this._options.validateServiceUrl) {
|
|
748
|
+
if (this._hostValidator.enabled || this._options.validateServiceUrl) {
|
|
713
749
|
logger.error(`Invalid service URL Claim='${safeClaim}', ServiceUrl='${safeServiceUrl}'`)
|
|
714
750
|
return false
|
|
715
751
|
}
|
|
@@ -717,6 +753,23 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
717
753
|
return true
|
|
718
754
|
}
|
|
719
755
|
|
|
756
|
+
/**
|
|
757
|
+
* Applies the shared outbound host policy.
|
|
758
|
+
*/
|
|
759
|
+
private isOutboundServiceUrlAllowed (serviceUrl: string | undefined): boolean {
|
|
760
|
+
if (!this._hostValidator.enabled || !serviceUrl || this._hostValidator.isAllowed(serviceUrl)) return true
|
|
761
|
+
|
|
762
|
+
logger.warn(`ServiceUrl host is not in the configured allowed hosts. ServiceUrl='${sanitizeForLog(serviceUrl)}'`)
|
|
763
|
+
return false
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** Ensures connector-client creation cannot bypass the outbound host policy. */
|
|
767
|
+
private ensureOutboundServiceUrlAllowed (serviceUrl: string | undefined): void {
|
|
768
|
+
if (!this.isOutboundServiceUrlAllowed(serviceUrl)) {
|
|
769
|
+
throw ExceptionHelper.generateException(Error, Errors.OutboundServiceUrlNotAllowed)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
720
773
|
/**
|
|
721
774
|
* Updates an activity.
|
|
722
775
|
* @param context - The TurnContext for the current turn.
|