Skip to content

Commit b4fe386

Browse files
brentschmaltzBrent Schmaltz
and
Brent Schmaltz
committed
Added JsonWebTokenHandler and TokenHandlers (#48857)
Co-authored-by: Brent Schmaltz <[email protected]>
1 parent 99afe5e commit b4fe386

File tree

7 files changed

+271
-78
lines changed

7 files changed

+271
-78
lines changed

src/Security/Authentication/JwtBearer/src/AuthenticateResults.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer;
66
internal static class AuthenticateResults
77
{
88
internal static AuthenticateResult ValidatorNotFound = AuthenticateResult.Fail("No SecurityTokenValidator available for token.");
9+
internal static AuthenticateResult TokenHandlerUnableToValidate = AuthenticateResult.Fail("No TokenHandler was able to validate the token.");
910
}

src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs

Lines changed: 106 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Globalization;
56
using System.Linq;
67
using System.Security.Claims;
@@ -96,79 +97,86 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
9697
}
9798
}
9899

99-
if (_configuration == null && Options.ConfigurationManager != null)
100-
{
101-
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
102-
}
103-
104-
var validationParameters = Options.TokenValidationParameters.Clone();
105-
if (_configuration != null)
106-
{
107-
var issuers = new[] { _configuration.Issuer };
108-
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;
109-
110-
validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys)
111-
?? _configuration.SigningKeys;
112-
}
113-
100+
var tvp = await SetupTokenValidationParameters();
114101
List<Exception>? validationFailures = null;
115102
SecurityToken? validatedToken = null;
116-
foreach (var validator in Options.SecurityTokenValidators)
103+
ClaimsPrincipal? principal = null;
104+
105+
if (Options.UseTokenHandlers)
117106
{
118-
if (validator.CanReadToken(token))
107+
foreach (var tokenHandler in Options.TokenHandlers)
119108
{
120-
ClaimsPrincipal principal;
121109
try
122110
{
123-
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
111+
var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tvp);
112+
if (tokenValidationResult.IsValid)
113+
{
114+
principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity);
115+
validatedToken = tokenValidationResult.SecurityToken;
116+
break;
117+
}
118+
else
119+
{
120+
validationFailures ??= new List<Exception>(1);
121+
RecordTokenValidationError(tokenValidationResult.Exception ?? new SecurityTokenValidationException($"The TokenHandler: '{tokenHandler}', was unable to validate the Token."), validationFailures);
122+
}
124123
}
125124
catch (Exception ex)
126125
{
127-
Logger.TokenValidationFailed(ex);
128-
129-
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
130-
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
131-
&& ex is SecurityTokenSignatureKeyNotFoundException)
126+
validationFailures ??= new List<Exception>(1);
127+
RecordTokenValidationError(new SecurityTokenValidationException($"TokenHandler: '{tokenHandler}', threw an exception (see inner exception).", ex), validationFailures);
128+
}
129+
}
130+
}
131+
else
132+
{
133+
foreach (var validator in Options.SecurityTokenValidators)
134+
{
135+
if (validator.CanReadToken(token))
136+
{
137+
try
132138
{
133-
Options.ConfigurationManager.RequestRefresh();
139+
principal = validator.ValidateToken(token, tvp, out validatedToken);
134140
}
135-
136-
if (validationFailures == null)
141+
catch (Exception ex)
137142
{
138-
validationFailures = new List<Exception>(1);
143+
validationFailures ??= new List<Exception>(1);
144+
RecordTokenValidationError(ex, validationFailures);
145+
continue;
139146
}
140-
validationFailures.Add(ex);
141-
continue;
142147
}
148+
}
149+
}
143150

144-
Logger.TokenValidationSucceeded();
151+
if (principal != null && validatedToken != null)
152+
{
153+
Logger.TokenValidationSucceeded();
145154

146-
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
147-
{
148-
Principal = principal,
149-
SecurityToken = validatedToken
150-
};
155+
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
156+
{
157+
Principal = principal
158+
};
151159

152-
tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo);
153-
tokenValidatedContext.Properties.IssuedUtc = GetSafeDateTime(validatedToken.ValidFrom);
160+
tokenValidatedContext.SecurityToken = validatedToken;
161+
tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo);
162+
tokenValidatedContext.Properties.IssuedUtc = GetSafeDateTime(validatedToken.ValidFrom);
154163

155-
await Events.TokenValidated(tokenValidatedContext);
156-
if (tokenValidatedContext.Result != null)
157-
{
158-
return tokenValidatedContext.Result;
159-
}
164+
await Events.TokenValidated(tokenValidatedContext);
165+
if (tokenValidatedContext.Result != null)
166+
{
167+
return tokenValidatedContext.Result;
168+
}
160169

161-
if (Options.SaveToken)
170+
if (Options.SaveToken)
171+
{
172+
tokenValidatedContext.Properties.StoreTokens(new[]
162173
{
163-
tokenValidatedContext.Properties.StoreTokens(new[]
164-
{
165-
new AuthenticationToken { Name = "access_token", Value = token }
166-
});
167-
}
168-
169-
tokenValidatedContext.Success();
170-
return tokenValidatedContext.Result!;
174+
new AuthenticationToken { Name = "access_token", Value = token }
175+
});
171176
}
177+
178+
tokenValidatedContext.Success();
179+
return tokenValidatedContext.Result!;
172180
}
173181

174182
if (validationFailures != null)
@@ -187,6 +195,11 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
187195
return AuthenticateResult.Fail(authenticationFailedContext.Exception);
188196
}
189197

198+
if (Options.UseTokenHandlers)
199+
{
200+
return AuthenticateResults.TokenHandlerUnableToValidate;
201+
}
202+
190203
return AuthenticateResults.ValidatorNotFound;
191204
}
192205
catch (Exception ex)
@@ -208,6 +221,47 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
208221
}
209222
}
210223

224+
private void RecordTokenValidationError(Exception exception, List<Exception> exceptions)
225+
{
226+
if (exception != null)
227+
{
228+
Logger.TokenValidationFailed(exception);
229+
exceptions.Add(exception);
230+
}
231+
232+
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
233+
// Refreshing on SecurityTokenSignatureKeyNotFound may be redundant if Last-Known-Good is enabled, it won't do much harm, most likely will be a nop.
234+
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
235+
&& exception is SecurityTokenSignatureKeyNotFoundException)
236+
{
237+
Options.ConfigurationManager.RequestRefresh();
238+
}
239+
}
240+
241+
private async Task<TokenValidationParameters> SetupTokenValidationParameters()
242+
{
243+
// Clone to avoid cross request race conditions for updated configurations.
244+
var tokenValidationParameters = Options.TokenValidationParameters.Clone();
245+
246+
if (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager)
247+
{
248+
tokenValidationParameters.ConfigurationManager = baseConfigurationManager;
249+
}
250+
else
251+
{
252+
if (Options.ConfigurationManager != null)
253+
{
254+
// GetConfigurationAsync has a time interval that must pass before new http request will be issued.
255+
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
256+
var issuers = new[] { _configuration.Issuer };
257+
tokenValidationParameters.ValidIssuers = (tokenValidationParameters.ValidIssuers == null ? issuers : tokenValidationParameters.ValidIssuers.Concat(issuers));
258+
tokenValidationParameters.IssuerSigningKeys = (tokenValidationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : tokenValidationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys));
259+
}
260+
}
261+
262+
return tokenValidationParameters;
263+
}
264+
211265
private static DateTime? GetSafeDateTime(DateTime dateTime)
212266
{
213267
// Assigning DateTime.MinValue or default(DateTime) to a DateTimeOffset when in a UTC+X timezone will throw

src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Net.Http;
66
using Microsoft.IdentityModel.Protocols;
77
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
8+
using Microsoft.IdentityModel.JsonWebTokens;
89
using Microsoft.IdentityModel.Tokens;
910

1011
namespace Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -15,13 +16,16 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer;
1516
public class JwtBearerOptions : AuthenticationSchemeOptions
1617
{
1718
private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler();
19+
private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler();
1820

1921
/// <summary>
2022
/// Initializes a new instance of <see cref="JwtBearerOptions"/>.
2123
/// </summary>
2224
public JwtBearerOptions()
2325
{
2426
SecurityTokenValidators = new List<ISecurityTokenValidator> { _defaultHandler };
27+
// TODO - communicate to IdentityModel team to see if ITokenValidator interface can be created.
28+
TokenHandlers = new List<TokenHandler> { _defaultTokenHandler };
2529
}
2630

2731
/// <summary>
@@ -105,6 +109,11 @@ public JwtBearerOptions()
105109
/// </summary>
106110
public IList<ISecurityTokenValidator> SecurityTokenValidators { get; private set; }
107111

112+
/// <summary>
113+
/// Gets the ordered list of <see cref="TokenHandler"/> used to validate access tokens.
114+
/// </summary>
115+
public IList<TokenHandler> TokenHandlers { get; private set; }
116+
108117
/// <summary>
109118
/// Gets or sets the parameters used to validate identity tokens.
110119
/// </summary>
@@ -152,4 +161,13 @@ public bool MapInboundClaims
152161
/// Defaults to <see cref="ConfigurationManager{OpenIdConnectConfiguration}.DefaultRefreshInterval" />.
153162
/// </value>
154163
public TimeSpan RefreshInterval { get; set; } = ConfigurationManager<OpenIdConnectConfiguration>.DefaultRefreshInterval;
164+
165+
/// <summary>
166+
/// Gets of sets the <see cref="UseTokenHandlers"/> property to control if <see cref="TokenHandlers"/> or <see cref="SecurityTokenValidators"/> will be used to validate the inbound token.
167+
/// The advantage of using the TokenHandlers is:
168+
/// <para>There is an Async model.</para>
169+
/// <para>The default token handler is a <see cref="JsonWebTokenHandler"/> which is 30 % faster when validating.</para>
170+
/// <para>There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors.</para>
171+
/// </summary>
172+
public bool UseTokenHandlers { get; set; }
155173
}

src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.RequireHttpsMetad
7272
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SaveToken.get -> bool
7373
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SaveToken.set -> void
7474
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SecurityTokenValidators.get -> System.Collections.Generic.IList<Microsoft.IdentityModel.Tokens.ISecurityTokenValidator!>!
75+
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenHandlers.get -> System.Collections.Generic.IList<Microsoft.IdentityModel.Tokens.TokenHandler!>!
7576
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters!
7677
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenValidationParameters.set -> void
78+
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.get -> bool
79+
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.set -> void
7780
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions
7881
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.JwtBearerPostConfigureOptions() -> void
7982
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void

src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,14 @@ Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.SkipUnrecog
106106
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.SkipUnrecognizedRequests.set -> void
107107
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.StateDataFormat.get -> Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties!>!
108108
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.StateDataFormat.set -> void
109+
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.get -> System.Collections.Generic.ICollection<Microsoft.IdentityModel.Tokens.TokenHandler!>!
110+
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.set -> void
109111
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters!
110112
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenValidationParameters.set -> void
111113
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenLifetime.get -> bool
112114
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenLifetime.set -> void
115+
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.get -> bool
116+
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.set -> void
113117
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.Wreply.get -> string?
114118
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.Wreply.set -> void
115119
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.WsFederationOptions() -> void

0 commit comments

Comments
 (0)