@microsoft/agents-hosting 0.5.4-ga4d0401645 → 0.5.16-g6bdf69cc43
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/src/app/adaptiveCards/adaptiveCardsActions.js +4 -4
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
- package/dist/src/app/agentApplication.d.ts +186 -20
- package/dist/src/app/agentApplication.js +235 -33
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/agentApplicationBuilder.d.ts +1 -1
- package/dist/src/app/agentApplicationOptions.d.ts +1 -1
- package/dist/src/app/appRoute.d.ts +5 -0
- package/dist/src/app/authorization.d.ts +293 -0
- package/dist/src/app/authorization.js +375 -0
- package/dist/src/app/authorization.js.map +1 -0
- package/dist/src/app/index.d.ts +1 -1
- package/dist/src/app/index.js +1 -1
- package/dist/src/app/index.js.map +1 -1
- package/dist/src/app/streaming/citation.d.ts +25 -0
- package/dist/src/app/streaming/{AIEntity.js → citation.js} +1 -1
- package/dist/src/app/streaming/citation.js.map +1 -0
- package/dist/src/app/streaming/{utilities.d.ts → citationUtil.d.ts} +3 -3
- package/dist/src/app/streaming/{utilities.js → citationUtil.js} +5 -36
- package/dist/src/app/streaming/citationUtil.js.map +1 -0
- package/dist/src/app/streaming/streamingResponse.d.ts +3 -4
- package/dist/src/app/streaming/streamingResponse.js +24 -22
- package/dist/src/app/streaming/streamingResponse.js.map +1 -1
- package/dist/src/auth/index.d.ts +1 -0
- package/dist/src/auth/index.js +1 -0
- package/dist/src/auth/index.js.map +1 -1
- package/dist/src/auth/jwt-middleware.js.map +1 -1
- package/dist/src/auth/msalTokenCredential.d.ts +10 -0
- package/dist/src/auth/msalTokenCredential.js +19 -0
- package/dist/src/auth/msalTokenCredential.js.map +1 -0
- package/dist/src/auth/msalTokenProvider.d.ts +1 -0
- package/dist/src/auth/msalTokenProvider.js +15 -0
- package/dist/src/auth/msalTokenProvider.js.map +1 -1
- package/dist/src/cards/cardFactory.d.ts +2 -2
- package/dist/src/cards/cardFactory.js.map +1 -1
- package/dist/src/oauth/index.d.ts +1 -4
- package/dist/src/oauth/index.js +1 -4
- package/dist/src/oauth/index.js.map +1 -1
- package/dist/src/oauth/oAuthFlow.d.ts +62 -19
- package/dist/src/oauth/oAuthFlow.js +193 -67
- package/dist/src/oauth/oAuthFlow.js.map +1 -1
- package/dist/src/oauth/userTokenClient.d.ts +37 -7
- package/dist/src/oauth/userTokenClient.js +56 -15
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/dist/src/oauth/userTokenClient.types.d.ts +147 -0
- package/dist/src/oauth/{oAuthCard.js → userTokenClient.types.js} +1 -1
- package/dist/src/oauth/userTokenClient.types.js.map +1 -0
- package/package.json +5 -4
- package/src/app/adaptiveCards/adaptiveCardsActions.ts +4 -4
- package/src/app/agentApplication.ts +248 -33
- package/src/app/agentApplicationBuilder.ts +1 -1
- package/src/app/agentApplicationOptions.ts +1 -1
- package/src/app/appRoute.ts +6 -0
- package/src/app/authorization.ts +418 -0
- package/src/app/index.ts +1 -1
- package/src/app/streaming/citation.ts +29 -0
- package/src/app/streaming/{utilities.ts → citationUtil.ts} +3 -35
- package/src/app/streaming/streamingResponse.ts +28 -27
- package/src/auth/index.ts +1 -0
- package/src/auth/jwt-middleware.ts +1 -1
- package/src/auth/msalTokenCredential.ts +14 -0
- package/src/auth/msalTokenProvider.ts +17 -1
- package/src/cards/cardFactory.ts +2 -3
- package/src/oauth/index.ts +1 -4
- package/src/oauth/oAuthFlow.ts +226 -70
- package/src/oauth/userTokenClient.ts +62 -19
- package/src/oauth/userTokenClient.types.ts +173 -0
- package/dist/src/app/oauth/authorization.d.ts +0 -87
- package/dist/src/app/oauth/authorization.js +0 -135
- package/dist/src/app/oauth/authorization.js.map +0 -1
- package/dist/src/app/streaming/AIEntity.d.ts +0 -36
- package/dist/src/app/streaming/AIEntity.js.map +0 -1
- package/dist/src/app/streaming/actionCall.d.ts +0 -33
- package/dist/src/app/streaming/actionCall.js +0 -7
- package/dist/src/app/streaming/actionCall.js.map +0 -1
- package/dist/src/app/streaming/clientCitation.d.ts +0 -68
- package/dist/src/app/streaming/clientCitation.js +0 -10
- package/dist/src/app/streaming/clientCitation.js.map +0 -1
- package/dist/src/app/streaming/message.d.ts +0 -106
- package/dist/src/app/streaming/message.js +0 -7
- package/dist/src/app/streaming/message.js.map +0 -1
- package/dist/src/app/streaming/sensitivityUsageInfo.d.ts +0 -40
- package/dist/src/app/streaming/sensitivityUsageInfo.js +0 -3
- package/dist/src/app/streaming/sensitivityUsageInfo.js.map +0 -1
- package/dist/src/app/streaming/utilities.js.map +0 -1
- package/dist/src/oauth/oAuthCard.d.ts +0 -27
- package/dist/src/oauth/oAuthCard.js.map +0 -1
- package/dist/src/oauth/signingResource.d.ts +0 -43
- package/dist/src/oauth/signingResource.js +0 -5
- package/dist/src/oauth/signingResource.js.map +0 -1
- package/dist/src/oauth/tokenExchangeRequest.d.ts +0 -17
- package/dist/src/oauth/tokenExchangeRequest.js +0 -5
- package/dist/src/oauth/tokenExchangeRequest.js.map +0 -1
- package/dist/src/oauth/tokenResponse.d.ts +0 -29
- package/dist/src/oauth/tokenResponse.js +0 -25
- package/dist/src/oauth/tokenResponse.js.map +0 -1
- package/src/app/oauth/authorization.ts +0 -162
- package/src/app/streaming/AIEntity.ts +0 -44
- package/src/app/streaming/actionCall.ts +0 -37
- package/src/app/streaming/clientCitation.ts +0 -102
- package/src/app/streaming/message.ts +0 -125
- package/src/app/streaming/sensitivityUsageInfo.ts +0 -48
- package/src/oauth/oAuthCard.ts +0 -30
- package/src/oauth/signingResource.ts +0 -48
- package/src/oauth/tokenExchangeRequest.ts +0 -20
- package/src/oauth/tokenResponse.ts +0 -43
|
@@ -25,7 +25,7 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
25
25
|
* @param scope The scope for the token.
|
|
26
26
|
* @returns A promise that resolves to the access token.
|
|
27
27
|
*/
|
|
28
|
-
async getAccessToken (authConfig: AuthConfiguration, scope: string): Promise<string> {
|
|
28
|
+
public async getAccessToken (authConfig: AuthConfiguration, scope: string): Promise<string> {
|
|
29
29
|
if (!authConfig.clientId && process.env.NODE_ENV !== 'production') {
|
|
30
30
|
return ''
|
|
31
31
|
}
|
|
@@ -51,6 +51,22 @@ export class MsalTokenProvider implements AuthProvider {
|
|
|
51
51
|
return token
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
public async acquireTokenOnBehalfOf (authConfig: AuthConfiguration, scopes: string[], oboAssertion: string): Promise<string> {
|
|
55
|
+
const cca = new ConfidentialClientApplication({
|
|
56
|
+
auth: {
|
|
57
|
+
clientId: authConfig.clientId as string,
|
|
58
|
+
authority: `https://login.microsoftonline.com/${authConfig.tenantId || 'botframework.com'}`,
|
|
59
|
+
clientSecret: authConfig.clientSecret
|
|
60
|
+
},
|
|
61
|
+
system: this.sysOptions
|
|
62
|
+
})
|
|
63
|
+
const token = await cca.acquireTokenOnBehalfOf({
|
|
64
|
+
oboAssertion,
|
|
65
|
+
scopes
|
|
66
|
+
})
|
|
67
|
+
return token?.accessToken as string
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
private readonly sysOptions: NodeSystemOptions = {
|
|
55
71
|
loggerOptions: {
|
|
56
72
|
logLevel: LogLevel.Trace,
|
package/src/cards/cardFactory.ts
CHANGED
|
@@ -13,8 +13,7 @@ import { O365ConnectorCard } from './o365ConnectorCard'
|
|
|
13
13
|
import { ThumbnailCard } from './thumbnailCard'
|
|
14
14
|
import { VideoCard } from './videoCard'
|
|
15
15
|
import { CardImage } from './cardImage'
|
|
16
|
-
import { OAuthCard } from '../oauth/
|
|
17
|
-
import { SigningResource } from '../oauth/signingResource'
|
|
16
|
+
import { OAuthCard, SignInResource } from '../oauth/userTokenClient.types'
|
|
18
17
|
import { SigninCard } from './signinCard'
|
|
19
18
|
|
|
20
19
|
/**
|
|
@@ -218,7 +217,7 @@ export class CardFactory {
|
|
|
218
217
|
* @param signingResource The signing resource.
|
|
219
218
|
* @returns The OAuth card attachment.
|
|
220
219
|
*/
|
|
221
|
-
static oauthCard (connectionName: string, title: string, text: string, signingResource:
|
|
220
|
+
static oauthCard (connectionName: string, title: string, text: string, signingResource: SignInResource) : Attachment {
|
|
222
221
|
const card: Partial<OAuthCard> = {
|
|
223
222
|
buttons: [{
|
|
224
223
|
type: ActionTypes.Signin,
|
package/src/oauth/index.ts
CHANGED
package/src/oauth/oAuthFlow.ts
CHANGED
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { debug } from './../logger'
|
|
4
|
-
import { ActivityTypes, Attachment } from '@microsoft/agents-activity'
|
|
4
|
+
import { Activity, ActivityTypes, Attachment } from '@microsoft/agents-activity'
|
|
5
5
|
import {
|
|
6
6
|
CardFactory,
|
|
7
|
-
AgentStatePropertyAccessor,
|
|
8
|
-
UserState,
|
|
9
7
|
TurnContext,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
UserTokenClient
|
|
8
|
+
Storage,
|
|
9
|
+
MessageFactory
|
|
13
10
|
} from '../'
|
|
14
|
-
import {
|
|
11
|
+
import { UserTokenClient } from './userTokenClient'
|
|
12
|
+
import { TokenExchangeRequest, TokenResponse } from './userTokenClient.types'
|
|
15
13
|
|
|
16
14
|
const logger = debug('agents:oauth-flow')
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Represents the state of the OAuth flow.
|
|
18
|
+
* @interface FlowState
|
|
19
|
+
*/
|
|
20
|
+
export interface FlowState {
|
|
21
|
+
/** Indicates whether the OAuth flow has been started */
|
|
22
|
+
flowStarted: boolean | undefined,
|
|
23
|
+
/** Timestamp when the OAuth flow expires (in milliseconds since epoch) */
|
|
24
|
+
flowExpires: number | undefined,
|
|
25
|
+
/** The absolute OAuth connection name used for the flow, null if not set */
|
|
26
|
+
absOauthConnectionName: string
|
|
27
|
+
/** Optional activity to continue the flow with, used for multi-turn scenarios */
|
|
28
|
+
continuationActivity?: Activity | null
|
|
29
|
+
|
|
30
|
+
eTag?: string // Optional ETag for optimistic concurrency control
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
interface TokenVerifyState {
|
|
24
34
|
state: string
|
|
25
35
|
}
|
|
36
|
+
|
|
37
|
+
interface CachedToken {
|
|
38
|
+
token: TokenResponse
|
|
39
|
+
expiresAt: number
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
/**
|
|
27
43
|
* Manages the OAuth flow
|
|
28
44
|
*/
|
|
@@ -35,17 +51,17 @@ export class OAuthFlow {
|
|
|
35
51
|
/**
|
|
36
52
|
* The current state of the OAuth flow.
|
|
37
53
|
*/
|
|
38
|
-
state: FlowState
|
|
54
|
+
state: FlowState
|
|
39
55
|
|
|
40
56
|
/**
|
|
41
|
-
* The
|
|
57
|
+
* The ID of the token exchange request, used to deduplicate requests.
|
|
42
58
|
*/
|
|
43
|
-
|
|
59
|
+
tokenExchangeId: string | null = null
|
|
44
60
|
|
|
45
61
|
/**
|
|
46
|
-
*
|
|
62
|
+
* In-memory cache for tokens with expiration.
|
|
47
63
|
*/
|
|
48
|
-
|
|
64
|
+
private tokenCache: Map<string, CachedToken> = new Map()
|
|
49
65
|
|
|
50
66
|
/**
|
|
51
67
|
* The name of the OAuth connection.
|
|
@@ -64,11 +80,14 @@ export class OAuthFlow {
|
|
|
64
80
|
|
|
65
81
|
/**
|
|
66
82
|
* Creates a new instance of OAuthFlow.
|
|
67
|
-
* @param
|
|
83
|
+
* @param storage The storage provider for persisting flow state.
|
|
84
|
+
* @param absOauthConnectionName The absolute OAuth connection name.
|
|
85
|
+
* @param tokenClient Optional user token client. If not provided, will be initialized automatically.
|
|
86
|
+
* @param cardTitle Optional title for the OAuth card. Defaults to 'Sign in'.
|
|
87
|
+
* @param cardText Optional text for the OAuth card. Defaults to 'login'.
|
|
68
88
|
*/
|
|
69
|
-
constructor (
|
|
70
|
-
this.state =
|
|
71
|
-
this.flowStateAccessor = userState.createProperty('flowState')
|
|
89
|
+
constructor (private storage: Storage, absOauthConnectionName: string, tokenClient?: UserTokenClient, cardTitle?: string, cardText?: string) {
|
|
90
|
+
this.state = { flowStarted: undefined, flowExpires: undefined, absOauthConnectionName }
|
|
72
91
|
this.absOauthConnectionName = absOauthConnectionName
|
|
73
92
|
this.userTokenClient = tokenClient ?? null!
|
|
74
93
|
this.cardTitle = cardTitle ?? this.cardTitle
|
|
@@ -76,77 +95,138 @@ export class OAuthFlow {
|
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
/**
|
|
79
|
-
* Retrieves the user token from the user token service.
|
|
98
|
+
* Retrieves the user token from the user token service with in-memory caching for 10 minutes.
|
|
80
99
|
* @param context The turn context containing the activity information.
|
|
81
100
|
* @returns A promise that resolves to the user token response.
|
|
82
101
|
* @throws Will throw an error if the channelId or from properties are not set in the activity.
|
|
83
102
|
*/
|
|
84
103
|
public async getUserToken (context: TurnContext): Promise<TokenResponse> {
|
|
85
104
|
await this.initializeTokenClient(context)
|
|
86
|
-
logger.info('Get token from user token service')
|
|
87
105
|
const activity = context.activity
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
} else {
|
|
106
|
+
|
|
107
|
+
if (!activity.channelId || !activity.from || !activity.from.id) {
|
|
91
108
|
throw new Error('UserTokenService requires channelId and from to be set')
|
|
92
109
|
}
|
|
110
|
+
|
|
111
|
+
const cacheKey = this.getCacheKey(context)
|
|
112
|
+
|
|
113
|
+
const cachedEntry = this.tokenCache.get(cacheKey)
|
|
114
|
+
if (cachedEntry && Date.now() < cachedEntry.expiresAt) {
|
|
115
|
+
logger.info(`Returning cached token for user with cache key: ${cacheKey}`)
|
|
116
|
+
return cachedEntry.token
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
logger.info('Get token from user token service')
|
|
120
|
+
const tokenResponse = await this.userTokenClient.getUserToken(this.absOauthConnectionName, activity.channelId, activity.from.id)
|
|
121
|
+
|
|
122
|
+
// Cache the token if it's valid (has a token value)
|
|
123
|
+
if (tokenResponse && tokenResponse.token) {
|
|
124
|
+
const cacheExpiry = Date.now() + (10 * 60 * 1000) // 10 minutes from now
|
|
125
|
+
this.tokenCache.set(cacheKey, {
|
|
126
|
+
token: tokenResponse,
|
|
127
|
+
expiresAt: cacheExpiry
|
|
128
|
+
})
|
|
129
|
+
logger.info('Token cached for 10 minutes')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return tokenResponse
|
|
93
133
|
}
|
|
94
134
|
|
|
95
135
|
/**
|
|
96
136
|
* Begins the OAuth flow.
|
|
97
137
|
* @param context The turn context.
|
|
98
|
-
* @returns A promise that resolves to the user token.
|
|
138
|
+
* @returns A promise that resolves to the user token if available, or undefined if OAuth flow needs to be started.
|
|
99
139
|
*/
|
|
100
|
-
public async beginFlow (context: TurnContext): Promise<TokenResponse> {
|
|
101
|
-
this.state = await this.
|
|
140
|
+
public async beginFlow (context: TurnContext): Promise<TokenResponse | undefined> {
|
|
141
|
+
this.state = await this.getFlowState(context)
|
|
102
142
|
if (this.absOauthConnectionName === '') {
|
|
103
143
|
throw new Error('connectionName is not set')
|
|
104
144
|
}
|
|
105
145
|
logger.info('Starting OAuth flow for connectionName:', this.absOauthConnectionName)
|
|
106
146
|
await this.initializeTokenClient(context)
|
|
107
147
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
const act = context.activity
|
|
149
|
+
|
|
150
|
+
// Check cache first before starting OAuth flow
|
|
151
|
+
if (act.channelId && act.from && act.from.id) {
|
|
152
|
+
const cacheKey = this.getCacheKey(context)
|
|
153
|
+
const cachedEntry = this.tokenCache.get(cacheKey)
|
|
154
|
+
if (cachedEntry && Date.now() < cachedEntry.expiresAt) {
|
|
155
|
+
logger.info(`Returning cached token for user in beginFlow with cache key: ${cacheKey}`)
|
|
156
|
+
return cachedEntry.token
|
|
157
|
+
}
|
|
115
158
|
}
|
|
116
159
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
160
|
+
const output = await this.userTokenClient.getTokenOrSignInResource(act.from?.id!, this.absOauthConnectionName, act.channelId!, act.getConversationReference(), act.relatesTo!, undefined!)
|
|
161
|
+
if (output && output.tokenResponse) {
|
|
162
|
+
// Cache the token if it's valid
|
|
163
|
+
if (act.channelId && act.from && act.from.id) {
|
|
164
|
+
const cacheKey = this.getCacheKey(context)
|
|
165
|
+
const cacheExpiry = Date.now() + (10 * 60 * 1000) // 10 minutes from now
|
|
166
|
+
this.tokenCache.set(cacheKey, {
|
|
167
|
+
token: output.tokenResponse,
|
|
168
|
+
expiresAt: cacheExpiry
|
|
169
|
+
})
|
|
170
|
+
logger.info('Token cached for 10 minutes in beginFlow')
|
|
171
|
+
this.state = { flowStarted: false, flowExpires: 0, absOauthConnectionName: this.absOauthConnectionName }
|
|
172
|
+
}
|
|
173
|
+
logger.info('Token retrieved successfully')
|
|
174
|
+
return output.tokenResponse
|
|
128
175
|
}
|
|
176
|
+
const oCard: Attachment = CardFactory.oauthCard(this.absOauthConnectionName, this.cardTitle, this.cardText, output.signInResource)
|
|
177
|
+
await context.sendActivity(MessageFactory.attachment(oCard))
|
|
178
|
+
this.state = { flowStarted: true, flowExpires: Date.now() + 60 * 5 * 1000, absOauthConnectionName: this.absOauthConnectionName }
|
|
179
|
+
await this.storage.write({ [this.getFlowStateKey(context)]: this.state })
|
|
180
|
+
logger.info('OAuth card sent, flow started')
|
|
181
|
+
return undefined
|
|
129
182
|
}
|
|
130
183
|
|
|
131
184
|
/**
|
|
132
185
|
* Continues the OAuth flow.
|
|
133
186
|
* @param context The turn context.
|
|
134
|
-
* @returns A promise that resolves to the user token.
|
|
187
|
+
* @returns A promise that resolves to the user token response.
|
|
135
188
|
*/
|
|
136
189
|
public async continueFlow (context: TurnContext): Promise<TokenResponse> {
|
|
137
|
-
this.state = await this.
|
|
190
|
+
this.state = await this.getFlowState(context)
|
|
138
191
|
await this.initializeTokenClient(context)
|
|
139
|
-
if (this.state?.flowExpires !== 0 && Date.now() > this.state
|
|
192
|
+
if (this.state?.flowExpires !== 0 && Date.now() > this.state?.flowExpires!) {
|
|
140
193
|
logger.warn('Flow expired')
|
|
141
|
-
this.state!.flowStarted = false
|
|
142
194
|
await context.sendActivity(MessageFactory.text('Sign-in session expired. Please try again.'))
|
|
143
|
-
|
|
195
|
+
this.state!.flowStarted = false
|
|
196
|
+
return { token: undefined }
|
|
144
197
|
}
|
|
145
198
|
const contFlowActivity = context.activity
|
|
146
199
|
if (contFlowActivity.type === ActivityTypes.Message) {
|
|
147
200
|
const magicCode = contFlowActivity.text as string
|
|
148
|
-
|
|
149
|
-
|
|
201
|
+
if (magicCode.match(/^\d{6}$/)) {
|
|
202
|
+
const result = await this.userTokenClient?.getUserToken(this.absOauthConnectionName, contFlowActivity.channelId!, contFlowActivity.from?.id!, magicCode)!
|
|
203
|
+
if (result && result.token) {
|
|
204
|
+
// Cache the token if it's valid
|
|
205
|
+
if (contFlowActivity.channelId && contFlowActivity.from && contFlowActivity.from.id) {
|
|
206
|
+
const cacheKey = this.getCacheKey(context)
|
|
207
|
+
const cacheExpiry = Date.now() + (10 * 60 * 1000) // 10 minutes from now
|
|
208
|
+
this.tokenCache.set(cacheKey, {
|
|
209
|
+
token: result,
|
|
210
|
+
expiresAt: cacheExpiry
|
|
211
|
+
})
|
|
212
|
+
logger.info('Token cached for 10 minutes in continueFlow (magic code)')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await this.storage.delete([this.getFlowStateKey(context)])
|
|
216
|
+
logger.info('Token retrieved successfully')
|
|
217
|
+
return result
|
|
218
|
+
} else {
|
|
219
|
+
// await context.sendActivity(MessageFactory.text('Invalid code. Please try again.'))
|
|
220
|
+
logger.warn('Invalid magic code provided')
|
|
221
|
+
this.state = { flowStarted: true, flowExpires: Date.now() + 30000, absOauthConnectionName: this.absOauthConnectionName }
|
|
222
|
+
await this.storage.write({ [this.getFlowStateKey(context)]: this.state })
|
|
223
|
+
return { token: undefined }
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
logger.warn('Invalid magic code format')
|
|
227
|
+
await context.sendActivity(MessageFactory.text('Invalid code format. Please enter a 6-digit code.'))
|
|
228
|
+
return { token: undefined }
|
|
229
|
+
}
|
|
150
230
|
}
|
|
151
231
|
|
|
152
232
|
if (contFlowActivity.type === ActivityTypes.Invoke && contFlowActivity.name === 'signin/verifyState') {
|
|
@@ -154,6 +234,16 @@ export class OAuthFlow {
|
|
|
154
234
|
const tokenVerifyState = contFlowActivity.value as TokenVerifyState
|
|
155
235
|
const magicCode = tokenVerifyState.state
|
|
156
236
|
const result = await this.userTokenClient?.getUserToken(this.absOauthConnectionName, contFlowActivity.channelId!, contFlowActivity.from?.id!, magicCode)!
|
|
237
|
+
// Cache the token if it's valid
|
|
238
|
+
if (result && result.token && contFlowActivity.channelId && contFlowActivity.from && contFlowActivity.from.id) {
|
|
239
|
+
const cacheKey = this.getCacheKey(context)
|
|
240
|
+
const cacheExpiry = Date.now() + (10 * 60 * 1000) // 10 minutes from now
|
|
241
|
+
this.tokenCache.set(cacheKey, {
|
|
242
|
+
token: result,
|
|
243
|
+
expiresAt: cacheExpiry
|
|
244
|
+
})
|
|
245
|
+
logger.info('Token cached for 10 minutes in continueFlow (verifyState)')
|
|
246
|
+
}
|
|
157
247
|
return result
|
|
158
248
|
}
|
|
159
249
|
|
|
@@ -161,22 +251,34 @@ export class OAuthFlow {
|
|
|
161
251
|
logger.info('Continuing OAuth flow with tokenExchange')
|
|
162
252
|
const tokenExchangeRequest = contFlowActivity.value as TokenExchangeRequest
|
|
163
253
|
if (this.tokenExchangeId === tokenExchangeRequest.id) { // dedupe
|
|
164
|
-
|
|
254
|
+
logger.debug('Token exchange request already processed, skipping')
|
|
255
|
+
return { token: undefined }
|
|
165
256
|
}
|
|
166
257
|
this.tokenExchangeId = tokenExchangeRequest.id!
|
|
167
258
|
const userTokenResp = await this.userTokenClient?.exchangeTokenAsync(contFlowActivity.from?.id!, this.absOauthConnectionName, contFlowActivity.channelId!, tokenExchangeRequest)
|
|
168
|
-
if (userTokenResp
|
|
259
|
+
if (userTokenResp && userTokenResp.token) {
|
|
260
|
+
// Cache the token if it's valid
|
|
261
|
+
if (contFlowActivity.channelId && contFlowActivity.from && contFlowActivity.from.id) {
|
|
262
|
+
const cacheKey = this.getCacheKey(context)
|
|
263
|
+
const cacheExpiry = Date.now() + (10 * 60 * 1000) // 10 minutes from now
|
|
264
|
+
this.tokenCache.set(cacheKey, {
|
|
265
|
+
token: userTokenResp,
|
|
266
|
+
expiresAt: cacheExpiry
|
|
267
|
+
})
|
|
268
|
+
logger.info('Token cached for 10 minutes in continueFlow (tokenExchange)')
|
|
269
|
+
}
|
|
270
|
+
|
|
169
271
|
logger.info('Token exchanged')
|
|
170
272
|
this.state!.flowStarted = false
|
|
171
|
-
await this.
|
|
273
|
+
await this.storage.write({ [this.getFlowStateKey(context)]: this.state })
|
|
172
274
|
return userTokenResp
|
|
173
275
|
} else {
|
|
174
276
|
logger.warn('Token exchange failed')
|
|
175
277
|
this.state!.flowStarted = true
|
|
176
|
-
return {
|
|
278
|
+
return { token: undefined }
|
|
177
279
|
}
|
|
178
280
|
}
|
|
179
|
-
return {
|
|
281
|
+
return { token: undefined }
|
|
180
282
|
}
|
|
181
283
|
|
|
182
284
|
/**
|
|
@@ -185,32 +287,86 @@ export class OAuthFlow {
|
|
|
185
287
|
* @returns A promise that resolves when the sign-out operation is complete.
|
|
186
288
|
*/
|
|
187
289
|
public async signOut (context: TurnContext): Promise<void> {
|
|
188
|
-
this.state = await this.getUserState(context)
|
|
189
290
|
await this.initializeTokenClient(context)
|
|
291
|
+
|
|
292
|
+
// Clear cached token for this user
|
|
293
|
+
const activity = context.activity
|
|
294
|
+
if (activity.channelId && activity.from && activity.from.id) {
|
|
295
|
+
const cacheKey = this.getCacheKey(context)
|
|
296
|
+
this.tokenCache.delete(cacheKey)
|
|
297
|
+
logger.info('Cached token cleared for user')
|
|
298
|
+
}
|
|
299
|
+
|
|
190
300
|
await this.userTokenClient?.signOut(context.activity.from?.id as string, this.absOauthConnectionName, context.activity.channelId as string)
|
|
191
|
-
this.state
|
|
192
|
-
await this.
|
|
193
|
-
logger.info('User signed out successfully')
|
|
301
|
+
this.state = { flowStarted: false, flowExpires: 0, absOauthConnectionName: this.absOauthConnectionName }
|
|
302
|
+
await this.storage.delete([this.getFlowStateKey(context)])
|
|
303
|
+
logger.info('User signed out successfully from connection:', this.absOauthConnectionName)
|
|
194
304
|
}
|
|
195
305
|
|
|
196
306
|
/**
|
|
197
|
-
* Gets the user state.
|
|
307
|
+
* Gets the user state for the OAuth flow.
|
|
198
308
|
* @param context The turn context.
|
|
199
|
-
* @returns A promise that resolves to the
|
|
309
|
+
* @returns A promise that resolves to the flow state.
|
|
200
310
|
*/
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
311
|
+
public async getFlowState (context: TurnContext) : Promise<FlowState> {
|
|
312
|
+
const key = this.getFlowStateKey(context)
|
|
313
|
+
const data = await this.storage.read([key])
|
|
314
|
+
const flowState: FlowState = data[key] // ?? { flowStarted: false, flowExpires: 0 }
|
|
315
|
+
return flowState
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Sets the flow state for the OAuth flow.
|
|
320
|
+
* @param context The turn context.
|
|
321
|
+
* @param flowState The flow state to set.
|
|
322
|
+
* @returns A promise that resolves when the flow state is set.
|
|
323
|
+
*/
|
|
324
|
+
public async setFlowState (context: TurnContext, flowState: FlowState) : Promise<void> {
|
|
325
|
+
const key = this.getFlowStateKey(context)
|
|
326
|
+
await this.storage.write({ [key]: flowState })
|
|
327
|
+
this.state = flowState
|
|
328
|
+
logger.info('Flow state set:', flowState)
|
|
207
329
|
}
|
|
208
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Initializes the user token client if not already initialized.
|
|
333
|
+
* @param context The turn context used to get authentication credentials.
|
|
334
|
+
*/
|
|
209
335
|
private async initializeTokenClient (context: TurnContext) {
|
|
210
336
|
if (this.userTokenClient === undefined || this.userTokenClient === null) {
|
|
211
337
|
const scope = 'https://api.botframework.com'
|
|
212
338
|
const accessToken = await context.adapter.authProvider.getAccessToken(context.adapter.authConfig, scope)
|
|
213
|
-
this.userTokenClient = new UserTokenClient(accessToken)
|
|
339
|
+
this.userTokenClient = new UserTokenClient(accessToken, context.adapter.authConfig.clientId!)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Generates a cache key for storing user tokens.
|
|
345
|
+
* @param context The turn context containing activity information.
|
|
346
|
+
* @returns The cache key string in format: channelId_userId_connectionName.
|
|
347
|
+
* @throws Will throw an error if required activity properties are missing.
|
|
348
|
+
*/
|
|
349
|
+
private getCacheKey (context: TurnContext): string {
|
|
350
|
+
const activity = context.activity
|
|
351
|
+
if (!activity.channelId || !activity.from || !activity.from.id) {
|
|
352
|
+
throw new Error('ChannelId and from.id must be set in the activity for cache key generation')
|
|
353
|
+
}
|
|
354
|
+
return `${activity.channelId}_${activity.from.id}_${this.absOauthConnectionName}`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Generates a storage key for persisting OAuth flow state.
|
|
359
|
+
* @param context The turn context containing activity information.
|
|
360
|
+
* @returns The storage key string in format: oauth/channelId/conversationId/userId/flowState.
|
|
361
|
+
* @throws Will throw an error if required activity properties are missing.
|
|
362
|
+
*/
|
|
363
|
+
private getFlowStateKey (context: TurnContext): string {
|
|
364
|
+
const channelId = context.activity.channelId
|
|
365
|
+
const conversationId = context.activity.conversation?.id
|
|
366
|
+
const userId = context.activity.from?.id
|
|
367
|
+
if (!channelId || !conversationId || !userId) {
|
|
368
|
+
throw new Error('ChannelId, conversationId, and userId must be set in the activity')
|
|
214
369
|
}
|
|
370
|
+
return `oauth/${channelId}/${userId}/${this.absOauthConnectionName}/flowState`
|
|
215
371
|
}
|
|
216
372
|
}
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
|
|
4
4
|
import axios, { AxiosInstance } from 'axios'
|
|
5
|
-
import { SigningResource } from './signingResource'
|
|
6
5
|
import { ConversationReference } from '@microsoft/agents-activity'
|
|
7
6
|
import { debug } from '../logger'
|
|
8
|
-
import { TokenExchangeRequest } from './tokenExchangeRequest'
|
|
9
7
|
import { normalizeTokenExchangeState } from '../activityWireCompat'
|
|
10
|
-
import {
|
|
8
|
+
import { AadResourceUrls, SignInResource, TokenExchangeRequest, TokenOrSinginResourceResponse, TokenResponse, TokenStatus } from './userTokenClient.types'
|
|
11
9
|
import { getProductInfo } from '../getProductInfo'
|
|
12
10
|
|
|
13
11
|
const logger = debug('agents:user-token-client')
|
|
@@ -17,12 +15,14 @@ const logger = debug('agents:user-token-client')
|
|
|
17
15
|
*/
|
|
18
16
|
export class UserTokenClient {
|
|
19
17
|
client: AxiosInstance
|
|
20
|
-
|
|
18
|
+
msAppId: string
|
|
21
19
|
/**
|
|
22
20
|
* Creates a new instance of UserTokenClient.
|
|
23
21
|
* @param token The token to use for authentication.
|
|
22
|
+
* @param msAppId The Microsoft application ID.
|
|
24
23
|
*/
|
|
25
|
-
constructor (token: string) {
|
|
24
|
+
constructor (token: string, appId: string) {
|
|
25
|
+
this.msAppId = appId
|
|
26
26
|
const baseURL = 'https://api.botframework.com'
|
|
27
27
|
const axiosInstance = axios.create({
|
|
28
28
|
baseURL,
|
|
@@ -47,15 +47,12 @@ export class UserTokenClient {
|
|
|
47
47
|
try {
|
|
48
48
|
const params = { connectionName, channelId, userId, code }
|
|
49
49
|
const response = await this.client.get('/api/usertoken/GetToken', { params })
|
|
50
|
-
return
|
|
50
|
+
return response.data as TokenResponse
|
|
51
51
|
} catch (error: any) {
|
|
52
52
|
if (error.response?.status !== 404) {
|
|
53
53
|
logger.error(error)
|
|
54
54
|
}
|
|
55
|
-
return {
|
|
56
|
-
status: TokenRequestStatus.Failed,
|
|
57
|
-
token: undefined
|
|
58
|
-
}
|
|
55
|
+
return { token: undefined }
|
|
59
56
|
}
|
|
60
57
|
}
|
|
61
58
|
|
|
@@ -81,24 +78,24 @@ export class UserTokenClient {
|
|
|
81
78
|
|
|
82
79
|
/**
|
|
83
80
|
* Gets the sign-in resource.
|
|
84
|
-
* @param
|
|
85
|
-
* @param
|
|
81
|
+
* @param msAppId The application ID.
|
|
82
|
+
* @param connectionName The connection name.
|
|
86
83
|
* @param activity The activity.
|
|
87
84
|
* @returns A promise that resolves to the signing resource.
|
|
88
85
|
*/
|
|
89
|
-
async getSignInResource (
|
|
86
|
+
async getSignInResource (msAppId: string, connectionName: string, conversation: ConversationReference, relatesTo?: ConversationReference) : Promise<SignInResource> {
|
|
90
87
|
try {
|
|
91
88
|
const tokenExchangeState = {
|
|
92
|
-
connectionName
|
|
93
|
-
conversation
|
|
89
|
+
connectionName,
|
|
90
|
+
conversation,
|
|
94
91
|
relatesTo,
|
|
95
|
-
msAppId
|
|
92
|
+
msAppId
|
|
96
93
|
}
|
|
97
94
|
const tokenExchangeStateNormalized = normalizeTokenExchangeState(tokenExchangeState)
|
|
98
95
|
const state = Buffer.from(JSON.stringify(tokenExchangeStateNormalized)).toString('base64')
|
|
99
96
|
const params = { state }
|
|
100
97
|
const response = await this.client.get('/api/botsignin/GetSignInResource', { params })
|
|
101
|
-
return response.data as
|
|
98
|
+
return response.data as SignInResource
|
|
102
99
|
} catch (error: any) {
|
|
103
100
|
logger.error(error)
|
|
104
101
|
throw error
|
|
@@ -117,10 +114,56 @@ export class UserTokenClient {
|
|
|
117
114
|
try {
|
|
118
115
|
const params = { userId, connectionName, channelId }
|
|
119
116
|
const response = await this.client.post('/api/usertoken/exchange', tokenExchangeRequest, { params })
|
|
120
|
-
return
|
|
117
|
+
return response.data as TokenResponse
|
|
121
118
|
} catch (error: any) {
|
|
122
119
|
logger.error(error)
|
|
123
|
-
return {
|
|
120
|
+
return { token: undefined }
|
|
124
121
|
}
|
|
125
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Gets the token or sign-in resource.
|
|
126
|
+
* @param userId The user ID.
|
|
127
|
+
* @param connectionName The connection name.
|
|
128
|
+
* @param channelId The channel ID.
|
|
129
|
+
* @param conversation The conversation reference.
|
|
130
|
+
* @param relatesTo The related conversation reference.
|
|
131
|
+
* @param code The code.
|
|
132
|
+
* @param finalRedirect The final redirect URL.
|
|
133
|
+
* @param fwdUrl The forward URL.
|
|
134
|
+
* @returns A promise that resolves to the token or sign-in resource response.
|
|
135
|
+
*/
|
|
136
|
+
async getTokenOrSignInResource (userId: string, connectionName: string, channelId: string, conversation: ConversationReference, relatesTo: ConversationReference, code: string, finalRedirect: string = '', fwdUrl: string = '') : Promise<TokenOrSinginResourceResponse> {
|
|
137
|
+
const state = Buffer.from(JSON.stringify({ conversation, relatesTo, connectionName, msAppId: this.msAppId })).toString('base64')
|
|
138
|
+
const params = { userId, connectionName, channelId, state, code, finalRedirect, fwdUrl }
|
|
139
|
+
const response = await this.client.get('/api/usertoken/GetTokenOrSignInResource', { params })
|
|
140
|
+
return response.data as TokenOrSinginResourceResponse
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Gets the token status.
|
|
145
|
+
* @param userId The user ID.
|
|
146
|
+
* @param channelId The channel ID.
|
|
147
|
+
* @param include The optional include parameter.
|
|
148
|
+
* @returns A promise that resolves to the token status.
|
|
149
|
+
*/
|
|
150
|
+
async getTokenStatus (userId: string, channelId: string, include: string = null!): Promise<TokenStatus[]> {
|
|
151
|
+
const params = { userId, channelId, include }
|
|
152
|
+
const response = await this.client.get('/api/usertoken/GetTokenStatus', { params })
|
|
153
|
+
return response.data as TokenStatus[]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Gets the AAD tokens.
|
|
158
|
+
* @param userId The user ID.
|
|
159
|
+
* @param connectionName The connection name.
|
|
160
|
+
* @param channelId The channel ID.
|
|
161
|
+
* @param resourceUrls The resource URLs.
|
|
162
|
+
* @returns A promise that resolves to the AAD tokens.
|
|
163
|
+
*/
|
|
164
|
+
async getAadTokens (userId: string, connectionName: string, channelId: string, resourceUrls: AadResourceUrls) : Promise<Record<string, TokenResponse>> {
|
|
165
|
+
const params = { userId, connectionName, channelId }
|
|
166
|
+
const response = await this.client.post('/api/usertoken/GetAadTokens', resourceUrls, { params })
|
|
167
|
+
return response.data as Record<string, TokenResponse>
|
|
168
|
+
}
|
|
126
169
|
}
|