@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.
Files changed (106) hide show
  1. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +4 -4
  2. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
  3. package/dist/src/app/agentApplication.d.ts +186 -20
  4. package/dist/src/app/agentApplication.js +235 -33
  5. package/dist/src/app/agentApplication.js.map +1 -1
  6. package/dist/src/app/agentApplicationBuilder.d.ts +1 -1
  7. package/dist/src/app/agentApplicationOptions.d.ts +1 -1
  8. package/dist/src/app/appRoute.d.ts +5 -0
  9. package/dist/src/app/authorization.d.ts +293 -0
  10. package/dist/src/app/authorization.js +375 -0
  11. package/dist/src/app/authorization.js.map +1 -0
  12. package/dist/src/app/index.d.ts +1 -1
  13. package/dist/src/app/index.js +1 -1
  14. package/dist/src/app/index.js.map +1 -1
  15. package/dist/src/app/streaming/citation.d.ts +25 -0
  16. package/dist/src/app/streaming/{AIEntity.js → citation.js} +1 -1
  17. package/dist/src/app/streaming/citation.js.map +1 -0
  18. package/dist/src/app/streaming/{utilities.d.ts → citationUtil.d.ts} +3 -3
  19. package/dist/src/app/streaming/{utilities.js → citationUtil.js} +5 -36
  20. package/dist/src/app/streaming/citationUtil.js.map +1 -0
  21. package/dist/src/app/streaming/streamingResponse.d.ts +3 -4
  22. package/dist/src/app/streaming/streamingResponse.js +24 -22
  23. package/dist/src/app/streaming/streamingResponse.js.map +1 -1
  24. package/dist/src/auth/index.d.ts +1 -0
  25. package/dist/src/auth/index.js +1 -0
  26. package/dist/src/auth/index.js.map +1 -1
  27. package/dist/src/auth/jwt-middleware.js.map +1 -1
  28. package/dist/src/auth/msalTokenCredential.d.ts +10 -0
  29. package/dist/src/auth/msalTokenCredential.js +19 -0
  30. package/dist/src/auth/msalTokenCredential.js.map +1 -0
  31. package/dist/src/auth/msalTokenProvider.d.ts +1 -0
  32. package/dist/src/auth/msalTokenProvider.js +15 -0
  33. package/dist/src/auth/msalTokenProvider.js.map +1 -1
  34. package/dist/src/cards/cardFactory.d.ts +2 -2
  35. package/dist/src/cards/cardFactory.js.map +1 -1
  36. package/dist/src/oauth/index.d.ts +1 -4
  37. package/dist/src/oauth/index.js +1 -4
  38. package/dist/src/oauth/index.js.map +1 -1
  39. package/dist/src/oauth/oAuthFlow.d.ts +62 -19
  40. package/dist/src/oauth/oAuthFlow.js +193 -67
  41. package/dist/src/oauth/oAuthFlow.js.map +1 -1
  42. package/dist/src/oauth/userTokenClient.d.ts +37 -7
  43. package/dist/src/oauth/userTokenClient.js +56 -15
  44. package/dist/src/oauth/userTokenClient.js.map +1 -1
  45. package/dist/src/oauth/userTokenClient.types.d.ts +147 -0
  46. package/dist/src/oauth/{oAuthCard.js → userTokenClient.types.js} +1 -1
  47. package/dist/src/oauth/userTokenClient.types.js.map +1 -0
  48. package/package.json +5 -4
  49. package/src/app/adaptiveCards/adaptiveCardsActions.ts +4 -4
  50. package/src/app/agentApplication.ts +248 -33
  51. package/src/app/agentApplicationBuilder.ts +1 -1
  52. package/src/app/agentApplicationOptions.ts +1 -1
  53. package/src/app/appRoute.ts +6 -0
  54. package/src/app/authorization.ts +418 -0
  55. package/src/app/index.ts +1 -1
  56. package/src/app/streaming/citation.ts +29 -0
  57. package/src/app/streaming/{utilities.ts → citationUtil.ts} +3 -35
  58. package/src/app/streaming/streamingResponse.ts +28 -27
  59. package/src/auth/index.ts +1 -0
  60. package/src/auth/jwt-middleware.ts +1 -1
  61. package/src/auth/msalTokenCredential.ts +14 -0
  62. package/src/auth/msalTokenProvider.ts +17 -1
  63. package/src/cards/cardFactory.ts +2 -3
  64. package/src/oauth/index.ts +1 -4
  65. package/src/oauth/oAuthFlow.ts +226 -70
  66. package/src/oauth/userTokenClient.ts +62 -19
  67. package/src/oauth/userTokenClient.types.ts +173 -0
  68. package/dist/src/app/oauth/authorization.d.ts +0 -87
  69. package/dist/src/app/oauth/authorization.js +0 -135
  70. package/dist/src/app/oauth/authorization.js.map +0 -1
  71. package/dist/src/app/streaming/AIEntity.d.ts +0 -36
  72. package/dist/src/app/streaming/AIEntity.js.map +0 -1
  73. package/dist/src/app/streaming/actionCall.d.ts +0 -33
  74. package/dist/src/app/streaming/actionCall.js +0 -7
  75. package/dist/src/app/streaming/actionCall.js.map +0 -1
  76. package/dist/src/app/streaming/clientCitation.d.ts +0 -68
  77. package/dist/src/app/streaming/clientCitation.js +0 -10
  78. package/dist/src/app/streaming/clientCitation.js.map +0 -1
  79. package/dist/src/app/streaming/message.d.ts +0 -106
  80. package/dist/src/app/streaming/message.js +0 -7
  81. package/dist/src/app/streaming/message.js.map +0 -1
  82. package/dist/src/app/streaming/sensitivityUsageInfo.d.ts +0 -40
  83. package/dist/src/app/streaming/sensitivityUsageInfo.js +0 -3
  84. package/dist/src/app/streaming/sensitivityUsageInfo.js.map +0 -1
  85. package/dist/src/app/streaming/utilities.js.map +0 -1
  86. package/dist/src/oauth/oAuthCard.d.ts +0 -27
  87. package/dist/src/oauth/oAuthCard.js.map +0 -1
  88. package/dist/src/oauth/signingResource.d.ts +0 -43
  89. package/dist/src/oauth/signingResource.js +0 -5
  90. package/dist/src/oauth/signingResource.js.map +0 -1
  91. package/dist/src/oauth/tokenExchangeRequest.d.ts +0 -17
  92. package/dist/src/oauth/tokenExchangeRequest.js +0 -5
  93. package/dist/src/oauth/tokenExchangeRequest.js.map +0 -1
  94. package/dist/src/oauth/tokenResponse.d.ts +0 -29
  95. package/dist/src/oauth/tokenResponse.js +0 -25
  96. package/dist/src/oauth/tokenResponse.js.map +0 -1
  97. package/src/app/oauth/authorization.ts +0 -162
  98. package/src/app/streaming/AIEntity.ts +0 -44
  99. package/src/app/streaming/actionCall.ts +0 -37
  100. package/src/app/streaming/clientCitation.ts +0 -102
  101. package/src/app/streaming/message.ts +0 -125
  102. package/src/app/streaming/sensitivityUsageInfo.ts +0 -48
  103. package/src/oauth/oAuthCard.ts +0 -30
  104. package/src/oauth/signingResource.ts +0 -48
  105. package/src/oauth/tokenExchangeRequest.ts +0 -20
  106. 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,
@@ -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/oAuthCard'
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: SigningResource) : Attachment {
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,
@@ -1,6 +1,3 @@
1
- export * from './oAuthCard'
2
- export * from './signingResource'
3
1
  export * from './userTokenClient'
4
- export * from './tokenExchangeRequest'
5
2
  export * from './oAuthFlow'
6
- export * from './tokenResponse'
3
+ export * from './userTokenClient.types'
@@ -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
- MessageFactory,
11
- TokenExchangeRequest,
12
- UserTokenClient
8
+ Storage,
9
+ MessageFactory
13
10
  } from '../'
14
- import { TokenRequestStatus, TokenResponse } from './tokenResponse'
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
- export class FlowState {
19
- public flowStarted: boolean = false
20
- public flowExpires: number = 0
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 | null
54
+ state: FlowState
39
55
 
40
56
  /**
41
- * The accessor for managing the flow state in user state.
57
+ * The ID of the token exchange request, used to deduplicate requests.
42
58
  */
43
- flowStateAccessor: AgentStatePropertyAccessor<FlowState | null>
59
+ tokenExchangeId: string | null = null
44
60
 
45
61
  /**
46
- * The ID of the token exchange request, used to deduplicate requests.
62
+ * In-memory cache for tokens with expiration.
47
63
  */
48
- tokenExchangeId: string | null = null
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 userState The user state.
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 (userState: UserState, absOauthConnectionName: string, tokenClient?: UserTokenClient, cardTitle?: string, cardText?: string) {
70
- this.state = new FlowState()
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
- if (activity.channelId && activity.from && activity.from.id) {
89
- return await this.userTokenClient.getUserToken(this.absOauthConnectionName, activity.channelId, activity.from.id)
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.getUserState(context)
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 tokenResponse = await this.userTokenClient.getUserToken(this.absOauthConnectionName, context.activity.channelId!, context.activity.from?.id!)
109
- if (tokenResponse?.status === TokenRequestStatus.Success) {
110
- this.state.flowStarted = false
111
- this.state.flowExpires = 0
112
- await this.flowStateAccessor.set(context, this.state)
113
- logger.info('User token retrieved successfully from service')
114
- return tokenResponse
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 authConfig = context.adapter.authConfig
118
- const signingResource = await this.userTokenClient.getSignInResource(authConfig.clientId!, this.absOauthConnectionName, context.activity.getConversationReference(), context.activity.relatesTo)
119
- const oCard: Attachment = CardFactory.oauthCard(this.absOauthConnectionName, this.cardTitle, this.cardText, signingResource)
120
- await context.sendActivity(MessageFactory.attachment(oCard))
121
- this.state.flowStarted = true
122
- this.state.flowExpires = Date.now() + 30000
123
- await this.flowStateAccessor.set(context, this.state)
124
- logger.info('OAuth begin flow completed, waiting for user to sign in')
125
- return {
126
- token: undefined,
127
- status: TokenRequestStatus.InProgress
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.getUserState(context)
190
+ this.state = await this.getFlowState(context)
138
191
  await this.initializeTokenClient(context)
139
- if (this.state?.flowExpires !== 0 && Date.now() > this.state!.flowExpires) {
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
- return { status: TokenRequestStatus.Expired, token: undefined }
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
- const result = await this.userTokenClient?.getUserToken(this.absOauthConnectionName, contFlowActivity.channelId!, contFlowActivity.from?.id!, magicCode)!
149
- return result
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
- return { status: TokenRequestStatus.InProgress, token: undefined }
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?.status === TokenRequestStatus.Success) {
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.flowStateAccessor.set(context, this.state)
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 { status: TokenRequestStatus.Failed, token: undefined }
278
+ return { token: undefined }
177
279
  }
178
280
  }
179
- return { status: TokenRequestStatus.Failed, token: undefined }
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!.flowExpires = 0
192
- await this.flowStateAccessor.set(context, this.state)
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 user state.
309
+ * @returns A promise that resolves to the flow state.
200
310
  */
201
- private async getUserState (context: TurnContext) {
202
- let userProfile: FlowState | null = await this.flowStateAccessor.get(context, null)
203
- if (userProfile === null) {
204
- userProfile = new FlowState()
205
- }
206
- return userProfile
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 { TokenRequestStatus, TokenResponse } from './tokenResponse'
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 { ...response.data, status: TokenRequestStatus.Success }
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 appId The application ID.
85
- * @param cnxName The connection name.
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 (appId: string, cnxName: string, conversationReference: ConversationReference, relatesTo?: ConversationReference) : Promise<SigningResource> {
86
+ async getSignInResource (msAppId: string, connectionName: string, conversation: ConversationReference, relatesTo?: ConversationReference) : Promise<SignInResource> {
90
87
  try {
91
88
  const tokenExchangeState = {
92
- connectionName: cnxName,
93
- conversation: conversationReference,
89
+ connectionName,
90
+ conversation,
94
91
  relatesTo,
95
- msAppId: appId
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 SigningResource
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 { ...response.data, status: TokenRequestStatus.Success }
117
+ return response.data as TokenResponse
121
118
  } catch (error: any) {
122
119
  logger.error(error)
123
- return { status: TokenRequestStatus.Failed, token: undefined }
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
  }