@microsoft/agents-hosting 1.7.1 → 1.8.0-beta.34.g5d543f96c0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -2
- package/dist/package.json +3 -3
- package/dist/src/agent-client/agentClient.d.ts +18 -6
- package/dist/src/agent-client/agentClient.js +34 -18
- package/dist/src/agent-client/agentClient.js.map +1 -1
- package/dist/src/agent-client/agentResponseHandler.d.ts +14 -9
- package/dist/src/agent-client/agentResponseHandler.js +10 -5
- package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
- package/dist/src/agent-client/createAgentResponseHandler.d.ts +7 -2
- package/dist/src/agent-client/createAgentResponseHandler.js +55 -8
- package/dist/src/agent-client/createAgentResponseHandler.js.map +1 -1
- package/dist/src/app/adaptiveCards/activityValueParsers.d.ts +1 -4
- package/dist/src/app/adaptiveCards/activityValueParsers.js +2 -6
- package/dist/src/app/adaptiveCards/activityValueParsers.js.map +1 -1
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.d.ts +18 -0
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +53 -23
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
- package/dist/src/app/agentApplication.d.ts +7 -0
- package/dist/src/app/agentApplication.js +130 -70
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/attachmentDownloader.d.ts +4 -1
- package/dist/src/app/attachmentDownloader.js +27 -5
- package/dist/src/app/attachmentDownloader.js.map +1 -1
- package/dist/src/app/auth/handlers/agenticAuthorization.d.ts +1 -1
- package/dist/src/app/auth/handlers/azureBotAuthorization.d.ts +1 -1
- package/dist/src/app/auth/types.d.ts +3 -4
- package/dist/src/app/index.d.ts +2 -0
- package/dist/src/app/index.js.map +1 -1
- package/dist/src/app/streaming/streamingResponse.js +2 -9
- package/dist/src/app/streaming/streamingResponse.js.map +1 -1
- package/dist/src/app/teamsAttachmentDownloader.d.ts +3 -1
- package/dist/src/app/teamsAttachmentDownloader.js +12 -1
- package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
- package/dist/src/auth/authConfiguration.js +12 -0
- package/dist/src/auth/authConfiguration.js.map +1 -1
- package/dist/src/auth/index.d.ts +3 -1
- package/dist/src/auth/index.js +3 -1
- package/dist/src/auth/index.js.map +1 -1
- package/dist/src/auth/jwt-middleware.d.ts +6 -0
- package/dist/src/auth/jwt-middleware.js +263 -13
- package/dist/src/auth/jwt-middleware.js.map +1 -1
- package/dist/src/auth/msal/msalConnectionSettings.d.ts +8 -0
- package/dist/src/auth/msal/msalHttpRetryHandlerHelper.d.ts +34 -0
- package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js +126 -0
- package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js.map +1 -0
- package/dist/src/auth/msal/msalTokenProvider.d.ts +1 -0
- package/dist/src/auth/msal/msalTokenProvider.js +23 -14
- package/dist/src/auth/msal/msalTokenProvider.js.map +1 -1
- package/dist/src/auth/settings.d.ts +18 -0
- package/dist/src/auth/settings.js +8 -2
- package/dist/src/auth/settings.js.map +1 -1
- package/dist/src/cloudAdapter.d.ts +29 -2
- package/dist/src/cloudAdapter.js +44 -2
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/errorHelper.js +137 -101
- package/dist/src/errorHelper.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/invoke/adaptiveCardInvokeValue.d.ts +5 -0
- package/dist/src/oauth/userTokenClient.d.ts +0 -1
- package/dist/src/oauth/userTokenClient.js +0 -1
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/dist/src/observability/traces.js +3 -2
- package/dist/src/observability/traces.js.map +1 -1
- package/dist/src/outboundHostValidator.d.ts +47 -0
- package/dist/src/outboundHostValidator.js +149 -0
- package/dist/src/outboundHostValidator.js.map +1 -0
- package/dist/src/state/agentState.d.ts +5 -0
- package/dist/src/state/agentState.js +16 -3
- package/dist/src/state/agentState.js.map +1 -1
- package/package.json +3 -3
- package/src/agent-client/agentClient.ts +46 -23
- package/src/agent-client/agentResponseHandler.ts +14 -9
- package/src/agent-client/createAgentResponseHandler.ts +70 -9
- package/src/app/adaptiveCards/activityValueParsers.ts +3 -10
- package/src/app/adaptiveCards/adaptiveCardsActions.ts +52 -24
- package/src/app/agentApplication.ts +143 -67
- package/src/app/attachmentDownloader.ts +25 -5
- package/src/app/auth/handlers/agenticAuthorization.ts +1 -1
- package/src/app/auth/handlers/azureBotAuthorization.ts +1 -1
- package/src/app/auth/types.ts +3 -3
- package/src/app/index.ts +8 -0
- package/src/app/streaming/streamingResponse.ts +2 -8
- package/src/app/teamsAttachmentDownloader.ts +11 -1
- package/src/auth/authConfiguration.ts +12 -0
- package/src/auth/index.ts +3 -1
- package/src/auth/jwt-middleware.ts +298 -14
- package/src/auth/msal/msalConnectionSettings.ts +10 -0
- package/src/auth/msal/msalHttpRetryHandlerHelper.ts +161 -0
- package/src/auth/msal/msalTokenProvider.ts +32 -14
- package/src/auth/settings.ts +18 -3
- package/src/cloudAdapter.ts +56 -3
- package/src/errorHelper.ts +155 -114
- package/src/index.ts +1 -0
- package/src/invoke/adaptiveCardInvokeValue.ts +5 -0
- package/src/oauth/userTokenClient.ts +0 -1
- package/src/observability/traces.ts +2 -1
- package/src/outboundHostValidator.ts +172 -0
- package/src/state/agentState.ts +24 -6
|
@@ -11,6 +11,8 @@ import { Attachment } from '@microsoft/agents-activity'
|
|
|
11
11
|
import { AuthProvider } from '../auth/authProvider'
|
|
12
12
|
import { debug } from '@microsoft/agents-telemetry'
|
|
13
13
|
import { loadAuthConfigFromEnv, MsalTokenProvider } from '../auth'
|
|
14
|
+
import { createOutboundHostValidator, OutboundUrlPolicy } from '../outboundHostValidator'
|
|
15
|
+
import { Connections } from '../auth/connections'
|
|
14
16
|
|
|
15
17
|
const logger = debug('agents:attachmentDownloader')
|
|
16
18
|
|
|
@@ -27,16 +29,19 @@ const logger = debug('agents:attachmentDownloader')
|
|
|
27
29
|
export class AttachmentDownloader<TState extends TurnState = TurnState> implements InputFileDownloader<TState> {
|
|
28
30
|
private _httpClient: HttpClient
|
|
29
31
|
private _stateKey: string
|
|
32
|
+
private readonly _hostValidator: OutboundUrlPolicy
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* Creates an instance of AttachmentDownloader.
|
|
33
36
|
* This class is responsible for downloading input files from attachments.
|
|
34
37
|
*
|
|
35
38
|
* @param stateKey The key to store files in state. Defaults to 'inputFiles'.
|
|
39
|
+
* @param outboundHostValidator Optional shared policy used to validate outbound attachment URLs.
|
|
36
40
|
*/
|
|
37
|
-
public constructor (stateKey: string = 'inputFiles') {
|
|
41
|
+
public constructor (stateKey: string = 'inputFiles', outboundHostValidator?: OutboundUrlPolicy) {
|
|
38
42
|
this._httpClient = new HttpClient()
|
|
39
43
|
this._stateKey = stateKey
|
|
44
|
+
this._hostValidator = outboundHostValidator ?? createOutboundHostValidator()
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/**
|
|
@@ -52,10 +57,18 @@ export class AttachmentDownloader<TState extends TurnState = TurnState> implemen
|
|
|
52
57
|
return Promise.resolve([])
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
let accessToken = ''
|
|
61
|
+
const connectionManager = (context.adapter as { connectionManager?: Connections })?.connectionManager
|
|
62
|
+
const identity = context.identity
|
|
63
|
+
if (connectionManager && identity) {
|
|
64
|
+
const scope = identity.azp ?? identity.appid ?? 'https://api.botframework.com'
|
|
65
|
+
logger.debug(`Using adapter connection manager token provider for attachment downloads with scope ${scope}`)
|
|
66
|
+
accessToken = await connectionManager.getTokenProviderFromActivity(identity, context.activity).getAccessToken(scope)
|
|
67
|
+
} else {
|
|
68
|
+
logger.debug('Using MsalTokenProvider fallback for attachment downloads')
|
|
69
|
+
const authProvider: AuthProvider = new MsalTokenProvider()
|
|
70
|
+
accessToken = await authProvider.getAccessToken(loadAuthConfigFromEnv(), 'https://api.botframework.com')
|
|
71
|
+
}
|
|
59
72
|
|
|
60
73
|
const files: InputFile[] = []
|
|
61
74
|
for (const attachment of attachments) {
|
|
@@ -90,6 +103,13 @@ export class AttachmentDownloader<TState extends TurnState = TurnState> implemen
|
|
|
90
103
|
if (accessToken.length > 0) {
|
|
91
104
|
headers.Authorization = `Bearer ${accessToken}`
|
|
92
105
|
}
|
|
106
|
+
if (!this._hostValidator.isAllowed(attachment.contentUrl)) {
|
|
107
|
+
let host = '[invalid-url]'
|
|
108
|
+
try { host = new URL(attachment.contentUrl).hostname } catch { /* invalid */ }
|
|
109
|
+
logger.warn(`Attachment contentUrl host is not in the configured allowed hosts. Host='${host}'`)
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
93
113
|
const response = await this._httpClient.get(attachment.contentUrl, {
|
|
94
114
|
headers,
|
|
95
115
|
responseType: 'arraybuffer'
|
|
@@ -22,7 +22,7 @@ const logger = debug('agents:authorization:agentic')
|
|
|
22
22
|
* where `{handlerId}` is the handler's unique identifier and `{propertyName}` matches the property name.
|
|
23
23
|
*
|
|
24
24
|
* @example
|
|
25
|
-
* ```
|
|
25
|
+
* ```
|
|
26
26
|
* # For a handler with id "myAuth":
|
|
27
27
|
* AgentApplication__UserAuthorization__handlers__myAuth__settings__type=AgenticUserAuthorization
|
|
28
28
|
* AgentApplication__UserAuthorization__handlers__myAuth__settings__scopes=api://scope1 api://scope2
|
|
@@ -48,7 +48,7 @@ interface AzureBotActiveHandler extends ActiveAuthorizationHandler {
|
|
|
48
48
|
* where `{handlerId}` is the handler's unique identifier and `{propertyName}` matches the property name.
|
|
49
49
|
*
|
|
50
50
|
* @example
|
|
51
|
-
* ```
|
|
51
|
+
* ```
|
|
52
52
|
* # For a handler with id "myAuth":
|
|
53
53
|
* AgentApplication__UserAuthorization__handlers__myAuth__settings__azureBotOAuthConnectionName=MyConnection
|
|
54
54
|
* AgentApplication__UserAuthorization__handlers__myAuth__settings__oboScopes=api://scope1 api://scope2
|
package/src/app/auth/types.ts
CHANGED
|
@@ -126,7 +126,7 @@ export interface AuthorizationHandlerTokenOptions {
|
|
|
126
126
|
* where `{handlerId}` is the handler's unique identifier and `{propertyName}` matches the property name.
|
|
127
127
|
*
|
|
128
128
|
* @example
|
|
129
|
-
* ```
|
|
129
|
+
* ```
|
|
130
130
|
* # For a handler with id "myAuth":
|
|
131
131
|
* AgentApplication__UserAuthorization__handlers__myAuth__settings__azureBotOAuthConnectionName=MyConnection
|
|
132
132
|
* AgentApplication__UserAuthorization__handlers__myAuth__settings__oboScopes=api://scope1 api://scope2
|
|
@@ -170,7 +170,7 @@ export interface AzureBotAuthorizationOptionsLegacy {
|
|
|
170
170
|
* @deprecated
|
|
171
171
|
* Messages configuration for the AzureBotAuthorization handler.
|
|
172
172
|
*/
|
|
173
|
-
interface AzureBotAuthorizationOptionsMessages {
|
|
173
|
+
export interface AzureBotAuthorizationOptionsMessages {
|
|
174
174
|
/**
|
|
175
175
|
* @deprecated Use `invalidSignInRetryMessage` instead.
|
|
176
176
|
* Message displayed when an invalid code is entered.
|
|
@@ -198,7 +198,7 @@ interface AzureBotAuthorizationOptionsMessages {
|
|
|
198
198
|
* @deprecated
|
|
199
199
|
* Settings for on-behalf-of token acquisition.
|
|
200
200
|
*/
|
|
201
|
-
interface AzureBotAuthorizationOptionsOBO {
|
|
201
|
+
export interface AzureBotAuthorizationOptionsOBO {
|
|
202
202
|
/**
|
|
203
203
|
* @deprecated Use `oboConnectionName` instead.
|
|
204
204
|
* Connection name to use for on-behalf-of token acquisition.
|
package/src/app/index.ts
CHANGED
|
@@ -3,6 +3,14 @@ export * from './agentApplicationBuilder'
|
|
|
3
3
|
export * from './agentApplicationOptions'
|
|
4
4
|
export * from './appRoute'
|
|
5
5
|
export { Authorization } from './auth/authorization'
|
|
6
|
+
export type { AgenticAuthorizationOptions, AzureBotAuthorizationOptions } from './auth/handlers'
|
|
7
|
+
export type {
|
|
8
|
+
AuthorizationOptions,
|
|
9
|
+
AuthorizationHandlerTokenOptions,
|
|
10
|
+
AzureBotAuthorizationOptionsLegacy,
|
|
11
|
+
AzureBotAuthorizationOptionsMessages,
|
|
12
|
+
AzureBotAuthorizationOptionsOBO
|
|
13
|
+
} from './auth/types'
|
|
6
14
|
export * from './routeHandler'
|
|
7
15
|
export * from './routeList'
|
|
8
16
|
export * from './routeRank'
|
|
@@ -537,14 +537,8 @@ export class StreamingResponse {
|
|
|
537
537
|
if (activity.deliveryMode === DeliveryModes.ExpectReplies) {
|
|
538
538
|
this._isStreamingChannel = false
|
|
539
539
|
} else if (Channels.Msteams === activity.channelIdChannel) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
// TODO: Enable streaming for agentic requests when supported.
|
|
543
|
-
this._isStreamingChannel = false
|
|
544
|
-
} else {
|
|
545
|
-
this._isStreamingChannel = true
|
|
546
|
-
this._delayInMs = 1000
|
|
547
|
-
}
|
|
540
|
+
this._isStreamingChannel = true
|
|
541
|
+
this._delayInMs = 1000
|
|
548
542
|
} else if (Channels.Webchat === activity.channelId || Channels.Directline === activity.channelId || Channels.Emulator === activity.channelId) {
|
|
549
543
|
this._isStreamingChannel = true
|
|
550
544
|
this._delayInMs = 500
|
|
@@ -11,6 +11,7 @@ import { TurnContext } from '../turnContext'
|
|
|
11
11
|
import { TurnState } from './turnState'
|
|
12
12
|
import { HttpClient } from '../httpClient'
|
|
13
13
|
import { z } from 'zod'
|
|
14
|
+
import { createOutboundHostValidator, OutboundUrlPolicy } from '../outboundHostValidator'
|
|
14
15
|
|
|
15
16
|
const logger = debug('agents:M365AttachmentDownloader')
|
|
16
17
|
|
|
@@ -20,10 +21,12 @@ const logger = debug('agents:M365AttachmentDownloader')
|
|
|
20
21
|
export class M365AttachmentDownloader<TState extends TurnState = TurnState> implements InputFileDownloader<TState> {
|
|
21
22
|
private _httpClient: HttpClient
|
|
22
23
|
private _stateKey: string
|
|
24
|
+
private readonly _hostValidator: OutboundUrlPolicy
|
|
23
25
|
|
|
24
|
-
public constructor (stateKey: string = 'inputFiles') {
|
|
26
|
+
public constructor (stateKey: string = 'inputFiles', outboundHostValidator?: OutboundUrlPolicy) {
|
|
25
27
|
this._httpClient = new HttpClient()
|
|
26
28
|
this._stateKey = stateKey
|
|
29
|
+
this._hostValidator = outboundHostValidator ?? createOutboundHostValidator()
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -69,6 +72,13 @@ export class M365AttachmentDownloader<TState extends TurnState = TurnState> impl
|
|
|
69
72
|
const contentSchema = z.object({ downloadUrl: z.string().url() })
|
|
70
73
|
const parsed = contentSchema.safeParse(attachment.content)
|
|
71
74
|
const downloadUrl = parsed.success ? parsed.data.downloadUrl : attachment.contentUrl
|
|
75
|
+
if (!this._hostValidator.isAllowed(downloadUrl)) {
|
|
76
|
+
let host = '[invalid-url]'
|
|
77
|
+
try { host = new URL(downloadUrl).hostname } catch { /* invalid */ }
|
|
78
|
+
logger.warn(`Attachment downloadUrl host is not in the configured allowed hosts. Host='${host}'`)
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
const response = await this._httpClient.get(downloadUrl, { responseType: 'arraybuffer' })
|
|
73
83
|
|
|
74
84
|
const content = Buffer.from(response.data as ArrayBuffer)
|
|
@@ -8,6 +8,7 @@ import { loadEnvSettings, AuthConfiguration, envParser, envParserUtils, LoadEnv,
|
|
|
8
8
|
|
|
9
9
|
export { type AuthConfiguration, type ConnectionSettings, type ConnectionSettingsBase, type MsalConnectionSettings, type SidecarConnectionSettings, AuthType, resolveAuthority, type ConnectionMapItem, resolveAuthType } from './settings'
|
|
10
10
|
import { prune } from '../utils'
|
|
11
|
+
import { parseBooleanEnv } from '../utils/env'
|
|
11
12
|
import { ExceptionHelper } from '@microsoft/agents-activity'
|
|
12
13
|
import { Errors } from '../errorHelper'
|
|
13
14
|
|
|
@@ -28,6 +29,7 @@ function summarizeAuthConfiguration (authConfig: AuthConfiguration) {
|
|
|
28
29
|
authorityEndpoint: config.authorityEndpoint ? redactUrl(config.authorityEndpoint) : undefined,
|
|
29
30
|
scopes: (config.scopes ? redactScopes(config.scopes) : undefined) as any,
|
|
30
31
|
issuers: config.issuers?.map(redactUrl).filter(e => e !== undefined),
|
|
32
|
+
validateIssuer: config.validateIssuer,
|
|
31
33
|
federatedClientId: redactString(config.federatedClientId, true),
|
|
32
34
|
certPemFile: redactString(config.certPemFile),
|
|
33
35
|
certKeyFile: redactString(config.certKeyFile),
|
|
@@ -40,6 +42,7 @@ function summarizeAuthConfiguration (authConfig: AuthConfiguration) {
|
|
|
40
42
|
alternateBlueprintConnectionName: undefined, // Alias of altBlueprintConnectionName, avoid logging duplicate info
|
|
41
43
|
azureRegion: config.azureRegion,
|
|
42
44
|
sendX5C: config.sendX5C,
|
|
45
|
+
msalRetryCount: config.msalRetryCount,
|
|
43
46
|
sidecarBaseUrl: config.sidecarBaseUrl ? redactUrl(config.sidecarBaseUrl) : undefined,
|
|
44
47
|
serviceName: config.serviceName,
|
|
45
48
|
blueprintServiceName: config.blueprintServiceName,
|
|
@@ -96,6 +99,10 @@ const connectionsEnv = {
|
|
|
96
99
|
idpmResource: envParserUtils.bypass,
|
|
97
100
|
azureRegion: envParserUtils.bypass,
|
|
98
101
|
sendX5C: (value) => ({ value: value === 'true' }),
|
|
102
|
+
msalRetryCount: (value) => {
|
|
103
|
+
const n = parseInt(value, 10)
|
|
104
|
+
return { value: Number.isFinite(n) && n >= 0 ? n : undefined }
|
|
105
|
+
},
|
|
99
106
|
sidecarBaseUrl: envParserUtils.bypass,
|
|
100
107
|
serviceName: envParserUtils.bypass,
|
|
101
108
|
blueprintServiceName: envParserUtils.bypass,
|
|
@@ -114,6 +121,7 @@ const connectionsEnv = {
|
|
|
114
121
|
}
|
|
115
122
|
return { value: value.split(/\s+/).filter(Boolean) }
|
|
116
123
|
},
|
|
124
|
+
validateIssuer: (value) => ({ value: parseBooleanEnv(value) }),
|
|
117
125
|
}),
|
|
118
126
|
default (connections?: AuthConfiguration['connections'], connectionsMap?: AuthConfiguration['connectionsMap']) {
|
|
119
127
|
const conn = connections ?? this.connections
|
|
@@ -230,6 +238,7 @@ const legacyBotFrameworkEnv = {
|
|
|
230
238
|
WIDAssertionFile: envParserUtils.redirect(connectionsEnv.parser, 'WIDAssertionFile'),
|
|
231
239
|
azureRegion: envParserUtils.redirect(connectionsEnv.parser, 'azureRegion'),
|
|
232
240
|
sendX5C: envParserUtils.redirect(connectionsEnv.parser, 'sendX5C'),
|
|
241
|
+
msalRetryCount: envParserUtils.redirect(connectionsEnv.parser, 'msalRetryCount'),
|
|
233
242
|
authType: envParserUtils.redirect(connectionsEnv.parser, 'authType'),
|
|
234
243
|
federatedTokenFile: envParserUtils.redirect(connectionsEnv.parser, 'federatedTokenFile'),
|
|
235
244
|
idpmResource: envParserUtils.redirect(connectionsEnv.parser, 'idpmResource'),
|
|
@@ -239,6 +248,7 @@ const legacyBotFrameworkEnv = {
|
|
|
239
248
|
bypassLocalNetworkRestriction: envParserUtils.redirect(connectionsEnv.parser, 'bypassLocalNetworkRestriction'),
|
|
240
249
|
requestTimeout: envParserUtils.redirect(connectionsEnv.parser, 'requestTimeout'),
|
|
241
250
|
retryCount: envParserUtils.redirect(connectionsEnv.parser, 'retryCount'),
|
|
251
|
+
validateIssuer: envParserUtils.redirect(connectionsEnv.parser, 'validateIssuer'),
|
|
242
252
|
}),
|
|
243
253
|
process (env: LoadEnv) {
|
|
244
254
|
return legacyPrefixEnv.process.call(this, env)
|
|
@@ -265,6 +275,7 @@ const legacyPrefixEnv = {
|
|
|
265
275
|
WIDAssertionFile: envParserUtils.redirect(connectionsEnv.parser, 'WIDAssertionFile'),
|
|
266
276
|
azureRegion: envParserUtils.redirect(connectionsEnv.parser, 'azureRegion'),
|
|
267
277
|
sendX5C: envParserUtils.redirect(connectionsEnv.parser, 'sendX5C'),
|
|
278
|
+
msalRetryCount: envParserUtils.redirect(connectionsEnv.parser, 'msalRetryCount'),
|
|
268
279
|
authType: envParserUtils.redirect(connectionsEnv.parser, 'authType'),
|
|
269
280
|
federatedTokenFile: envParserUtils.redirect(connectionsEnv.parser, 'federatedTokenFile'),
|
|
270
281
|
idpmResource: envParserUtils.redirect(connectionsEnv.parser, 'idpmResource'),
|
|
@@ -274,6 +285,7 @@ const legacyPrefixEnv = {
|
|
|
274
285
|
bypassLocalNetworkRestriction: envParserUtils.redirect(connectionsEnv.parser, 'bypassLocalNetworkRestriction'),
|
|
275
286
|
requestTimeout: envParserUtils.redirect(connectionsEnv.parser, 'requestTimeout'),
|
|
276
287
|
retryCount: envParserUtils.redirect(connectionsEnv.parser, 'retryCount'),
|
|
288
|
+
validateIssuer: envParserUtils.redirect(connectionsEnv.parser, 'validateIssuer'),
|
|
277
289
|
}),
|
|
278
290
|
process (env: LoadEnv, prefix?: string) {
|
|
279
291
|
const settings: Partial<AuthConfiguration> = {}
|
package/src/auth/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export * from './authConfiguration'
|
|
2
2
|
export * from './authConstants'
|
|
3
3
|
export * from './authProvider'
|
|
4
|
-
export * from './
|
|
4
|
+
export * from './connections'
|
|
5
5
|
export * from './request'
|
|
6
|
+
export * from './msal/msalTokenProvider'
|
|
6
7
|
export * from './msal/msalTokenCredential'
|
|
7
8
|
export * from './connectionManager'
|
|
9
|
+
export * from './connections'
|
|
8
10
|
export * from './msal/msalConnectionManager'
|
|
9
11
|
export * from './sidecar/sidecarAuthProvider'
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { AuthConfiguration
|
|
6
|
+
import { AuthConfiguration } from './authConfiguration'
|
|
7
|
+
import { getDefaultIssuers, resolveAuthority } from './settings'
|
|
7
8
|
import { Request } from './request'
|
|
8
9
|
import { WebResponse, NextFunction } from '../interfaces/webResponse'
|
|
9
10
|
import { Errors } from '../errorHelper'
|
|
@@ -15,6 +16,25 @@ import { debug } from '@microsoft/agents-telemetry'
|
|
|
15
16
|
const logger = debug('agents:jwt-middleware')
|
|
16
17
|
const jwksClients = new Map<string, JwksClient>()
|
|
17
18
|
const maxJwksClients = 100
|
|
19
|
+
const authorizedAudience = Symbol('authorizedAudience')
|
|
20
|
+
|
|
21
|
+
interface AuthorizedRequest extends Request {
|
|
22
|
+
[authorizedAudience]?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface VerifiedToken {
|
|
26
|
+
payload: JwtPayload
|
|
27
|
+
audience: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gets the configured audience selected while authorizing a request.
|
|
32
|
+
* @param req The authorized request.
|
|
33
|
+
* @returns The matched configured client ID, or `undefined` for anonymous requests.
|
|
34
|
+
*/
|
|
35
|
+
export function getAuthorizedAudience (req: Request): string | undefined {
|
|
36
|
+
return (req as AuthorizedRequest)[authorizedAudience]
|
|
37
|
+
}
|
|
18
38
|
|
|
19
39
|
/**
|
|
20
40
|
* Clears process-wide JWKS clients.
|
|
@@ -23,6 +43,248 @@ export function clearJwksClients (): void {
|
|
|
23
43
|
jwksClients.clear()
|
|
24
44
|
}
|
|
25
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Well-known Microsoft first-party token issuer tenant IDs that are always trusted,
|
|
48
|
+
* mirroring the default `ValidIssuers` set in the .NET SDK
|
|
49
|
+
* (Microsoft.Agents.Hosting.AspNetCore `AspNetExtensions.AddAgentAspNetAuthentication`).
|
|
50
|
+
*
|
|
51
|
+
* These identify Microsoft infrastructure tenants used by Azure Bot Service, Teams and
|
|
52
|
+
* skill and SDK-specific Activity callback flows. They are accepted in addition to a connection's configured
|
|
53
|
+
* issuers so that enabling issuer validation does not reject legitimate first-party traffic.
|
|
54
|
+
*/
|
|
55
|
+
const WELL_KNOWN_PUBLIC_TENANT_IDS = [
|
|
56
|
+
'd6d49420-f39b-4df7-a1dc-d59a935871db',
|
|
57
|
+
'f8cdef31-a31e-4b4a-93e4-5f571e91255a',
|
|
58
|
+
'69e9b82d-4842-4902-8d1e-abc5b98a55e8'
|
|
59
|
+
]
|
|
60
|
+
const WELL_KNOWN_GOV_TENANT_ID = 'cab8a31a-1906-4287-a0d8-4eef66b95f6e'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Determines whether the configured authority targets the Azure US Government cloud.
|
|
64
|
+
* @param authority The configured Entra authority.
|
|
65
|
+
* @returns `true` when the authority is a US Government endpoint.
|
|
66
|
+
*/
|
|
67
|
+
function isGovAuthority (authority?: string): boolean {
|
|
68
|
+
return !!authority && /login\.microsoftonline\.us/i.test(authority)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getAuthority (authConfig: AuthConfiguration): string | undefined {
|
|
72
|
+
return authConfig.authorityEndpoint ?? authConfig.authority
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns the well-known Microsoft first-party issuers that are always trusted for the
|
|
77
|
+
* cloud implied by `authority` (public by default, US Government when the authority is gov).
|
|
78
|
+
* @param authority The configured Entra authority.
|
|
79
|
+
* @returns The well-known first-party issuer list.
|
|
80
|
+
*/
|
|
81
|
+
function getWellKnownFirstPartyIssuers (authority?: string): string[] {
|
|
82
|
+
if (isGovAuthority(authority)) {
|
|
83
|
+
return [
|
|
84
|
+
'https://api.botframework.us',
|
|
85
|
+
`https://sts.windows.net/${WELL_KNOWN_GOV_TENANT_ID}/`,
|
|
86
|
+
`https://login.microsoftonline.us/${WELL_KNOWN_GOV_TENANT_ID}/v2.0`
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
return [
|
|
90
|
+
'https://api.botframework.com',
|
|
91
|
+
...WELL_KNOWN_PUBLIC_TENANT_IDS.flatMap((t) => [
|
|
92
|
+
`https://sts.windows.net/${t}/`,
|
|
93
|
+
`https://login.microsoftonline.com/${t}/v2.0`
|
|
94
|
+
])
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Computes the lowercase issuer allow-list used to validate a token's `iss` claim against a
|
|
100
|
+
* matched connection. The list is the connection's configured issuers (or a tenant-scoped
|
|
101
|
+
* default when none are configured) unioned with the always-trusted Microsoft first-party
|
|
102
|
+
* issuers. Comparison is performed case-insensitively to tolerate operator-provided tenant
|
|
103
|
+
* GUID casing while still rejecting tokens from unrelated tenants.
|
|
104
|
+
* @param authConfig The matched connection configuration.
|
|
105
|
+
* @returns A de-duplicated, lowercased list of accepted issuers.
|
|
106
|
+
*/
|
|
107
|
+
function getValidIssuers (authConfig: AuthConfiguration): string[] {
|
|
108
|
+
const authority = getAuthority(authConfig)
|
|
109
|
+
const configured = authConfig.issuers && authConfig.issuers.length > 0
|
|
110
|
+
? authConfig.issuers
|
|
111
|
+
: getDefaultIssuers(authConfig.tenantId ?? '', authority ?? 'https://login.microsoftonline.com')
|
|
112
|
+
const accepted = new Set<string>()
|
|
113
|
+
for (const issuer of [...configured, ...getWellKnownFirstPartyIssuers(authority)]) {
|
|
114
|
+
accepted.add(issuer.toLowerCase())
|
|
115
|
+
}
|
|
116
|
+
return [...accepted]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Returns the effective tenant identifier for a connection: the tenant embedded in the authority
|
|
121
|
+
* path when present (which takes precedence, matching {@link resolveAuthority}), otherwise the
|
|
122
|
+
* configured `tenantId`.
|
|
123
|
+
* @param authConfig The matched connection configuration.
|
|
124
|
+
* @returns The effective tenant identifier, or `undefined` when none is configured.
|
|
125
|
+
*/
|
|
126
|
+
function getEffectiveTenant (authConfig: AuthConfiguration): string | undefined {
|
|
127
|
+
const authority = getAuthority(authConfig)
|
|
128
|
+
if (authority) {
|
|
129
|
+
try {
|
|
130
|
+
const segment = new URL(authority.replace(/\/+$/, '')).pathname.split('/').filter(Boolean).pop()
|
|
131
|
+
if (segment) {
|
|
132
|
+
return segment
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// Non-URL authority — fall through to the configured tenantId.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return authConfig.tenantId
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Determines whether a connection is configured as a multi-tenant ("blueprint") agent, i.e. its
|
|
143
|
+
* effective tenant is the Entra `common` or `organizations` meta-tenant. For such agents the
|
|
144
|
+
* calling tenant is not known at configuration time, so the strict issuer allow-list cannot
|
|
145
|
+
* enumerate it and issuer validation must instead accept any canonical Entra issuer for the
|
|
146
|
+
* configured cloud (still bound to the token's `tid` by {@link validateTenantBinding} and anchored
|
|
147
|
+
* by the signature and audience checks).
|
|
148
|
+
* @param authConfig The matched connection configuration.
|
|
149
|
+
* @returns `true` when the connection targets the `common`/`organizations` meta-tenant.
|
|
150
|
+
*/
|
|
151
|
+
function isMultiTenant (authConfig: AuthConfiguration): boolean {
|
|
152
|
+
const tenant = getEffectiveTenant(authConfig)?.toLowerCase()
|
|
153
|
+
return tenant === 'common' || tenant === 'organizations'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Determines whether `iss` is a canonical Entra issuer that may be accepted for a multi-tenant
|
|
158
|
+
* connection. The issuer must carry a concrete tenant GUID and, for cloud-specific v2 issuers,
|
|
159
|
+
* belong to the same cloud as the configured authority (public vs US Government). The cloud-
|
|
160
|
+
* agnostic v1 `sts.windows.net` host is accepted in either cloud.
|
|
161
|
+
* @param iss The token issuer claim.
|
|
162
|
+
* @param authConfig The matched connection configuration.
|
|
163
|
+
* @returns `true` when the issuer is an acceptable Entra tenant issuer for the configured cloud.
|
|
164
|
+
*/
|
|
165
|
+
function isAcceptableTenantIssuer (iss: string, authConfig: AuthConfiguration): boolean {
|
|
166
|
+
const info = getEntraIssuerInfo(iss)
|
|
167
|
+
if (!info) {
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
return info.gov === undefined || info.gov === isGovAuthority(getAuthority(authConfig))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Validates that the token's `iss` claim is present and acceptable for the matched connection.
|
|
175
|
+
* Without this check a signature-valid token from any tenant (Entra signing keys are shared across
|
|
176
|
+
* tenants in a cloud) with a matching audience would be accepted, enabling a cross-tenant
|
|
177
|
+
* authentication bypass for multi-tenant app registrations.
|
|
178
|
+
*
|
|
179
|
+
* For single-tenant connections the issuer must be in the connection's allow-list. For multi-tenant
|
|
180
|
+
* ("blueprint") connections, where the calling tenant is unknown at configuration time, any
|
|
181
|
+
* canonical Entra issuer for the configured cloud is also accepted; the token's tenant is then
|
|
182
|
+
* bound to its `tid` claim by {@link validateTenantBinding} and ultimately anchored by the
|
|
183
|
+
* signature and audience (clientId) checks.
|
|
184
|
+
* @param iss The token issuer claim (from the decoded payload).
|
|
185
|
+
* @param authConfig The matched connection configuration.
|
|
186
|
+
* @throws When the issuer is missing, non-string, or not acceptable for the connection.
|
|
187
|
+
*/
|
|
188
|
+
function validateIssuer (iss: unknown, authConfig: AuthConfiguration): void {
|
|
189
|
+
if (typeof iss === 'string') {
|
|
190
|
+
if (getValidIssuers(authConfig).includes(iss.toLowerCase())) {
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
if (isMultiTenant(authConfig) && isAcceptableTenantIssuer(iss, authConfig)) {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const err = ExceptionHelper.generateException(Error, Errors.JwtIssuerMismatch)
|
|
198
|
+
logger.error(err.message, iss)
|
|
199
|
+
throw err
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Matches a tenant GUID (the format Entra uses for the `tid` claim and the tenant segment of an
|
|
204
|
+
* Entra issuer). Operator-configured issuers may instead use a tenant domain alias, which cannot
|
|
205
|
+
* be compared to the GUID `tid` claim and is therefore left unbound.
|
|
206
|
+
*/
|
|
207
|
+
const ENTRA_TENANT_GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Cloud-affinity metadata for a recognised Entra issuer.
|
|
211
|
+
*/
|
|
212
|
+
interface EntraIssuerInfo {
|
|
213
|
+
/** The lowercased tenant GUID embedded in the issuer. */
|
|
214
|
+
tenant: string
|
|
215
|
+
/**
|
|
216
|
+
* `true` for US Government (`login.microsoftonline.us`) issuers, `false` for public
|
|
217
|
+
* (`login.microsoftonline.com`) issuers, and `undefined` for the cloud-agnostic v1
|
|
218
|
+
* `sts.windows.net` host (which is shared across the public and US Government clouds).
|
|
219
|
+
*/
|
|
220
|
+
gov?: boolean
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Parses a recognised public or US Government Entra issuer into its tenant GUID and cloud affinity,
|
|
225
|
+
* or returns `undefined` when the issuer is not such an Entra issuer or does not embed a tenant
|
|
226
|
+
* GUID.
|
|
227
|
+
*
|
|
228
|
+
* Only GUID tenants are recognised: the token's `tid` claim is always the tenant GUID, so an issuer
|
|
229
|
+
* whose tenant segment is a domain alias (e.g. `contoso.onmicrosoft.com`) cannot be compared to
|
|
230
|
+
* `tid` and is intentionally left unrecognised to preserve compatibility. Non-Entra issuers such as
|
|
231
|
+
* the Azure Bot Service `api.botframework.*` issuers carry no `tid` claim and are not matched here.
|
|
232
|
+
* @param iss The token issuer claim.
|
|
233
|
+
* @returns The issuer's tenant and cloud affinity, or `undefined` when not a recognised Entra issuer.
|
|
234
|
+
*/
|
|
235
|
+
function getEntraIssuerInfo (iss: string): EntraIssuerInfo | undefined {
|
|
236
|
+
const v1 = /^https:\/\/sts\.windows\.net\/([^/]+)\/$/i.exec(iss)
|
|
237
|
+
if (v1) {
|
|
238
|
+
return ENTRA_TENANT_GUID.test(v1[1]) ? { tenant: v1[1].toLowerCase() } : undefined
|
|
239
|
+
}
|
|
240
|
+
const v2 = /^([^:]+):\/\/([^/]+)\/([^/]+)\/v2\.0$/.exec(iss)
|
|
241
|
+
const v2Host = v2 ? /^login\.microsoftonline\.(com|us)$/i.exec(v2[2]) : undefined
|
|
242
|
+
if (v2 && v2Host && v2[1].toLowerCase() === 'https' && ENTRA_TENANT_GUID.test(v2[3])) {
|
|
243
|
+
return { tenant: v2[3].toLowerCase(), gov: v2Host[1].toLowerCase() === 'us' }
|
|
244
|
+
}
|
|
245
|
+
return undefined
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Extracts the tenant GUID embedded in a recognised public or US Government Entra issuer, or
|
|
250
|
+
* `undefined` when the issuer is not such an Entra issuer or does not embed a tenant GUID.
|
|
251
|
+
* @param iss The token issuer claim.
|
|
252
|
+
* @returns The lowercased tenant GUID, or `undefined` when no bindable tenant is present.
|
|
253
|
+
*/
|
|
254
|
+
function getIssuerTenant (iss: string): string | undefined {
|
|
255
|
+
return getEntraIssuerInfo(iss)?.tenant
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Validates that an Entra token's `tid` (tenant id) claim matches the tenant GUID embedded in its
|
|
260
|
+
* `iss` claim. This binds the accepted issuer to the token's tenant. Because
|
|
261
|
+
* Entra signing keys are shared across tenants within a cloud, requiring the issuer's tenant and
|
|
262
|
+
* the `tid` claim to agree prevents a token whose `iss` was allow-listed (for example one of the
|
|
263
|
+
* always-trusted Microsoft first-party tenants) from being accepted on behalf of a different
|
|
264
|
+
* tenant.
|
|
265
|
+
*
|
|
266
|
+
* The binding only applies when both claims are strings and the issuer is a recognised Entra
|
|
267
|
+
* issuer carrying a GUID tenant. Missing/non-string claims, Azure Bot Service issuers, and
|
|
268
|
+
* alias-based issuers are skipped for backward compatibility.
|
|
269
|
+
* @param iss The token issuer claim.
|
|
270
|
+
* @param tid The token tenant id claim.
|
|
271
|
+
* @throws When both claims are strings and the issuer embeds a tenant GUID different from `tid`.
|
|
272
|
+
*/
|
|
273
|
+
function validateTenantBinding (iss: unknown, tid: unknown): void {
|
|
274
|
+
if (typeof iss !== 'string' || typeof tid !== 'string') {
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
const issuerTenant = getIssuerTenant(iss)
|
|
278
|
+
if (!issuerTenant) {
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
if (tid.toLowerCase() !== issuerTenant) {
|
|
282
|
+
const err = ExceptionHelper.generateException(Error, Errors.JwtTenantMismatch)
|
|
283
|
+
logger.error(err.message, tid)
|
|
284
|
+
throw err
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
26
288
|
/**
|
|
27
289
|
* Builds the JWKS URI for the given token issuer and auth configuration.
|
|
28
290
|
* @param iss The token issuer claim.
|
|
@@ -30,9 +292,14 @@ export function clearJwksClients (): void {
|
|
|
30
292
|
* @returns The JWKS URI string.
|
|
31
293
|
*/
|
|
32
294
|
export function buildJwksUri (iss: string, authConfig: AuthConfiguration): string {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
295
|
+
switch (typeof iss === 'string' ? iss.toLowerCase() : '') {
|
|
296
|
+
case 'https://api.botframework.com':
|
|
297
|
+
return 'https://login.botframework.com/v1/.well-known/keys'
|
|
298
|
+
case 'https://api.botframework.us':
|
|
299
|
+
return 'https://login.botframework.azure.us/v1/.well-known/keys'
|
|
300
|
+
default:
|
|
301
|
+
return `${resolveAuthority(authConfig.authorityEndpoint ?? authConfig.authority, authConfig.tenantId)}/discovery/v2.0/keys`
|
|
302
|
+
}
|
|
36
303
|
}
|
|
37
304
|
|
|
38
305
|
export function getJwksClient (jwksUri: string): JwksClient {
|
|
@@ -59,31 +326,38 @@ export function getJwksClient (jwksUri: string): JwksClient {
|
|
|
59
326
|
* Verifies the JWT token.
|
|
60
327
|
* @param raw The raw JWT token.
|
|
61
328
|
* @param config The authentication configuration.
|
|
62
|
-
* @returns A promise that resolves to the
|
|
329
|
+
* @returns A promise that resolves to the verified payload and matched configured audience.
|
|
63
330
|
*/
|
|
64
|
-
const verifyToken = async (raw: string, config: AuthConfiguration): Promise<
|
|
331
|
+
const verifyToken = async (raw: string, config: AuthConfiguration): Promise<VerifiedToken> => {
|
|
65
332
|
const payload = jwt.decode(raw) as JwtPayload
|
|
66
333
|
logger.debug('jwt.decode ', JSON.stringify(payload))
|
|
67
334
|
|
|
68
335
|
if (!payload) {
|
|
69
336
|
throw ExceptionHelper.generateException(Error, Errors.InvalidJwtToken)
|
|
70
337
|
}
|
|
71
|
-
const
|
|
338
|
+
const audiences = Array.isArray(payload.aud)
|
|
339
|
+
? payload.aud
|
|
340
|
+
: typeof payload.aud === 'string'
|
|
341
|
+
? [payload.aud]
|
|
342
|
+
: []
|
|
72
343
|
|
|
73
344
|
const matchingEntry = config.connections && config.connections.size > 0
|
|
74
|
-
? [...config.connections.entries()].find(([_, configuration]) =>
|
|
345
|
+
? [...config.connections.entries()].find(([_, configuration]) =>
|
|
346
|
+
typeof configuration.clientId === 'string' && audiences.includes(configuration.clientId)
|
|
347
|
+
)
|
|
75
348
|
: undefined
|
|
76
349
|
|
|
77
350
|
if (!matchingEntry) {
|
|
78
351
|
const err = ExceptionHelper.generateException(Error, Errors.JwtAudienceMismatch)
|
|
79
|
-
logger.error(err.message,
|
|
352
|
+
logger.error(err.message, audiences)
|
|
80
353
|
throw err
|
|
81
354
|
}
|
|
82
355
|
|
|
83
356
|
const [key, authConfig] = matchingEntry
|
|
84
357
|
logger.debug(`Audience found at key: ${key}`)
|
|
85
358
|
|
|
86
|
-
const
|
|
359
|
+
const issuer = typeof payload.iss === 'string' ? payload.iss : ''
|
|
360
|
+
const jwksUri = buildJwksUri(issuer, authConfig)
|
|
87
361
|
|
|
88
362
|
logger.debug(`fetching keys from ${jwksUri}`)
|
|
89
363
|
const jwksClient = getJwksClient(jwksUri)
|
|
@@ -109,7 +383,7 @@ const verifyToken = async (raw: string, config: AuthConfiguration): Promise<JwtP
|
|
|
109
383
|
clockTolerance: 300
|
|
110
384
|
}
|
|
111
385
|
|
|
112
|
-
|
|
386
|
+
const verifiedPayload = await new Promise<JwtPayload>((resolve, reject) => {
|
|
113
387
|
jwt.verify(raw, getKey, verifyOptions, (err, user) => {
|
|
114
388
|
if (err) {
|
|
115
389
|
logger.error('jwt.verify ', JSON.stringify(err))
|
|
@@ -119,6 +393,14 @@ const verifyToken = async (raw: string, config: AuthConfiguration): Promise<JwtP
|
|
|
119
393
|
resolve(user as JwtPayload)
|
|
120
394
|
})
|
|
121
395
|
})
|
|
396
|
+
if (authConfig.validateIssuer) {
|
|
397
|
+
validateIssuer(verifiedPayload.iss, authConfig)
|
|
398
|
+
}
|
|
399
|
+
validateTenantBinding(verifiedPayload.iss, verifiedPayload.tid)
|
|
400
|
+
return {
|
|
401
|
+
payload: verifiedPayload,
|
|
402
|
+
audience: authConfig.clientId!
|
|
403
|
+
}
|
|
122
404
|
}
|
|
123
405
|
|
|
124
406
|
/**
|
|
@@ -203,9 +485,11 @@ export const authorizeJWT = (authConfig: AuthConfiguration) => {
|
|
|
203
485
|
const token = extractBearerToken(req.headers.authorization)
|
|
204
486
|
if (token) {
|
|
205
487
|
try {
|
|
206
|
-
const
|
|
207
|
-
logger.debug('token verified for ',
|
|
208
|
-
req.user =
|
|
488
|
+
const verifiedToken = await verifyToken(token, authConfig)
|
|
489
|
+
logger.debug('token verified for ', verifiedToken.payload)
|
|
490
|
+
req.user = verifiedToken.payload
|
|
491
|
+
const authorizedRequest = req as AuthorizedRequest
|
|
492
|
+
authorizedRequest[authorizedAudience] = verifiedToken.audience
|
|
209
493
|
} catch (err: Error | any) {
|
|
210
494
|
failed = true
|
|
211
495
|
logger.error(err)
|
|
@@ -76,4 +76,14 @@ export interface MsalConnectionSettings extends ConnectionSettingsBase {
|
|
|
76
76
|
* The federated client ID for the authentication configuration, used for workload identity federation scenarios.
|
|
77
77
|
*/
|
|
78
78
|
federatedClientId?: string
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Maximum number of retries for an MSAL HTTP request that returns status 408.
|
|
82
|
+
*
|
|
83
|
+
* @remarks
|
|
84
|
+
* The initial request is not included in this count. Set to `0` to disable retries.
|
|
85
|
+
* Defaults to `2`.
|
|
86
|
+
*/
|
|
87
|
+
msalRetryCount?: number
|
|
88
|
+
|
|
79
89
|
}
|