@microsoft/agents-hosting 1.1.0-alpha.8.g2362542eea → 1.1.0-alpha.85

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 (144) hide show
  1. package/dist/package.json +10 -6
  2. package/dist/src/activityWireCompat.d.ts +1 -1
  3. package/dist/src/activityWireCompat.js +11 -3
  4. package/dist/src/activityWireCompat.js.map +1 -1
  5. package/dist/src/agent-client/agentClient.js +7 -3
  6. package/dist/src/agent-client/agentClient.js.map +1 -1
  7. package/dist/src/agent-client/agentResponseHandler.js +6 -2
  8. package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
  9. package/dist/src/app/agentApplication.d.ts +26 -11
  10. package/dist/src/app/agentApplication.js +90 -79
  11. package/dist/src/app/agentApplication.js.map +1 -1
  12. package/dist/src/app/agentApplicationBuilder.d.ts +2 -2
  13. package/dist/src/app/agentApplicationBuilder.js.map +1 -1
  14. package/dist/src/app/agentApplicationOptions.d.ts +9 -2
  15. package/dist/src/app/appRoute.d.ts +7 -0
  16. package/dist/src/app/{authorization.d.ts → auth/authorization.d.ts} +41 -139
  17. package/dist/src/app/auth/authorization.js +188 -0
  18. package/dist/src/app/auth/authorization.js.map +1 -0
  19. package/dist/src/app/auth/authorizationManager.d.ts +71 -0
  20. package/dist/src/app/auth/authorizationManager.js +170 -0
  21. package/dist/src/app/auth/authorizationManager.js.map +1 -0
  22. package/dist/src/app/auth/handlerStorage.d.ts +36 -0
  23. package/dist/src/app/auth/handlerStorage.js +62 -0
  24. package/dist/src/app/auth/handlerStorage.js.map +1 -0
  25. package/dist/src/app/auth/handlers/agenticAuthorization.d.ts +93 -0
  26. package/dist/src/app/auth/handlers/agenticAuthorization.js +134 -0
  27. package/dist/src/app/auth/handlers/agenticAuthorization.js.map +1 -0
  28. package/dist/src/app/auth/handlers/azureBotAuthorization.d.ts +222 -0
  29. package/dist/src/app/auth/handlers/azureBotAuthorization.js +428 -0
  30. package/dist/src/app/auth/handlers/azureBotAuthorization.js.map +1 -0
  31. package/dist/src/app/auth/handlers/index.d.ts +2 -0
  32. package/dist/src/app/auth/handlers/index.js +19 -0
  33. package/dist/src/app/auth/handlers/index.js.map +1 -0
  34. package/dist/src/app/auth/index.d.ts +2 -0
  35. package/dist/src/app/auth/index.js +19 -0
  36. package/dist/src/app/auth/index.js.map +1 -0
  37. package/dist/src/app/auth/types.d.ts +104 -0
  38. package/dist/src/app/auth/types.js +24 -0
  39. package/dist/src/app/auth/types.js.map +1 -0
  40. package/dist/src/app/index.d.ts +3 -3
  41. package/dist/src/app/index.js +2 -3
  42. package/dist/src/app/index.js.map +1 -1
  43. package/dist/src/app/routeList.d.ts +1 -1
  44. package/dist/src/app/routeList.js +22 -5
  45. package/dist/src/app/routeList.js.map +1 -1
  46. package/dist/src/app/streaming/streamingResponse.js +2 -1
  47. package/dist/src/app/streaming/streamingResponse.js.map +1 -1
  48. package/dist/src/auth/MemoryCache.d.ts +16 -0
  49. package/dist/src/auth/MemoryCache.js +58 -0
  50. package/dist/src/auth/MemoryCache.js.map +1 -0
  51. package/dist/src/auth/authConfiguration.d.ts +40 -2
  52. package/dist/src/auth/authConfiguration.js +209 -55
  53. package/dist/src/auth/authConfiguration.js.map +1 -1
  54. package/dist/src/auth/authConstants.d.ts +11 -0
  55. package/dist/src/auth/authConstants.js +15 -0
  56. package/dist/src/auth/authConstants.js.map +1 -0
  57. package/dist/src/auth/authProvider.d.ts +23 -0
  58. package/dist/src/auth/connections.d.ts +41 -0
  59. package/dist/src/auth/connections.js +7 -0
  60. package/dist/src/auth/connections.js.map +1 -0
  61. package/dist/src/auth/index.d.ts +2 -0
  62. package/dist/src/auth/index.js +2 -0
  63. package/dist/src/auth/index.js.map +1 -1
  64. package/dist/src/auth/jwt-middleware.js +31 -18
  65. package/dist/src/auth/jwt-middleware.js.map +1 -1
  66. package/dist/src/auth/msalConnectionManager.d.ts +64 -0
  67. package/dist/src/auth/msalConnectionManager.js +148 -0
  68. package/dist/src/auth/msalConnectionManager.js.map +1 -0
  69. package/dist/src/auth/msalTokenProvider.d.ts +24 -0
  70. package/dist/src/auth/msalTokenProvider.js +143 -16
  71. package/dist/src/auth/msalTokenProvider.js.map +1 -1
  72. package/dist/src/baseAdapter.d.ts +10 -25
  73. package/dist/src/baseAdapter.js +2 -15
  74. package/dist/src/baseAdapter.js.map +1 -1
  75. package/dist/src/cloudAdapter.d.ts +40 -23
  76. package/dist/src/cloudAdapter.js +141 -63
  77. package/dist/src/cloudAdapter.js.map +1 -1
  78. package/dist/src/connector-client/connectorClient.d.ts +15 -0
  79. package/dist/src/connector-client/connectorClient.js +49 -15
  80. package/dist/src/connector-client/connectorClient.js.map +1 -1
  81. package/dist/src/index.d.ts +0 -1
  82. package/dist/src/index.js +0 -1
  83. package/dist/src/index.js.map +1 -1
  84. package/dist/src/oauth/customUserTokenAPI.d.ts +1 -0
  85. package/dist/src/oauth/customUserTokenAPI.js +11 -0
  86. package/dist/src/oauth/customUserTokenAPI.js.map +1 -0
  87. package/dist/src/oauth/index.d.ts +0 -1
  88. package/dist/src/oauth/index.js +0 -1
  89. package/dist/src/oauth/index.js.map +1 -1
  90. package/dist/src/oauth/userTokenClient.d.ts +30 -13
  91. package/dist/src/oauth/userTokenClient.js +62 -26
  92. package/dist/src/oauth/userTokenClient.js.map +1 -1
  93. package/dist/src/oauth/userTokenClient.types.d.ts +19 -6
  94. package/dist/src/turnContext.d.ts +7 -1
  95. package/dist/src/turnContext.js +11 -4
  96. package/dist/src/turnContext.js.map +1 -1
  97. package/package.json +10 -6
  98. package/src/activityWireCompat.ts +12 -4
  99. package/src/agent-client/agentClient.ts +9 -3
  100. package/src/agent-client/agentResponseHandler.ts +5 -2
  101. package/src/app/agentApplication.ts +95 -74
  102. package/src/app/agentApplicationBuilder.ts +2 -2
  103. package/src/app/agentApplicationOptions.ts +10 -2
  104. package/src/app/appRoute.ts +8 -0
  105. package/src/app/auth/authorization.ts +261 -0
  106. package/src/app/auth/authorizationManager.ts +213 -0
  107. package/src/app/auth/handlerStorage.ts +61 -0
  108. package/src/app/auth/handlers/agenticAuthorization.ts +182 -0
  109. package/src/app/auth/handlers/azureBotAuthorization.ts +599 -0
  110. package/src/app/auth/handlers/index.ts +2 -0
  111. package/src/app/auth/index.ts +2 -0
  112. package/src/app/auth/types.ts +111 -0
  113. package/src/app/index.ts +3 -3
  114. package/src/app/routeList.ts +24 -5
  115. package/src/app/streaming/streamingResponse.ts +2 -1
  116. package/src/auth/MemoryCache.ts +59 -0
  117. package/src/auth/authConfiguration.ts +239 -53
  118. package/src/auth/authConstants.ts +11 -0
  119. package/src/auth/authProvider.ts +31 -0
  120. package/src/auth/connections.ts +47 -0
  121. package/src/auth/index.ts +2 -0
  122. package/src/auth/jwt-middleware.ts +38 -21
  123. package/src/auth/msalConnectionManager.ts +175 -0
  124. package/src/auth/msalTokenProvider.ts +185 -9
  125. package/src/baseAdapter.ts +10 -29
  126. package/src/cloudAdapter.ts +205 -71
  127. package/src/connector-client/connectorClient.ts +59 -15
  128. package/src/index.ts +0 -1
  129. package/src/oauth/customUserTokenAPI.ts +5 -0
  130. package/src/oauth/index.ts +0 -1
  131. package/src/oauth/userTokenClient.ts +76 -22
  132. package/src/oauth/userTokenClient.types.ts +20 -8
  133. package/src/turnContext.ts +16 -5
  134. package/dist/src/app/authorization.js +0 -387
  135. package/dist/src/app/authorization.js.map +0 -1
  136. package/dist/src/claimsIdentity.d.ts +0 -35
  137. package/dist/src/claimsIdentity.js +0 -43
  138. package/dist/src/claimsIdentity.js.map +0 -1
  139. package/dist/src/oauth/oAuthFlow.d.ts +0 -119
  140. package/dist/src/oauth/oAuthFlow.js +0 -316
  141. package/dist/src/oauth/oAuthFlow.js.map +0 -1
  142. package/src/app/authorization.ts +0 -432
  143. package/src/claimsIdentity.ts +0 -47
  144. package/src/oauth/oAuthFlow.ts +0 -378
@@ -0,0 +1,599 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { debug } from '@microsoft/agents-activity/logger'
7
+ import { AuthorizationHandlerStatus, AuthorizationHandler, ActiveAuthorizationHandler, AuthorizationHandlerSettings, AuthorizationHandlerTokenOptions } from '../types'
8
+ import { MessageFactory } from '../../../messageFactory'
9
+ import { CardFactory } from '../../../cards'
10
+ import { TurnContext } from '../../../turnContext'
11
+ import { TokenExchangeRequest, TokenExchangeInvokeResponse, TokenResponse, UserTokenClient } from '../../../oauth'
12
+ import jwt, { JwtPayload } from 'jsonwebtoken'
13
+ import { HandlerStorage } from '../handlerStorage'
14
+ import { Activity, ActivityTypes, Channels } from '@microsoft/agents-activity'
15
+ import { InvokeResponse, TokenExchangeInvokeRequest } from '../../../invoke'
16
+
17
+ const logger = debug('agents:authorization:azurebot')
18
+
19
+ const DEFAULT_SIGN_IN_ATTEMPTS = 2
20
+
21
+ enum Category {
22
+ SIGNIN = 'signin',
23
+ UNKNOWN = 'unknown',
24
+ }
25
+
26
+ /**
27
+ * Active handler manager information.
28
+ */
29
+ export interface AzureBotActiveHandler extends ActiveAuthorizationHandler {
30
+ /**
31
+ * The number of attempts left for the handler to process in case of failure.
32
+ */
33
+ attemptsLeft: number
34
+ /**
35
+ * The current category of the handler.
36
+ */
37
+ category?: Category
38
+ }
39
+
40
+ /**
41
+ * Messages configuration for the AzureBotAuthorization handler.
42
+ */
43
+ export interface AzureBotAuthorizationOptionsMessages {
44
+ /**
45
+ * Message displayed when an invalid code is entered.
46
+ * Use `{code}` as a placeholder for the entered code.
47
+ * Defaults to: 'The code entered is invalid. Please sign-in again to continue.'
48
+ */
49
+ invalidCode?: string
50
+ /**
51
+ * Message displayed when the entered code format is invalid.
52
+ * Use `{attemptsLeft}` as a placeholder for the number of attempts left.
53
+ * Defaults to: 'Please enter a valid **6-digit** code format (_e.g. 123456_).\r\n**{attemptsLeft} attempt(s) left...**'
54
+ */
55
+ invalidCodeFormat?: string
56
+ /**
57
+ * Message displayed when the maximum number of attempts is exceeded.
58
+ * Use `{maxAttempts}` as a placeholder for the maximum number of attempts.
59
+ * Defaults to: 'You have exceeded the maximum number of sign-in attempts ({maxAttempts}).'
60
+ */
61
+ maxAttemptsExceeded?: string
62
+ }
63
+
64
+ /**
65
+ * Settings for on-behalf-of token acquisition.
66
+ */
67
+ export interface AzureBotAuthorizationOptionsOBO {
68
+ /**
69
+ * Connection name to use for on-behalf-of token acquisition.
70
+ */
71
+ connection?: string
72
+ /**
73
+ * Scopes to request for on-behalf-of token acquisition.
74
+ */
75
+ scopes?: string[]
76
+ }
77
+
78
+ /**
79
+ * Interface defining an authorization handler configuration.
80
+ */
81
+ export interface AzureBotAuthorizationOptions {
82
+ /**
83
+ * The type of authorization handler.
84
+ * This property is optional and should not be set when configuring this handler.
85
+ * It is included here for completeness and type safety.
86
+ */
87
+ type?: undefined
88
+ /**
89
+ * Connection name for the auth provider.
90
+ * @remarks
91
+ * When using environment variables, this can be set using the `${authHandlerId}_connectionName` variable.
92
+ */
93
+ name?: string,
94
+ /**
95
+ * Title to display on auth cards/UI.
96
+ * @remarks
97
+ * When using environment variables, this can be set using the `${authHandlerId}_connectionTitle` variable.
98
+ */
99
+ title?: string,
100
+ /**
101
+ * Text to display on auth cards/UI.
102
+ * @remarks
103
+ * When using environment variables, this can be set using the `${authHandlerId}_connectionText` variable.
104
+ */
105
+ text?: string,
106
+ /**
107
+ * Maximum number of attempts for entering the magic code. Defaults to 2.
108
+ * @remarks
109
+ * When using environment variables, this can be set using the `${authHandlerId}_maxAttempts` variable.
110
+ */
111
+ maxAttempts?: number
112
+ /**
113
+ * Messages to display for various authentication scenarios.
114
+ * @remarks
115
+ * When using environment variables, these can be set using the following variables:
116
+ * - `${authHandlerId}_messages_invalidCode`
117
+ * - `${authHandlerId}_messages_invalidCodeFormat`
118
+ * - `${authHandlerId}_messages_maxAttemptsExceeded`
119
+ */
120
+ messages?: AzureBotAuthorizationOptionsMessages
121
+ /**
122
+ * Settings for on-behalf-of token acquisition.
123
+ * @remarks
124
+ * When using environment variables, these can be set using the following variables:
125
+ * - `${authHandlerId}_obo_connection`
126
+ * - `${authHandlerId}_obo_scopes` (comma-separated values, e.g. `scope1,scope2`)
127
+ */
128
+ obo?: AzureBotAuthorizationOptionsOBO
129
+ }
130
+
131
+ /**
132
+ * Settings for configuring the AzureBot authorization handler.
133
+ */
134
+ export interface AzureBotAuthorizationSettings extends AuthorizationHandlerSettings {}
135
+
136
+ /**
137
+ * Interface for token verification state.
138
+ */
139
+ interface TokenVerifyState {
140
+ state: string
141
+ }
142
+
143
+ /**
144
+ * Interface for sign-in failure value.
145
+ */
146
+ interface SignInFailureValue {
147
+ code: string
148
+ message: string
149
+ }
150
+
151
+ /**
152
+ * Default implementation of an authorization handler using Azure Bot Service.
153
+ */
154
+ export class AzureBotAuthorization implements AuthorizationHandler {
155
+ private _options: AzureBotAuthorizationOptions
156
+ private _onSuccess?: Parameters<AuthorizationHandler['onSuccess']>[0]
157
+ private _onFailure?: Parameters<AuthorizationHandler['onFailure']>[0]
158
+
159
+ /**
160
+ * Creates an instance of the AzureBotAuthorization.
161
+ * @param id The unique identifier for the handler.
162
+ * @param options The settings for the handler.
163
+ * @param app The agent application instance.
164
+ */
165
+ constructor (public readonly id: string, options: AzureBotAuthorizationOptions, private settings: AzureBotAuthorizationSettings) {
166
+ if (!this.settings.storage) {
167
+ throw new Error(this.prefix('The \'storage\' option is not available in the app options. Ensure that the app is properly configured.'))
168
+ }
169
+
170
+ if (!this.settings.connections) {
171
+ throw new Error(this.prefix('The \'connections\' option is not available in the app options. Ensure that the app is properly configured.'))
172
+ }
173
+
174
+ this._options = this.loadOptions(options)
175
+ }
176
+
177
+ /**
178
+ * Loads and validates the authorization handler options.
179
+ */
180
+ private loadOptions (settings: AzureBotAuthorizationOptions) {
181
+ const result: AzureBotAuthorizationOptions = {
182
+ name: settings.name ?? (process.env[`${this.id}_connectionName`]),
183
+ title: settings.title ?? (process.env[`${this.id}_connectionTitle`]) ?? 'Sign-in',
184
+ text: settings.text ?? (process.env[`${this.id}_connectionText`]) ?? 'Please sign-in to continue',
185
+ maxAttempts: settings.maxAttempts ?? parseInt(process.env[`${this.id}_maxAttempts`]!),
186
+ messages: {
187
+ invalidCode: settings.messages?.invalidCode ?? process.env[`${this.id}_messages_invalidCode`],
188
+ invalidCodeFormat: settings.messages?.invalidCodeFormat ?? process.env[`${this.id}_messages_invalidCodeFormat`],
189
+ maxAttemptsExceeded: settings.messages?.maxAttemptsExceeded ?? process.env[`${this.id}_messages_maxAttemptsExceeded`],
190
+ },
191
+ obo: {
192
+ connection: settings.obo?.connection ?? process.env[`${this.id}_obo_connection`],
193
+ scopes: settings.obo?.scopes ?? this.loadScopes(process.env[`${this.id}_obo_scopes`]),
194
+ }
195
+ }
196
+
197
+ if (!result.name) {
198
+ throw new Error(this.prefix(`The 'name' property or '${this.id}_connectionName' env variable is required to initialize the handler.`))
199
+ }
200
+
201
+ return result
202
+ }
203
+
204
+ /**
205
+ * Maximum number of attempts for magic code entry.
206
+ */
207
+ private get maxAttempts (): number {
208
+ const attempts = this._options.maxAttempts
209
+ const result = typeof attempts === 'number' && Number.isFinite(attempts) ? Math.round(attempts) : NaN
210
+ return result > 0 ? result : DEFAULT_SIGN_IN_ATTEMPTS
211
+ }
212
+
213
+ /**
214
+ * Sets a handler to be called when a user successfully signs in.
215
+ * @param callback The callback function to be invoked on successful sign-in.
216
+ */
217
+ onSuccess (callback: (context: TurnContext) => Promise<void> | void): void {
218
+ this._onSuccess = callback
219
+ }
220
+
221
+ /**
222
+ * Sets a handler to be called when a user fails to sign in.
223
+ * @param callback The callback function to be invoked on sign-in failure.
224
+ */
225
+ onFailure (callback: (context: TurnContext, reason?: string) => Promise<void> | void): void {
226
+ this._onFailure = callback
227
+ }
228
+
229
+ /**
230
+ * Retrieves the token for the user, optionally using on-behalf-of flow for specified scopes.
231
+ * @param context The turn context.
232
+ * @param options Optional options for token acquisition, including connection and scopes for on-behalf-of flow.
233
+ * @returns The token response containing the token or undefined if not available.
234
+ */
235
+ async token (context: TurnContext, options?: AuthorizationHandlerTokenOptions): Promise<TokenResponse> {
236
+ let { token } = this.getContext(context)
237
+
238
+ if (!token?.trim()) {
239
+ const { activity } = context
240
+
241
+ const userTokenClient = await this.getUserTokenClient(context)
242
+ // Using getTokenOrSignInResource instead of getUserToken to avoid HTTP 404 errors.
243
+ const { tokenResponse } = await userTokenClient.getTokenOrSignInResource(activity.from?.id!, this._options.name!, activity.channelId!, activity.getConversationReference(), activity.relatesTo!, '')
244
+ token = tokenResponse?.token
245
+ }
246
+
247
+ if (!token?.trim()) {
248
+ return { token: undefined }
249
+ }
250
+
251
+ return await this.handleOBO(token, options)
252
+ }
253
+
254
+ /**
255
+ * Signs out the user from the service.
256
+ * @param context The turn context.
257
+ * @returns True if the signout was successful, false otherwise.
258
+ */
259
+ async signout (context: TurnContext): Promise<boolean> {
260
+ const user = context.activity.from?.id
261
+ const channel = context.activity.channelId
262
+ const connection = this._options.name!
263
+
264
+ if (!channel || !user) {
265
+ throw new Error(this.prefix('Both \'activity.channelId\' and \'activity.from.id\' are required to perform signout.'))
266
+ }
267
+
268
+ logger.debug(this.prefix(`Signing out User '${user}' from => Channel: '${channel}', Connection: '${connection}'`), context.activity)
269
+ const userTokenClient = await this.getUserTokenClient(context)
270
+ await userTokenClient.signOut(user, connection, channel)
271
+ return true
272
+ }
273
+
274
+ /**
275
+ * Initiates the sign-in process for the handler.
276
+ * @param context The turn context.
277
+ * @param active Optional active handler data.
278
+ * @returns The status of the sign-in attempt.
279
+ */
280
+ async signin (context: TurnContext, active?: AzureBotActiveHandler): Promise<AuthorizationHandlerStatus> {
281
+ const { activity } = context
282
+ const [category] = activity.name?.split('/') ?? [Category.UNKNOWN]
283
+
284
+ const storage = new HandlerStorage<AzureBotActiveHandler>(this.settings.storage, context)
285
+
286
+ if (!active) {
287
+ return this.setToken(storage, context)
288
+ }
289
+
290
+ logger.debug(this.prefix('Sign-in active session detected'), active.activity)
291
+
292
+ if (active.activity.conversation?.id !== activity.conversation?.id) {
293
+ await this.sendInvokeResponse(context, { status: 400 })
294
+ logger.warn(this.prefix('Discarding the active session due to the conversation has changed during an active sign-in process'), activity)
295
+ return AuthorizationHandlerStatus.IGNORED
296
+ }
297
+
298
+ if (active.attemptsLeft <= 0) {
299
+ logger.warn(this.prefix('Maximum sign-in attempts exceeded'), activity)
300
+ await context.sendActivity(MessageFactory.text(this.messages.maxAttemptsExceeded(this.maxAttempts)))
301
+ return AuthorizationHandlerStatus.REJECTED
302
+ }
303
+
304
+ if (category === Category.SIGNIN) {
305
+ await storage.write({ ...active, category })
306
+ const status = await this.handleSignInActivities(context)
307
+ if (status !== AuthorizationHandlerStatus.IGNORED) {
308
+ return status
309
+ }
310
+ } else if (active.category === Category.SIGNIN) {
311
+ // This is only for safety in case of unexpected behaviors during the MS Teams sign-in process,
312
+ // e.g., user interrupts the flow by clicking the Consent Cancel button.
313
+ logger.warn(this.prefix('The incoming activity will be revalidated due to a change in the sign-in flow'), activity)
314
+ return AuthorizationHandlerStatus.REVALIDATE
315
+ }
316
+
317
+ const { status, code } = await this.codeVerification(storage, context, active)
318
+ if (status !== AuthorizationHandlerStatus.APPROVED) {
319
+ return status
320
+ }
321
+
322
+ try {
323
+ const result = await this.setToken(storage, context, active, code)
324
+ if (result !== AuthorizationHandlerStatus.APPROVED) {
325
+ await this.sendInvokeResponse(context, { status: 404 })
326
+ return result
327
+ }
328
+
329
+ await this.sendInvokeResponse(context, { status: 200 })
330
+ await this._onSuccess?.(context)
331
+ return result
332
+ } catch (error) {
333
+ await this.sendInvokeResponse(context, { status: 500 })
334
+ if (error instanceof Error) {
335
+ error.message = this.prefix(error.message)
336
+ }
337
+ throw error
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Handles on-behalf-of token acquisition.
343
+ */
344
+ private async handleOBO (token:string, options?: AuthorizationHandlerTokenOptions): Promise<TokenResponse> {
345
+ const oboConnection = options?.connection ?? this._options.obo?.connection
346
+ const oboScopes = options?.scopes && options.scopes.length > 0 ? options.scopes : this._options.obo?.scopes
347
+
348
+ if (!oboScopes || oboScopes.length === 0) {
349
+ return { token }
350
+ }
351
+
352
+ if (!this.isExchangeable(token)) {
353
+ throw new Error(this.prefix('The current token is not exchangeable for an on-behalf-of flow. Ensure the token audience starts with \'api://\'.'))
354
+ }
355
+
356
+ try {
357
+ const provider = oboConnection ? this.settings.connections.getConnection(oboConnection) : this.settings.connections.getDefaultConnection()
358
+ const newToken = await provider.acquireTokenOnBehalfOf(oboScopes, token)
359
+ logger.debug(this.prefix('Successfully acquired on-behalf-of token'), { connection: oboConnection, scopes: oboScopes })
360
+ return { token: newToken }
361
+ } catch (error) {
362
+ logger.error(this.prefix('Failed to exchange on-behalf-of token'), { connection: oboConnection, scopes: oboScopes }, error)
363
+ return { token: undefined }
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Checks if a token is exchangeable for an on-behalf-of flow.
369
+ */
370
+ private isExchangeable (token: string | undefined): boolean {
371
+ if (!token || typeof token !== 'string') {
372
+ return false
373
+ }
374
+ const payload = jwt.decode(token) as JwtPayload
375
+ const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud]
376
+ return audiences.some(aud => typeof aud === 'string' && aud.startsWith('api://'))
377
+ }
378
+
379
+ /**
380
+ * Sets the token from the token response or initiates the sign-in flow.
381
+ */
382
+ private async setToken (storage: HandlerStorage<AzureBotActiveHandler>, context: TurnContext, active?: AzureBotActiveHandler, code?: string): Promise<AuthorizationHandlerStatus> {
383
+ const { activity } = context
384
+
385
+ const userTokenClient = await this.getUserTokenClient(context)
386
+ const { tokenResponse, signInResource } = await userTokenClient.getTokenOrSignInResource(activity.from?.id!, this._options.name!, activity.channelId!, activity.getConversationReference(), activity.relatesTo!, code ?? '')
387
+
388
+ if (!tokenResponse && active) {
389
+ logger.warn(this.prefix('Invalid code entered. Restarting sign-in flow'), activity)
390
+ await context.sendActivity(MessageFactory.text(this.messages.invalidCode(code ?? '')))
391
+ return AuthorizationHandlerStatus.REJECTED
392
+ }
393
+
394
+ if (!tokenResponse) {
395
+ logger.debug(this.prefix('Cannot find token. Sending sign-in card'), activity)
396
+ const oCard = CardFactory.oauthCard(this._options.name!, this._options.title!, this._options.text!, signInResource)
397
+ await context.sendActivity(MessageFactory.attachment(oCard))
398
+ await storage.write({ activity, id: this.id, ...(active ?? {}), attemptsLeft: this.maxAttempts })
399
+ return AuthorizationHandlerStatus.PENDING
400
+ }
401
+
402
+ logger.debug(this.prefix('Successfully acquired token'), activity)
403
+ this.setContext(context, { token: tokenResponse.token })
404
+ return AuthorizationHandlerStatus.APPROVED
405
+ }
406
+
407
+ /**
408
+ * Handles sign-in related activities.
409
+ */
410
+ private async handleSignInActivities (context: TurnContext): Promise<AuthorizationHandlerStatus> {
411
+ const { activity } = context
412
+
413
+ // Ignore signin/verifyState here (handled in codeVerification).
414
+ if (activity.name === 'signin/verifyState') {
415
+ return AuthorizationHandlerStatus.IGNORED
416
+ }
417
+
418
+ const userTokenClient = await this.getUserTokenClient(context)
419
+
420
+ if (activity.name === 'signin/tokenExchange') {
421
+ const tokenExchangeInvokeRequest = activity.value as TokenExchangeInvokeRequest
422
+ const tokenExchangeRequest: TokenExchangeRequest = { token: tokenExchangeInvokeRequest.token }
423
+
424
+ if (!tokenExchangeRequest?.token) {
425
+ const reason = 'The Agent received an InvokeActivity that is missing a TokenExchangeInvokeRequest value. This is required to be sent with the InvokeActivity.'
426
+ await this.sendInvokeResponse<TokenExchangeInvokeResponse>(context, {
427
+ status: 400,
428
+ body: { connectionName: this._options.name!, failureDetail: reason }
429
+ })
430
+ logger.error(this.prefix(reason))
431
+ await this._onFailure?.(context, reason)
432
+ return AuthorizationHandlerStatus.REJECTED
433
+ }
434
+
435
+ if (tokenExchangeInvokeRequest.connectionName !== this._options.name) {
436
+ const reason = `The Agent received an InvokeActivity with a TokenExchangeInvokeRequest for a different connection name ('${tokenExchangeInvokeRequest.connectionName}') than expected ('${this._options.name}').`
437
+ await this.sendInvokeResponse<TokenExchangeInvokeResponse>(context, {
438
+ status: 400,
439
+ body: { id: tokenExchangeInvokeRequest.id, connectionName: this._options.name!, failureDetail: reason }
440
+ })
441
+ logger.error(this.prefix(reason))
442
+ await this._onFailure?.(context, reason)
443
+ return AuthorizationHandlerStatus.REJECTED
444
+ }
445
+
446
+ const { token } = await userTokenClient.exchangeTokenAsync(activity.from?.id!, this._options.name!, activity.channelId!, tokenExchangeRequest)
447
+ if (!token) {
448
+ const reason = 'The MS Teams token service didn\'t send back the exchanged token. Waiting for MS Teams to send another signin/tokenExchange request. After multiple failed attempts, the user will be asked to enter the magic code.'
449
+ await this.sendInvokeResponse<TokenExchangeInvokeResponse>(context, {
450
+ status: 412,
451
+ body: { id: tokenExchangeInvokeRequest.id, connectionName: this._options.name!, failureDetail: reason }
452
+ })
453
+ logger.debug(this.prefix(reason))
454
+ return AuthorizationHandlerStatus.PENDING
455
+ }
456
+
457
+ await this.sendInvokeResponse<TokenExchangeInvokeResponse>(context, {
458
+ status: 200,
459
+ body: { id: tokenExchangeInvokeRequest.id, connectionName: this._options.name! }
460
+ })
461
+ logger.debug(this.prefix('Successfully exchanged token'))
462
+ this.setContext(context, { token })
463
+ await this._onSuccess?.(context)
464
+ return AuthorizationHandlerStatus.APPROVED
465
+ }
466
+
467
+ if (activity.name === 'signin/failure') {
468
+ await this.sendInvokeResponse(context, { status: 200 })
469
+ const reason = 'Failed to sign-in'
470
+ const value = activity.value as SignInFailureValue
471
+ logger.error(this.prefix(reason), value, activity)
472
+ if (this._onFailure) {
473
+ await this._onFailure(context, value.message || reason)
474
+ } else {
475
+ await context.sendActivity(MessageFactory.text(`${reason}. Please try again.`))
476
+ }
477
+ return AuthorizationHandlerStatus.REJECTED
478
+ }
479
+
480
+ logger.error(this.prefix(`Unknown sign-in activity name: ${activity.name}`), activity)
481
+ return AuthorizationHandlerStatus.REJECTED
482
+ }
483
+
484
+ /**
485
+ * Verifies the magic code provided by the user.
486
+ */
487
+ private async codeVerification (storage: HandlerStorage<AzureBotActiveHandler>, context: TurnContext, active?: AzureBotActiveHandler): Promise<{ status: AuthorizationHandlerStatus, code?: string }> {
488
+ if (!active) {
489
+ logger.debug(this.prefix('No active session found. Skipping code verification.'), context.activity)
490
+ return { status: AuthorizationHandlerStatus.IGNORED }
491
+ }
492
+
493
+ const { activity } = context
494
+ let state: string | undefined = activity.text
495
+
496
+ if (activity.name === 'signin/verifyState') {
497
+ logger.debug(this.prefix('Getting code from activity.value'), activity)
498
+ const { state: teamsState } = activity.value as TokenVerifyState
499
+ state = teamsState
500
+ }
501
+
502
+ if (state === 'CancelledByUser') {
503
+ await this.sendInvokeResponse(context, { status: 200 })
504
+ logger.warn(this.prefix('Sign-in process was cancelled by the user'), activity)
505
+ return { status: AuthorizationHandlerStatus.REJECTED }
506
+ }
507
+
508
+ if (!state?.match(/^\d{6}$/)) {
509
+ logger.warn(this.prefix(`Invalid magic code entered. Attempts left: ${active.attemptsLeft}`), activity)
510
+ await context.sendActivity(MessageFactory.text(this.messages.invalidCodeFormat(active.attemptsLeft)))
511
+ await storage.write({ ...active, attemptsLeft: active.attemptsLeft - 1 })
512
+ return { status: AuthorizationHandlerStatus.PENDING }
513
+ }
514
+
515
+ await this.sendInvokeResponse(context, { status: 200 })
516
+ logger.debug(this.prefix('Code verification successful'), activity)
517
+ return { status: AuthorizationHandlerStatus.APPROVED, code: state }
518
+ }
519
+
520
+ private _key = `${AzureBotAuthorization.name}/${this.id}`
521
+
522
+ /**
523
+ * Sets the authorization context in the turn state.
524
+ */
525
+ private setContext (context: TurnContext, data: TokenResponse) {
526
+ return context.turnState.set(this._key, () => data)
527
+ }
528
+
529
+ /**
530
+ * Gets the authorization context from the turn state.
531
+ */
532
+ private getContext (context: TurnContext): TokenResponse {
533
+ const result = context.turnState.get(this._key)
534
+ return result?.() ?? { token: undefined }
535
+ }
536
+
537
+ /**
538
+ * Gets the user token client from the turn context.
539
+ */
540
+ private async getUserTokenClient (context: TurnContext): Promise<UserTokenClient> {
541
+ const userTokenClient = context.turnState.get<UserTokenClient>(context.adapter.UserTokenClientKey)
542
+ if (!userTokenClient) {
543
+ throw new Error(this.prefix('The \'userTokenClient\' is not available in the adapter. Ensure that the adapter supports user token operations.'))
544
+ }
545
+ return userTokenClient
546
+ }
547
+
548
+ /**
549
+ * Sends an InvokeResponse activity if the channel is Microsoft Teams.
550
+ */
551
+ private sendInvokeResponse <T>(context: TurnContext, response: InvokeResponse<T>) {
552
+ if (context.activity.channelId !== Channels.Msteams) {
553
+ return Promise.resolve()
554
+ }
555
+
556
+ return context.sendActivity(Activity.fromObject({
557
+ type: ActivityTypes.InvokeResponse,
558
+ value: response
559
+ }))
560
+ }
561
+
562
+ /**
563
+ * Prefixes a message with the handler ID.
564
+ */
565
+ private prefix (message: string) {
566
+ return `[handler:${this.id}] ${message}`
567
+ }
568
+
569
+ /**
570
+ * Predefined messages with dynamic placeholders.
571
+ */
572
+ private messages = {
573
+ invalidCode: (code: string) => {
574
+ const message = this._options.messages?.invalidCode ?? 'Invalid **{code}** code entered. Please try again with a new sign-in request.'
575
+ return message.replaceAll('{code}', code)
576
+ },
577
+ invalidCodeFormat: (attemptsLeft: number) => {
578
+ const message = this._options.messages?.invalidCodeFormat ?? 'Please enter a valid **6-digit** code format (_e.g. 123456_).\r\n**{attemptsLeft} attempt(s) left...**'
579
+ return message.replaceAll('{attemptsLeft}', attemptsLeft.toString())
580
+ },
581
+ maxAttemptsExceeded: (maxAttempts: number) => {
582
+ const message = this._options.messages?.maxAttemptsExceeded ?? 'You have exceeded the maximum number of sign-in attempts ({maxAttempts}). Please try again with a new sign-in request.'
583
+ return message.replaceAll('{maxAttempts}', maxAttempts.toString())
584
+ },
585
+ }
586
+
587
+ /**
588
+ * Loads the OAuth scopes from the environment variables.
589
+ */
590
+ private loadScopes (value:string | undefined): string[] {
591
+ return value?.split(',').reduce<string[]>((acc, scope) => {
592
+ const trimmed = scope.trim()
593
+ if (trimmed) {
594
+ acc.push(trimmed)
595
+ }
596
+ return acc
597
+ }, []) ?? []
598
+ }
599
+ }
@@ -0,0 +1,2 @@
1
+ export * from './azureBotAuthorization'
2
+ export * from './agenticAuthorization'
@@ -0,0 +1,2 @@
1
+ export * from './authorization'
2
+ export * from './authorizationManager'