• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-js / 7249230892

18 Dec 2023 02:09PM UTC coverage: 84.539% (-0.09%) from 84.629%
7249230892

Pull #4589

github

web-flow
Merge c8030b61d into f3db3e98b
Pull Request #4589: fix: Add ASE channel validation

9985 of 13088 branches covered (0.0%)

Branch coverage included in aggregate %.

62 of 90 new or added lines in 13 files covered. (68.89%)

99 existing lines in 4 files now uncovered.

20416 of 22873 relevant lines covered (89.26%)

7171.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.67
/libraries/botframework-connector/src/auth/jwtTokenValidation.ts
1
/**
2
 * @module botframework-connector
3
 */
4
/**
5
 * Copyright (c) Microsoft Corporation. All rights reserved.
6
 * Licensed under the MIT License.
7
 */
8

9
import { Activity, Channels, RoleTypes, StatusCodes } from 'botframework-schema';
2✔
10

11
import { AuthenticationError } from './authenticationError';
2✔
12
import { AuthenticationConfiguration } from './authenticationConfiguration';
2✔
13
import { AuthenticationConstants } from './authenticationConstants';
2✔
14
import { ChannelValidation } from './channelValidation';
2✔
15
import { Claim, ClaimsIdentity } from './claimsIdentity';
2✔
16
import { ICredentialProvider } from './credentialProvider';
17
import { EmulatorValidation } from './emulatorValidation';
2✔
18
import { EnterpriseChannelValidation } from './enterpriseChannelValidation';
2✔
19
import { GovernmentChannelValidation } from './governmentChannelValidation';
2✔
20
import { GovernmentConstants } from './governmentConstants';
2✔
21
import { SkillValidation } from './skillValidation';
2✔
22
import { AseChannelValidation } from './aseChannelValidation';
2✔
23

24
/**
25
 * @deprecated Use `ConfigurationBotFrameworkAuthentication` instead to perform JWT token validation.
26
 */
27
// eslint-disable-next-line @typescript-eslint/no-namespace
28
export namespace JwtTokenValidation {
2✔
29
    /**
30
     * Authenticates the request and sets the service url in the set of trusted urls.
31
     *
32
     * @param {Partial<Activity>} activity The incoming Activity from the Bot Framework or the Emulator
33
     * @param {string} authHeader The Bearer token included as part of the request
34
     * @param {ICredentialProvider} credentials The set of valid credentials, such as the Bot Application ID
35
     * @param {string} channelService The channel service
36
     * @param {AuthenticationConfiguration} authConfig Optional, the auth config
37
     * @returns {Promise<ClaimsIdentity>} Promise with ClaimsIdentity for the request.
38
     */
39
    export async function authenticateRequest(
2✔
40
        activity: Partial<Activity>,
41
        authHeader: string,
42
        credentials: ICredentialProvider,
43
        channelService: string,
44
        authConfig?: AuthenticationConfiguration
45
    ): Promise<ClaimsIdentity> {
46
        if (!authConfig) {
44✔
47
            authConfig = new AuthenticationConfiguration();
42✔
48
        }
49

50
        if (!authHeader.trim()) {
44✔
51
            const isAuthDisabled = await credentials.isAuthenticationDisabled();
14✔
52
            if (!isAuthDisabled) {
14✔
53
                throw new AuthenticationError(
8✔
54
                    'Unauthorized Access. Request is not authorized',
55
                    StatusCodes.UNAUTHORIZED
56
                );
57
            }
58

59
            // Check if the activity is for a skill call and is coming from the Emulator.
60
            if (
6!
61
                activity.channelId === Channels.Emulator &&
6!
62
                activity.recipient &&
63
                activity.recipient.role === RoleTypes.Skill
64
            ) {
65
                return SkillValidation.createAnonymousSkillClaim();
×
66
            }
67

68
            // In the scenario where Auth is disabled, we still want to have the
69
            // IsAuthenticated flag set in the ClaimsIdentity. To do this requires
70
            // adding in an empty claim.
71
            return new ClaimsIdentity([], AuthenticationConstants.AnonymousAuthType);
6✔
72
        }
73

74
        const claimsIdentity: ClaimsIdentity = await validateAuthHeader(
30✔
75
            authHeader,
76
            credentials,
77
            channelService,
78
            activity.channelId,
79
            activity.serviceUrl,
80
            authConfig
81
        );
82

83
        return claimsIdentity;
18✔
84
    }
85

86
    /**
87
     * Validate an auth header.
88
     *
89
     * @param {string} authHeader the auth header
90
     * @param {ICredentialProvider} credentials the credentials
91
     * @param {string} channelService the channel service
92
     * @param {string} channelId the channel ID
93
     * @param {string} serviceUrl the service URL
94
     * @param {AuthenticationConfiguration} authConfig the auth config
95
     * @returns {Promise<ClaimsIdentity>} a promise that resolves to the validated claims, or rejects if validation fails
96
     */
97
    export async function validateAuthHeader(
2✔
98
        authHeader: string,
99
        credentials: ICredentialProvider,
100
        channelService: string,
101
        channelId: string,
102
        serviceUrl = '',
30✔
103
        authConfig: AuthenticationConfiguration = new AuthenticationConfiguration()
4✔
104
    ): Promise<ClaimsIdentity> {
105
        if (!authHeader.trim()) {
36✔
106
            throw new AuthenticationError("'authHeader' required.", StatusCodes.BAD_REQUEST);
2✔
107
        }
108

109
        const identity = await authenticateToken(
34✔
110
            authHeader,
111
            credentials,
112
            channelService,
113
            channelId,
114
            authConfig,
115
            serviceUrl
116
        );
117

118
        await validateClaims(authConfig, identity.claims);
18✔
119

120
        return identity;
18✔
121
    }
122

123
    // eslint-disable-next-line jsdoc/require-jsdoc, no-inner-declarations
124
    async function authenticateToken(
125
        authHeader: string,
126
        credentials: ICredentialProvider,
127
        channelService: string,
128
        channelId: string,
129
        authConfig: AuthenticationConfiguration,
130
        serviceUrl: string
131
    ): Promise<ClaimsIdentity> {
132

133
        if (AseChannelValidation.isTokenFromAseChannel(channelId)) {
34!
NEW
134
            return AseChannelValidation.authenticateAseChannelToken(authHeader);
×
135
        }
136

137
        if (SkillValidation.isSkillToken(authHeader)) {
34!
UNCOV
138
            return await SkillValidation.authenticateChannelToken(
×
139
                authHeader,
140
                credentials,
141
                channelService,
142
                channelId,
143
                authConfig
144
            );
145
        }
146

147
        if (EmulatorValidation.isTokenFromEmulator(authHeader)) {
34✔
148
            return await EmulatorValidation.authenticateEmulatorToken(
10✔
149
                authHeader,
150
                credentials,
151
                channelService,
152
                channelId
153
            );
154
        }
155

156
        if (isPublicAzure(channelService)) {
24✔
157
            if (serviceUrl.trim()) {
12✔
158
                return await ChannelValidation.authenticateChannelTokenWithServiceUrl(
2✔
159
                    authHeader,
160
                    credentials,
161
                    serviceUrl,
162
                    channelId
163
                );
164
            }
165

166
            return await ChannelValidation.authenticateChannelToken(authHeader, credentials, channelId);
10✔
167
        }
168

169
        if (isGovernment(channelService)) {
12✔
170
            if (serviceUrl.trim()) {
10✔
171
                return await GovernmentChannelValidation.authenticateChannelTokenWithServiceUrl(
2✔
172
                    authHeader,
173
                    credentials,
174
                    serviceUrl,
175
                    channelId
176
                );
177
            }
178

179
            return await GovernmentChannelValidation.authenticateChannelToken(authHeader, credentials, channelId);
8✔
180
        }
181

182
        // Otherwise use Enterprise Channel Validation
183
        if (serviceUrl.trim()) {
2!
UNCOV
184
            return await EnterpriseChannelValidation.authenticateChannelTokenWithServiceUrl(
×
185
                authHeader,
186
                credentials,
187
                serviceUrl,
188
                channelId,
189
                channelService
190
            );
191
        }
192

193
        return await EnterpriseChannelValidation.authenticateChannelToken(
2✔
194
            authHeader,
195
            credentials,
196
            channelId,
197
            channelService
198
        );
199
    }
200

201
    /**
202
     * Validates the identity claims against the ClaimsValidator in AuthenticationConfiguration if present.
203
     *
204
     * @param authConfig The authentication configuration.
205
     * @param claims The list of claims to validate.
206
     */
207
    // eslint-disable-next-line no-inner-declarations
208
    async function validateClaims(authConfig: AuthenticationConfiguration, claims: Claim[] = []): Promise<void> {
×
209
        if (authConfig.validateClaims) {
18!
210
            // Call the validation method if defined (it should throw an exception if the validation fails)
UNCOV
211
            await authConfig.validateClaims(claims);
×
212
        } else if (SkillValidation.isSkillClaim(claims)) {
18!
213
            // Skill claims must be validated using AuthenticationConfiguration validateClaims
UNCOV
214
            throw new AuthenticationError(
×
215
                'Unauthorized Access. Request is not authorized. Skill Claims require validation.',
216
                StatusCodes.UNAUTHORIZED
217
            );
218
        }
219
    }
220

221
    /**
222
     * Gets the AppId from a claims list.
223
     *
224
     * @summary
225
     * In v1 tokens the AppId is in the "ver" AuthenticationConstants.AppIdClaim claim.
226
     * In v2 tokens the AppId is in the "azp" AuthenticationConstants.AuthorizedParty claim.
227
     * If the AuthenticationConstants.VersionClaim is not present, this method will attempt to
228
     * obtain the attribute from the AuthenticationConstants.AppIdClaim or if present.
229
     *
230
     * Throws a TypeError if claims is falsy.
231
     *
232
     * @param {Claim[]} claims An object containing claims types and their values.
233
     * @returns {string} the app ID
234
     */
235
    export function getAppIdFromClaims(claims: Claim[]): string {
2✔
236
        if (!claims) {
160!
UNCOV
237
            throw new TypeError('JwtTokenValidation.getAppIdFromClaims(): missing claims.');
×
238
        }
239

240
        let appId: string;
241

242
        // Group claims by type for fast lookup
243
        const claimsByType = claims.reduce((acc, claim) => ({ ...acc, [claim.type]: claim }), {});
598✔
244

245
        // Depending on Version, the AppId is either in the
246
        // appid claim (Version 1) or the 'azp' claim (Version 2).
247
        const versionClaim = claimsByType[AuthenticationConstants.VersionClaim];
160✔
248
        const versionValue = versionClaim && versionClaim.value;
160✔
249
        if (!versionValue || versionValue === '1.0') {
160✔
250
            // No version or a version of '1.0' means we should look for
251
            // the claim in the 'appid' claim.
252
            const appIdClaim = claimsByType[AuthenticationConstants.AppIdClaim];
70✔
253
            if (appIdClaim && appIdClaim.value) {
70✔
254
                appId = appIdClaim.value;
24✔
255
            }
256
        } else if (versionValue === '2.0') {
90!
257
            // Version '2.0' puts the AppId in the 'azp' claim.
258
            const azpClaim = claimsByType[AuthenticationConstants.AuthorizedParty];
90✔
259
            if (azpClaim && azpClaim.value) {
90✔
260
                appId = azpClaim.value;
88✔
261
            }
262
        }
263

264
        return appId;
160✔
265
    }
266

267
    // eslint-disable-next-line jsdoc/require-jsdoc, no-inner-declarations
268
    function isPublicAzure(channelService: string): boolean {
269
        return !channelService || channelService.length === 0;
24✔
270
    }
271

272
    /**
273
     * Determine whether or not a channel service is government
274
     *
275
     * @param {string} channelService the channel service
276
     * @returns {boolean} true if this is a government channel service
277
     */
278
    export function isGovernment(channelService: string): boolean {
2✔
279
        return channelService && channelService.toLowerCase() === GovernmentConstants.ChannelService;
512✔
280
    }
281

282
    /**
283
     * Internal helper to check if the token has the shape we expect "Bearer [big long string]".
284
     *
285
     * @param {string} authHeader A string containing the token header.
286
     * @returns {boolean} True if the token is valid, false if not.
287
     */
288
    export function isValidTokenFormat(authHeader: string): boolean {
2✔
289
        if (!authHeader) {
54✔
290
            // No token, not valid.
291
            return false;
4✔
292
        }
293

294
        const parts: string[] = authHeader.trim().split(' ');
50✔
295
        if (parts.length !== 2) {
50✔
296
            // Tokens MUST have exactly 2 parts. If we don't have 2 parts, it's not a valid token
297
            return false;
6✔
298
        }
299

300
        // We now have an array that should be:
301
        // [0] = "Bearer"
302
        // [1] = "[Big Long String]"
303
        const authScheme: string = parts[0];
44✔
304
        if (authScheme !== 'Bearer') {
44✔
305
            // The scheme MUST be "Bearer"
306
            return false;
2✔
307
        }
308

309
        return true;
42✔
310
    }
311
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc