@microsoft/agents-hosting 1.1.0-alpha.5 → 1.1.0-alpha.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +10 -6
- package/dist/src/activityWireCompat.d.ts +1 -1
- package/dist/src/activityWireCompat.js +11 -3
- package/dist/src/activityWireCompat.js.map +1 -1
- package/dist/src/agent-client/agentClient.js +7 -3
- package/dist/src/agent-client/agentClient.js.map +1 -1
- package/dist/src/agent-client/agentResponseHandler.js +6 -2
- package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
- package/dist/src/app/agentApplication.d.ts +28 -13
- package/dist/src/app/agentApplication.js +93 -82
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/agentApplicationBuilder.d.ts +2 -2
- package/dist/src/app/agentApplicationBuilder.js.map +1 -1
- package/dist/src/app/agentApplicationOptions.d.ts +9 -2
- package/dist/src/app/appRoute.d.ts +7 -0
- package/dist/src/app/{authorization.d.ts → auth/authorization.d.ts} +33 -139
- package/dist/src/app/auth/authorization.js +188 -0
- package/dist/src/app/auth/authorization.js.map +1 -0
- package/dist/src/app/auth/authorizationManager.d.ts +71 -0
- package/dist/src/app/auth/authorizationManager.js +170 -0
- package/dist/src/app/auth/authorizationManager.js.map +1 -0
- package/dist/src/app/auth/handlerStorage.d.ts +36 -0
- package/dist/src/app/auth/handlerStorage.js +62 -0
- package/dist/src/app/auth/handlerStorage.js.map +1 -0
- package/dist/src/app/auth/handlers/agenticAuthorization.d.ts +93 -0
- package/dist/src/app/auth/handlers/agenticAuthorization.js +134 -0
- package/dist/src/app/auth/handlers/agenticAuthorization.js.map +1 -0
- package/dist/src/app/auth/handlers/azureBotAuthorization.d.ts +222 -0
- package/dist/src/app/auth/handlers/azureBotAuthorization.js +428 -0
- package/dist/src/app/auth/handlers/azureBotAuthorization.js.map +1 -0
- package/dist/src/app/auth/handlers/index.d.ts +2 -0
- package/dist/src/app/auth/handlers/index.js +19 -0
- package/dist/src/app/auth/handlers/index.js.map +1 -0
- package/dist/src/app/auth/index.d.ts +2 -0
- package/dist/src/app/auth/index.js +19 -0
- package/dist/src/app/auth/index.js.map +1 -0
- package/dist/src/app/auth/types.d.ts +104 -0
- package/dist/src/app/auth/types.js +24 -0
- package/dist/src/app/auth/types.js.map +1 -0
- package/dist/src/app/index.d.ts +2 -3
- package/dist/src/app/index.js +2 -3
- package/dist/src/app/index.js.map +1 -1
- package/dist/src/app/routeList.d.ts +1 -1
- package/dist/src/app/routeList.js +22 -5
- package/dist/src/app/routeList.js.map +1 -1
- package/dist/src/app/streaming/streamingResponse.js +2 -1
- package/dist/src/app/streaming/streamingResponse.js.map +1 -1
- package/dist/src/auth/MemoryCache.d.ts +16 -0
- package/dist/src/auth/MemoryCache.js +58 -0
- package/dist/src/auth/MemoryCache.js.map +1 -0
- package/dist/src/auth/authConfiguration.d.ts +44 -2
- package/dist/src/auth/authConfiguration.js +209 -53
- package/dist/src/auth/authConfiguration.js.map +1 -1
- package/dist/src/auth/authConstants.d.ts +11 -0
- package/dist/src/auth/authConstants.js +15 -0
- package/dist/src/auth/authConstants.js.map +1 -0
- package/dist/src/auth/authProvider.d.ts +23 -0
- package/dist/src/auth/connections.d.ts +41 -0
- package/dist/src/auth/connections.js +7 -0
- package/dist/src/auth/connections.js.map +1 -0
- package/dist/src/auth/index.d.ts +2 -0
- package/dist/src/auth/index.js +2 -0
- package/dist/src/auth/index.js.map +1 -1
- package/dist/src/auth/jwt-middleware.js +31 -18
- package/dist/src/auth/jwt-middleware.js.map +1 -1
- package/dist/src/auth/msalConnectionManager.d.ts +64 -0
- package/dist/src/auth/msalConnectionManager.js +148 -0
- package/dist/src/auth/msalConnectionManager.js.map +1 -0
- package/dist/src/auth/msalTokenProvider.d.ts +31 -0
- package/dist/src/auth/msalTokenProvider.js +167 -16
- package/dist/src/auth/msalTokenProvider.js.map +1 -1
- package/dist/src/baseAdapter.d.ts +10 -25
- package/dist/src/baseAdapter.js +2 -15
- package/dist/src/baseAdapter.js.map +1 -1
- package/dist/src/cloudAdapter.d.ts +40 -23
- package/dist/src/cloudAdapter.js +128 -60
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/connector-client/connectorClient.d.ts +15 -0
- package/dist/src/connector-client/connectorClient.js +49 -15
- package/dist/src/connector-client/connectorClient.js.map +1 -1
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/oauth/customUserTokenAPI.d.ts +1 -0
- package/dist/src/oauth/customUserTokenAPI.js +11 -0
- package/dist/src/oauth/customUserTokenAPI.js.map +1 -0
- package/dist/src/oauth/index.d.ts +0 -1
- package/dist/src/oauth/index.js +0 -1
- package/dist/src/oauth/index.js.map +1 -1
- package/dist/src/oauth/userTokenClient.d.ts +30 -13
- package/dist/src/oauth/userTokenClient.js +60 -26
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/dist/src/oauth/userTokenClient.types.d.ts +19 -6
- package/dist/src/turnContext.d.ts +7 -1
- package/dist/src/turnContext.js +11 -4
- package/dist/src/turnContext.js.map +1 -1
- package/package.json +10 -6
- package/src/activityWireCompat.ts +12 -4
- package/src/agent-client/agentClient.ts +9 -3
- package/src/agent-client/agentResponseHandler.ts +5 -2
- package/src/app/agentApplication.ts +98 -77
- package/src/app/agentApplicationBuilder.ts +2 -2
- package/src/app/agentApplicationOptions.ts +10 -2
- package/src/app/appRoute.ts +8 -0
- package/src/app/auth/authorization.ts +252 -0
- package/src/app/auth/authorizationManager.ts +213 -0
- package/src/app/auth/handlerStorage.ts +61 -0
- package/src/app/auth/handlers/agenticAuthorization.ts +182 -0
- package/src/app/auth/handlers/azureBotAuthorization.ts +599 -0
- package/src/app/auth/handlers/index.ts +2 -0
- package/src/app/auth/index.ts +2 -0
- package/src/app/auth/types.ts +111 -0
- package/src/app/index.ts +2 -3
- package/src/app/routeList.ts +24 -5
- package/src/app/streaming/streamingResponse.ts +2 -1
- package/src/auth/MemoryCache.ts +59 -0
- package/src/auth/authConfiguration.ts +245 -52
- package/src/auth/authConstants.ts +11 -0
- package/src/auth/authProvider.ts +31 -0
- package/src/auth/connections.ts +47 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/jwt-middleware.ts +38 -21
- package/src/auth/msalConnectionManager.ts +175 -0
- package/src/auth/msalTokenProvider.ts +209 -9
- package/src/baseAdapter.ts +10 -29
- package/src/cloudAdapter.ts +189 -71
- package/src/connector-client/connectorClient.ts +59 -15
- package/src/index.ts +0 -1
- package/src/oauth/customUserTokenAPI.ts +5 -0
- package/src/oauth/index.ts +0 -1
- package/src/oauth/userTokenClient.ts +74 -22
- package/src/oauth/userTokenClient.types.ts +20 -8
- package/src/turnContext.ts +16 -5
- package/dist/src/app/authorization.js +0 -387
- package/dist/src/app/authorization.js.map +0 -1
- package/dist/src/claimsIdentity.d.ts +0 -35
- package/dist/src/claimsIdentity.js +0 -43
- package/dist/src/claimsIdentity.js.map +0 -1
- package/dist/src/oauth/oAuthFlow.d.ts +0 -119
- package/dist/src/oauth/oAuthFlow.js +0 -316
- package/dist/src/oauth/oAuthFlow.js.map +0 -1
- package/src/app/authorization.ts +0 -432
- package/src/claimsIdentity.ts +0 -47
- 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
|
+
}
|