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

microsoft / botbuilder-dotnet / 333699

15 Dec 2022 07:53PM UTC coverage: 79.128% (+0.07%) from 79.062%
333699

Pull #6572

CI-PR build

GitHub
Merge cd7fc8620 into a908ca355
Pull Request #6572: [#6563] Expired JWT token exception not being handled

25742 of 32532 relevant lines covered (79.13%)

0.79 hits per line

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

54.81
/libraries/Microsoft.Bot.Connector/Authentication/JwtTokenExtractor.cs
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
// Licensed under the MIT License.
3

4
using System;
5
using System.Collections.Concurrent;
6
using System.Collections.Generic;
7
using System.Diagnostics;
8
using System.IdentityModel.Tokens.Jwt;
9
using System.Linq;
10
using System.Net.Http;
11
using System.Security.Claims;
12
using System.Threading.Tasks;
13
using Microsoft.IdentityModel.Protocols;
14
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
15
using Microsoft.IdentityModel.Tokens;
16

17
namespace Microsoft.Bot.Connector.Authentication
18
{
19
    /// <summary>
20
    /// A JWT token processing class that gets identity information and performs security token validation.
21
    /// </summary>
22
    public class JwtTokenExtractor
23
    {
24
        /// <summary>
25
        /// Cache for OpenIdConnect configuration managers (one per metadata URL).
26
        /// </summary>
27
        private static readonly ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>> _openIdMetadataCache =
1✔
28
            new ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>>();
1✔
29

30
        /// <summary>
31
        /// Cache for Endorsement configuration managers (one per metadata URL).
32
        /// </summary>
33
        private static readonly ConcurrentDictionary<string, ConfigurationManager<IDictionary<string, HashSet<string>>>> _endorsementsCache =
1✔
34
            new ConcurrentDictionary<string, ConfigurationManager<IDictionary<string, HashSet<string>>>>();
1✔
35

36
        /// <summary>
37
        /// Token validation parameters for this instance.
38
        /// </summary>
39
        private readonly TokenValidationParameters _tokenValidationParameters;
40

41
        /// <summary>
42
        /// OpenIdConnect configuration manager for this instance.
43
        /// </summary>
44
        private readonly ConfigurationManager<OpenIdConnectConfiguration> _openIdMetadata;
45

46
        /// <summary>
47
        /// Endorsements configuration manager for this instance.
48
        /// </summary>
49
        private readonly ConfigurationManager<IDictionary<string, HashSet<string>>> _endorsementsData;
50

51
        /// <summary>
52
        /// Allowed signing algorithms.
53
        /// </summary>
54
        private readonly HashSet<string> _allowedSigningAlgorithms;
55

56
        /// <summary>
57
        /// Initializes a new instance of the <see cref="JwtTokenExtractor"/> class.
58
        /// Extracts relevant data from JWT Tokens.
59
        /// </summary>
60
        /// <param name="httpClient">As part of validating JWT Tokens, endorsements need to be fetched from
61
        /// sources specified by the relevant security URLs. This HttpClient is used to allow for resource
62
        /// pooling around those retrievals. As those resources require TLS sharing the HttpClient is
63
        /// important to overall performance.</param>
64
        /// <param name="tokenValidationParameters">tokenValidationParameters.</param>
65
        /// <param name="metadataUrl">metadataUrl.</param>
66
        /// <param name="allowedSigningAlgorithms">allowedSigningAlgorithms.</param>
67
        public JwtTokenExtractor(
1✔
68
            HttpClient httpClient,
1✔
69
            TokenValidationParameters tokenValidationParameters,
1✔
70
            string metadataUrl,
1✔
71
            HashSet<string> allowedSigningAlgorithms)
1✔
72
        {
73
            // Make our own copy so we can edit it
74
            _tokenValidationParameters = tokenValidationParameters.Clone();
1✔
75
            _tokenValidationParameters.RequireSignedTokens = true;
1✔
76
            _allowedSigningAlgorithms = allowedSigningAlgorithms;
1✔
77

78
            _openIdMetadata = _openIdMetadataCache.GetOrAdd(metadataUrl, key =>
1✔
79
            {
1✔
80
                return new ConfigurationManager<OpenIdConnectConfiguration>(metadataUrl, new OpenIdConnectConfigurationRetriever(), httpClient);
1✔
81
            });
1✔
82

83
            _endorsementsData = _endorsementsCache.GetOrAdd(metadataUrl, key =>
1✔
84
            {
1✔
85
                var retriever = new EndorsementsRetriever(httpClient);
1✔
86
                return new ConfigurationManager<IDictionary<string, HashSet<string>>>(metadataUrl, retriever, retriever);
1✔
87
            });
1✔
88
        }
1✔
89

90
        /// <summary>
91
        /// Initializes a new instance of the <see cref="JwtTokenExtractor"/> class.
92
        /// Extracts relevant data from JWT Tokens.
93
        /// </summary>
94
        /// <param name="httpClient">As part of validating JWT Tokens, endorsements need to be fetched from
95
        /// sources specified by the relevant security URLs. This HttpClient is used to allow for resource
96
        /// pooling around those retrievals. As those resources require TLS sharing the HttpClient is
97
        /// important to overall performance.</param>
98
        /// <param name="tokenValidationParameters">tokenValidationParameters.</param>
99
        /// <param name="metadataUrl">metadataUrl.</param>
100
        /// <param name="allowedSigningAlgorithms">allowedSigningAlgorithms.</param>
101
        /// <param name="customEndorsementsConfig">Custom endorsement configuration to be used by the JwtTokenExtractor.</param>
102
        public JwtTokenExtractor(
×
103
            HttpClient httpClient,
×
104
            TokenValidationParameters tokenValidationParameters,
×
105
            string metadataUrl,
×
106
            HashSet<string> allowedSigningAlgorithms,
×
107
            ConfigurationManager<IDictionary<string, HashSet<string>>> customEndorsementsConfig)
×
108
        {
109
            // Make our own copy so we can edit it
110
            _tokenValidationParameters = tokenValidationParameters.Clone();
×
111
            _tokenValidationParameters.RequireSignedTokens = true;
×
112
            _allowedSigningAlgorithms = allowedSigningAlgorithms;
×
113

114
            _openIdMetadata = _openIdMetadataCache.GetOrAdd(metadataUrl, key =>
×
115
            {
×
116
                return new ConfigurationManager<OpenIdConnectConfiguration>(metadataUrl, new OpenIdConnectConfigurationRetriever(), httpClient);
×
117
            });
×
118

119
            _endorsementsData = customEndorsementsConfig ?? throw new ArgumentNullException(nameof(customEndorsementsConfig));
×
120
        }
×
121

122
        /// <summary>
123
        /// Gets the claims identity associated with a request.
124
        /// </summary>
125
        /// <param name="authorizationHeader">The raw HTTP header in the format: "Bearer [longString]".</param>
126
        /// <param name="channelId">The Id of the channel being validated in the original request.</param>
127
        /// <returns>A <see cref="Task{ClaimsIdentity}"/> object.</returns>
128
        public async Task<ClaimsIdentity> GetIdentityAsync(string authorizationHeader, string channelId)
129
        {
130
            return await GetIdentityAsync(authorizationHeader, channelId, Array.Empty<string>()).ConfigureAwait(false);
1✔
131
        }
1✔
132

133
        /// <summary>
134
        /// Gets the claims identity associated with a request.
135
        /// </summary>
136
        /// <param name="authorizationHeader">The raw HTTP header in the format: "Bearer [longString]".</param>
137
        /// <param name="channelId">The Id of the channel being validated in the original request.</param>
138
        /// <param name="requiredEndorsements">The required JWT endorsements.</param>
139
        /// <returns>A <see cref="Task{ClaimsIdentity}"/> object.</returns>
140
        public async Task<ClaimsIdentity> GetIdentityAsync(string authorizationHeader, string channelId, string[] requiredEndorsements)
141
        {
142
            if (authorizationHeader == null)
1✔
143
            {
144
                return null;
×
145
            }
146

147
            string[] parts = authorizationHeader?.Split(' ');
×
148
            if (parts.Length == 2)
1✔
149
            {
150
                return await GetIdentityAsync(parts[0], parts[1], channelId, requiredEndorsements).ConfigureAwait(false);
1✔
151
            }
152

153
            return null;
×
154
        }
1✔
155

156
        /// <summary>
157
        /// Gets the claims identity associated with a request.
158
        /// </summary>
159
        /// <param name="scheme">The associated scheme.</param>
160
        /// <param name="parameter">The token.</param>
161
        /// <param name="channelId">The Id of the channel being validated in the original request.</param>
162
        /// <returns>A <see cref="Task{ClaimsIdentity}"/> object.</returns>
163
        public async Task<ClaimsIdentity> GetIdentityAsync(string scheme, string parameter, string channelId)
164
        {
165
            return await GetIdentityAsync(scheme, parameter, channelId, Array.Empty<string>()).ConfigureAwait(false);
×
166
        }
×
167

168
        /// <summary>
169
        /// Gets the claims identity associated with a request.
170
        /// </summary>
171
        /// <param name="scheme">The associated scheme.</param>
172
        /// <param name="parameter">The token.</param>
173
        /// <param name="channelId">The Id of the channel being validated in the original request.</param>
174
        /// <param name="requiredEndorsements">The required JWT endorsements.</param>
175
        /// <returns>A <see cref="Task{ClaimsIdentity}"/> object.</returns>
176
        public async Task<ClaimsIdentity> GetIdentityAsync(string scheme, string parameter, string channelId, string[] requiredEndorsements)
177
        {
178
            if (requiredEndorsements == null)
1✔
179
            {
180
                throw new ArgumentNullException(nameof(requiredEndorsements));
×
181
            }
182

183
            // No header in correct scheme or no token
184
            if (scheme != "Bearer" || string.IsNullOrEmpty(parameter))
1✔
185
            {
186
                return null;
×
187
            }
188

189
            // Issuer isn't allowed? No need to check signature
190
            if (!HasAllowedIssuer(parameter))
1✔
191
            {
192
                return null;
×
193
            }
194

195
            try
196
            {
197
                var claimsPrincipal = await ValidateTokenAsync(parameter, channelId, requiredEndorsements).ConfigureAwait(false);
1✔
198
                return claimsPrincipal.Identities.OfType<ClaimsIdentity>().FirstOrDefault();
1✔
199
            }
200
            catch (Exception e)
1✔
201
            {
202
                Trace.TraceWarning("Invalid token. " + e.ToString());
1✔
203
                throw;
1✔
204
            }
205
        }
1✔
206

207
        private bool HasAllowedIssuer(string jwtToken)
208
        {
209
            if (!_tokenValidationParameters.ValidateIssuer)
1✔
210
            {
211
                return true;
1✔
212
            }
213

214
            JwtSecurityToken token = new JwtSecurityToken(jwtToken);
1✔
215

216
            if (_tokenValidationParameters.ValidIssuer != null && _tokenValidationParameters.ValidIssuer == token.Issuer)
×
217
            {
218
                return true;
×
219
            }
220

221
            if ((_tokenValidationParameters.ValidIssuers ?? Enumerable.Empty<string>()).Contains(token.Issuer))
×
222
            {
223
                return true;
1✔
224
            }
225

226
            return false;
×
227
        }
228

229
        private async Task<ClaimsPrincipal> ValidateTokenAsync(string jwtToken, string channelId, string[] requiredEndorsements)
230
        {
231
            if (requiredEndorsements == null)
1✔
232
            {
233
                throw new ArgumentNullException(nameof(requiredEndorsements));
×
234
            }
235

236
            // _openIdMetadata only does a full refresh when the cache expires every 5 days
237
            OpenIdConnectConfiguration config = null;
1✔
238
            try
239
            {
240
                config = await _openIdMetadata.GetConfigurationAsync().ConfigureAwait(false);
1✔
241
            }
1✔
242
            catch (Exception e)
×
243
            {
244
                Trace.TraceError($"Error refreshing OpenId configuration: {e}");
×
245

246
                // No config? We can't continue
247
                if (config == null)
×
248
                {
249
                    throw;
×
250
                }
251
            }
×
252

253
            // Update the signing tokens from the last refresh
254
            _tokenValidationParameters.IssuerSigningKeys = config.SigningKeys;
1✔
255
            var tokenHandler = new JwtSecurityTokenHandler();
1✔
256

257
            try
258
            {
259
                var principal = tokenHandler.ValidateToken(jwtToken, _tokenValidationParameters, out SecurityToken parsedToken);
1✔
260
                var parsedJwtToken = parsedToken as JwtSecurityToken;
1✔
261

262
                // Validate Channel / Token Endorsements. For this, the channelID present on the Activity
263
                // needs to be matched by an endorsement.
264
                var keyId = (string)parsedJwtToken?.Header?[AuthenticationConstants.KeyIdHeader];
×
265
                var endorsements = await _endorsementsData.GetConfigurationAsync().ConfigureAwait(false);
1✔
266

267
                // Note: On the Emulator Code Path, the endorsements collection is empty so the validation code
268
                // below won't run. This is normal.
269
                if (!string.IsNullOrEmpty(keyId) && endorsements.TryGetValue(keyId, out var endorsementsForKey))
1✔
270
                {
271
                    // Verify that channelId is included in endorsements
272
                    var isEndorsed = EndorsementsValidator.Validate(channelId, endorsementsForKey);
×
273

274
                    if (!isEndorsed)
×
275
                    {
276
                        throw new UnauthorizedAccessException($"Could not validate endorsement for key: {keyId} with endorsements: {string.Join(",", endorsementsForKey)}");
×
277
                    }
278

279
                    // Verify that additional endorsements are satisfied. If no additional endorsements are expected, the requirement is satisfied as well
280
                    var additionalEndorsementsSatisfied = requiredEndorsements.All(
×
281
                            endorsement => EndorsementsValidator.Validate(endorsement, endorsementsForKey));
×
282

283
                    if (!additionalEndorsementsSatisfied)
×
284
                    {
285
                        throw new UnauthorizedAccessException($"Could not validate additional endorsement for key: {keyId} with endorsements: {string.Join(",", endorsementsForKey)}. Expected endorsements: {string.Join(",", requiredEndorsements)}");
×
286
                    }
287
                }
288

289
                if (_allowedSigningAlgorithms != null)
1✔
290
                {
291
                    var algorithm = parsedJwtToken?.Header?.Alg;
×
292
                    if (!_allowedSigningAlgorithms.Contains(algorithm))
1✔
293
                    {
294
                        throw new UnauthorizedAccessException($"Token signing algorithm '{algorithm}' not in allowed list");
×
295
                    }
296
                }
297

298
                return principal;
1✔
299
            }
300
            catch (SecurityTokenExpiredException err)
301
            {
302
                Trace.TraceError(err.Message);
1✔
303
                throw new UnauthorizedAccessException($"The token has expired");
1✔
304
            }
305
            catch (SecurityTokenSignatureKeyNotFoundException)
×
306
            {
307
                var keys = string.Join(", ", (config?.SigningKeys ?? Enumerable.Empty<SecurityKey>()).Select(t => t.KeyId));
×
308
                Trace.TraceError("Error finding key for token. Available keys: " + keys);
×
309
                throw;
×
310
            }
311
        }
1✔
312
    }
313
}
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