@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.
Files changed (100) hide show
  1. package/README.md +64 -2
  2. package/dist/package.json +3 -3
  3. package/dist/src/agent-client/agentClient.d.ts +18 -6
  4. package/dist/src/agent-client/agentClient.js +34 -18
  5. package/dist/src/agent-client/agentClient.js.map +1 -1
  6. package/dist/src/agent-client/agentResponseHandler.d.ts +14 -9
  7. package/dist/src/agent-client/agentResponseHandler.js +10 -5
  8. package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
  9. package/dist/src/agent-client/createAgentResponseHandler.d.ts +7 -2
  10. package/dist/src/agent-client/createAgentResponseHandler.js +55 -8
  11. package/dist/src/agent-client/createAgentResponseHandler.js.map +1 -1
  12. package/dist/src/app/adaptiveCards/activityValueParsers.d.ts +1 -4
  13. package/dist/src/app/adaptiveCards/activityValueParsers.js +2 -6
  14. package/dist/src/app/adaptiveCards/activityValueParsers.js.map +1 -1
  15. package/dist/src/app/adaptiveCards/adaptiveCardsActions.d.ts +18 -0
  16. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +53 -23
  17. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
  18. package/dist/src/app/agentApplication.d.ts +7 -0
  19. package/dist/src/app/agentApplication.js +130 -70
  20. package/dist/src/app/agentApplication.js.map +1 -1
  21. package/dist/src/app/attachmentDownloader.d.ts +4 -1
  22. package/dist/src/app/attachmentDownloader.js +27 -5
  23. package/dist/src/app/attachmentDownloader.js.map +1 -1
  24. package/dist/src/app/auth/handlers/agenticAuthorization.d.ts +1 -1
  25. package/dist/src/app/auth/handlers/azureBotAuthorization.d.ts +1 -1
  26. package/dist/src/app/auth/types.d.ts +3 -4
  27. package/dist/src/app/index.d.ts +2 -0
  28. package/dist/src/app/index.js.map +1 -1
  29. package/dist/src/app/streaming/streamingResponse.js +2 -9
  30. package/dist/src/app/streaming/streamingResponse.js.map +1 -1
  31. package/dist/src/app/teamsAttachmentDownloader.d.ts +3 -1
  32. package/dist/src/app/teamsAttachmentDownloader.js +12 -1
  33. package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
  34. package/dist/src/auth/authConfiguration.js +12 -0
  35. package/dist/src/auth/authConfiguration.js.map +1 -1
  36. package/dist/src/auth/index.d.ts +3 -1
  37. package/dist/src/auth/index.js +3 -1
  38. package/dist/src/auth/index.js.map +1 -1
  39. package/dist/src/auth/jwt-middleware.d.ts +6 -0
  40. package/dist/src/auth/jwt-middleware.js +263 -13
  41. package/dist/src/auth/jwt-middleware.js.map +1 -1
  42. package/dist/src/auth/msal/msalConnectionSettings.d.ts +8 -0
  43. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.d.ts +34 -0
  44. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js +126 -0
  45. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js.map +1 -0
  46. package/dist/src/auth/msal/msalTokenProvider.d.ts +1 -0
  47. package/dist/src/auth/msal/msalTokenProvider.js +23 -14
  48. package/dist/src/auth/msal/msalTokenProvider.js.map +1 -1
  49. package/dist/src/auth/settings.d.ts +18 -0
  50. package/dist/src/auth/settings.js +8 -2
  51. package/dist/src/auth/settings.js.map +1 -1
  52. package/dist/src/cloudAdapter.d.ts +29 -2
  53. package/dist/src/cloudAdapter.js +44 -2
  54. package/dist/src/cloudAdapter.js.map +1 -1
  55. package/dist/src/errorHelper.js +137 -101
  56. package/dist/src/errorHelper.js.map +1 -1
  57. package/dist/src/index.d.ts +1 -0
  58. package/dist/src/index.js +1 -0
  59. package/dist/src/index.js.map +1 -1
  60. package/dist/src/invoke/adaptiveCardInvokeValue.d.ts +5 -0
  61. package/dist/src/oauth/userTokenClient.d.ts +0 -1
  62. package/dist/src/oauth/userTokenClient.js +0 -1
  63. package/dist/src/oauth/userTokenClient.js.map +1 -1
  64. package/dist/src/observability/traces.js +3 -2
  65. package/dist/src/observability/traces.js.map +1 -1
  66. package/dist/src/outboundHostValidator.d.ts +47 -0
  67. package/dist/src/outboundHostValidator.js +149 -0
  68. package/dist/src/outboundHostValidator.js.map +1 -0
  69. package/dist/src/state/agentState.d.ts +5 -0
  70. package/dist/src/state/agentState.js +16 -3
  71. package/dist/src/state/agentState.js.map +1 -1
  72. package/package.json +3 -3
  73. package/src/agent-client/agentClient.ts +46 -23
  74. package/src/agent-client/agentResponseHandler.ts +14 -9
  75. package/src/agent-client/createAgentResponseHandler.ts +70 -9
  76. package/src/app/adaptiveCards/activityValueParsers.ts +3 -10
  77. package/src/app/adaptiveCards/adaptiveCardsActions.ts +52 -24
  78. package/src/app/agentApplication.ts +143 -67
  79. package/src/app/attachmentDownloader.ts +25 -5
  80. package/src/app/auth/handlers/agenticAuthorization.ts +1 -1
  81. package/src/app/auth/handlers/azureBotAuthorization.ts +1 -1
  82. package/src/app/auth/types.ts +3 -3
  83. package/src/app/index.ts +8 -0
  84. package/src/app/streaming/streamingResponse.ts +2 -8
  85. package/src/app/teamsAttachmentDownloader.ts +11 -1
  86. package/src/auth/authConfiguration.ts +12 -0
  87. package/src/auth/index.ts +3 -1
  88. package/src/auth/jwt-middleware.ts +298 -14
  89. package/src/auth/msal/msalConnectionSettings.ts +10 -0
  90. package/src/auth/msal/msalHttpRetryHandlerHelper.ts +161 -0
  91. package/src/auth/msal/msalTokenProvider.ts +32 -14
  92. package/src/auth/settings.ts +18 -3
  93. package/src/cloudAdapter.ts +56 -3
  94. package/src/errorHelper.ts +155 -114
  95. package/src/index.ts +1 -0
  96. package/src/invoke/adaptiveCardInvokeValue.ts +5 -0
  97. package/src/oauth/userTokenClient.ts +0 -1
  98. package/src/observability/traces.ts +2 -1
  99. package/src/outboundHostValidator.ts +172 -0
  100. 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.sysOptions
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.sysOptions
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.sysOptions
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.sysOptions
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(this.connectionSettings.federatedClientId as string || this.connectionSettings.FICClientId as string)
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.sysOptions
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.sysOptions
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(authConfig.federatedClientId as string || authConfig.FICClientId as string)
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.sysOptions
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.sysOptions
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.sysOptions
886
+ system: this.getSystemOptions(authConfig ?? this.connectionSettings)
869
887
  }
870
888
  )
871
889
  const response = await managedIdentityClientAssertion.acquireToken({
@@ -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
  */
@@ -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.