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

microsoft / botbuilder-js / 16948256108

13 Aug 2025 08:14PM UTC coverage: 84.361% (-0.1%) from 84.47%
16948256108

push

github

web-flow
bump: dependencies to safe versions (#4896)

* Bump dependencies to safe versions

* Add flag to avoid test failing in Node > 22.18

* Add flag to avoid test failing in Node > 22.18 to test:min

8282 of 10996 branches covered (75.32%)

Branch coverage included in aggregate %.

20589 of 23227 relevant lines covered (88.64%)

3863.72 hits per line

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

76.77
/libraries/botframework-connector/src/auth/jwtTokenExtractor.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 { Algorithm, decode, JwtHeader, verify, VerifyOptions } from 'jsonwebtoken';
1✔
10
import { Claim, ClaimsIdentity } from './claimsIdentity';
1✔
11
import { EndorsementsValidator } from './endorsementsValidator';
1✔
12
import { OpenIdMetadata } from './openIdMetadata';
1✔
13
import { AuthenticationError } from './authenticationError';
1✔
14
import { StatusCodes } from 'botframework-schema';
1✔
15
import { ProxySettings } from 'botbuilder-stdlib/lib/azureCoreHttpCompat';
16

17
/**
18
 * A JWT token processing class that gets identity information and performs security token validation.
19
 */
20
export class JwtTokenExtractor {
1✔
21
    // Cache for OpenIdConnect configuration managers (one per metadata URL)
22
    private static openIdMetadataCache: Map<string, OpenIdMetadata> = new Map<string, OpenIdMetadata>();
1✔
23

24
    // Token validation parameters for this instance
25
    readonly tokenValidationParameters: VerifyOptions;
26

27
    // OpenIdMetadata for this instance
28
    readonly openIdMetadata: OpenIdMetadata;
29

30
    /**
31
     * Initializes a new instance of the [JwtTokenExtractor](xref:botframework-connector.JwtTokenExtractor) class. Extracts relevant data from JWT Tokens.
32
     *
33
     * @param tokenValidationParameters Token validation parameters.
34
     * @param metadataUrl Metadata Url.
35
     * @param allowedSigningAlgorithms Allowed signing algorithms.
36
     * @param proxySettings The proxy settings for the request.
37
     * @param tokenRefreshInterval The token refresh interval in hours. The default value is 24 hours.
38
     */
39
    constructor(
40
        tokenValidationParameters: VerifyOptions,
41
        metadataUrl: string,
42
        allowedSigningAlgorithms: string[] | Algorithm[],
43
        proxySettings?: ProxySettings,
44
        tokenRefreshInterval?: number,
45
    ) {
46
        this.tokenValidationParameters = { ...tokenValidationParameters };
20✔
47
        this.tokenValidationParameters.algorithms = allowedSigningAlgorithms as Algorithm[];
20✔
48
        this.openIdMetadata = JwtTokenExtractor.getOrAddOpenIdMetadata(
20✔
49
            metadataUrl,
50
            proxySettings,
51
            tokenRefreshInterval,
52
        );
53
    }
54

55
    private static getOrAddOpenIdMetadata(
56
        metadataUrl: string,
57
        proxySettings?: ProxySettings,
58
        tokenRefreshInterval?: number,
59
    ): OpenIdMetadata {
60
        let metadata = this.openIdMetadataCache.get(metadataUrl);
20✔
61
        if (!metadata) {
20✔
62
            metadata = new OpenIdMetadata(metadataUrl, proxySettings, tokenRefreshInterval);
20✔
63
            this.openIdMetadataCache.set(metadataUrl, metadata);
20✔
64
        }
65

66
        return metadata;
20✔
67
    }
68

69
    /**
70
     * Gets the claims identity associated with a request.
71
     *
72
     * @param authorizationHeader The raw HTTP header in the format: "Bearer [longString]".
73
     * @param channelId The Id of the channel being validated in the original request.
74
     * @param requiredEndorsements The required JWT endorsements.
75
     * @returns A `Promise` representation for either a [ClaimsIdentity](botframework-connector:module.ClaimsIdentity) or `null`.
76
     */
77
    async getIdentityFromAuthHeader(
78
        authorizationHeader: string,
79
        channelId: string,
80
        requiredEndorsements?: string[],
81
    ): Promise<ClaimsIdentity | null> {
82
        if (!authorizationHeader) {
20!
83
            return null;
×
84
        }
85

86
        const parts: string[] = authorizationHeader.split(' ');
20✔
87
        if (parts.length === 2) {
20✔
88
            return await this.getIdentity(parts[0], parts[1], channelId, requiredEndorsements || []);
19✔
89
        }
90

91
        return null;
1✔
92
    }
93

94
    /**
95
     * Gets the claims identity associated with a request.
96
     *
97
     * @param scheme The associated scheme.
98
     * @param parameter The token.
99
     * @param channelId The Id of the channel being validated in the original request.
100
     * @param requiredEndorsements The required JWT endorsements.
101
     * @returns A `Promise` representation for either a [ClaimsIdentity](botframework-connector:module.ClaimsIdentity) or `null`.
102
     */
103
    async getIdentity(
104
        scheme: string,
105
        parameter: string,
106
        channelId: string,
107
        requiredEndorsements: string[] = [],
×
108
    ): Promise<ClaimsIdentity | null> {
109
        // No header in correct scheme or no token
110
        if (scheme !== 'Bearer' || !parameter) {
19!
111
            return null;
×
112
        }
113

114
        // Issuer isn't allowed? No need to check signature
115
        if (!this.hasAllowedIssuer(parameter)) {
19✔
116
            return null;
1✔
117
        }
118

119
        return await this.validateToken(parameter, channelId, requiredEndorsements);
18✔
120
    }
121

122
    /**
123
     * @private
124
     */
125
    private hasAllowedIssuer(jwtToken: string): boolean {
126
        const payload = decode(jwtToken);
19✔
127

128
        let issuer: string;
129
        if (payload && typeof payload === 'object') {
19✔
130
            issuer = payload.iss;
18✔
131
        } else {
132
            return false;
1✔
133
        }
134

135
        if (Array.isArray(this.tokenValidationParameters.issuer)) {
18✔
136
            return this.tokenValidationParameters.issuer.indexOf(issuer) !== -1;
16✔
137
        }
138

139
        if (typeof this.tokenValidationParameters.issuer === 'string') {
2✔
140
            return this.tokenValidationParameters.issuer === issuer;
2✔
141
        }
142

143
        return false;
×
144
    }
145

146
    /**
147
     * @private
148
     */
149
    private async validateToken(
150
        jwtToken: string,
151
        channelId: string,
152
        requiredEndorsements: string[],
153
    ): Promise<ClaimsIdentity> {
154
        let header: Partial<JwtHeader> = {};
18✔
155
        const decodedToken = decode(jwtToken, { complete: true });
18✔
156
        if (decodedToken && typeof decodedToken === 'object') {
18✔
157
            header = decodedToken.header;
18✔
158
        }
159

160
        // Update the signing tokens from the last refresh
161
        const keyId = header.kid;
18✔
162
        const metadata = await this.openIdMetadata.getKey(keyId);
18✔
163
        if (!metadata) {
18!
164
            throw new AuthenticationError('Signing Key could not be retrieved.', StatusCodes.UNAUTHORIZED);
×
165
        }
166

167
        try {
18✔
168
            let decodedPayload: Record<string, string> = {};
18✔
169
            const verifyResults = verify(jwtToken, metadata.key, this.tokenValidationParameters);
18✔
170
            if (verifyResults && typeof verifyResults === 'object') {
16✔
171
                // Note: casting is necessary here, but we know `object` is loosely equivalent to a Record
172
                decodedPayload = verifyResults as Record<string, string>;
16✔
173
            }
174

175
            // enforce endorsements in openIdMetadadata if there is any endorsements associated with the key
176
            const endorsements = metadata.endorsements;
16✔
177
            if (Array.isArray(endorsements) && endorsements.length !== 0) {
16!
178
                const isEndorsed = EndorsementsValidator.validate(channelId, endorsements);
×
179
                if (!isEndorsed) {
×
180
                    throw new AuthenticationError(
×
181
                        `Could not validate endorsement for key: ${keyId} with endorsements: ${endorsements.join(',')}`,
182
                        StatusCodes.UNAUTHORIZED,
183
                    );
184
                }
185

186
                // Verify that additional endorsements are satisfied. If no additional endorsements are expected, the requirement is satisfied as well
187
                const additionalEndorsementsSatisfied = requiredEndorsements.every((endorsement) =>
×
188
                    EndorsementsValidator.validate(endorsement, endorsements),
×
189
                );
190

191
                if (!additionalEndorsementsSatisfied) {
×
192
                    throw new AuthenticationError(
×
193
                        `Could not validate additional endorsement for key: ${keyId} with endorsements: ${requiredEndorsements.join(
194
                            ',',
195
                        )}. Expected endorsements: ${requiredEndorsements.join(',')}`,
196
                        StatusCodes.UNAUTHORIZED,
197
                    );
198
                }
199
            }
200

201
            if (this.tokenValidationParameters.algorithms) {
16✔
202
                if (this.tokenValidationParameters.algorithms.indexOf(header.alg as Algorithm) === -1) {
16!
203
                    throw new AuthenticationError(
×
204
                        `"Token signing algorithm '${header.alg}' not in allowed list`,
205
                        StatusCodes.UNAUTHORIZED,
206
                    );
207
                }
208
            }
209

210
            const claims = Object.entries(decodedPayload).map<Claim>(([type, value]) => ({ type, value }));
82✔
211

212
            // Note: true is used here to indicate that these claims are to be considered authenticated. They are sourced
213
            // from a validated JWT (see `verify` above), so no harm in doing so.
214
            return new ClaimsIdentity(claims, true);
16✔
215
        } catch (err) {
216
            if (err.name === 'TokenExpiredError') {
2✔
217
                console.error(err);
2✔
218
                throw new AuthenticationError('The token has expired', StatusCodes.UNAUTHORIZED);
2✔
219
            }
220
            console.error(`Error finding key for token. Available keys: ${metadata.key}`);
×
221
            throw err;
×
222
        }
223
    }
224
}
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

© 2026 Coveralls, Inc