Skip to content

Commit bd84275

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

File tree

7 files changed

+270
-74
lines changed

7 files changed

+270
-74
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: 118 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -96,79 +96,82 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
9696
}
9797
}
9898

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-
99+
var tvp = await SetupTokenValidationParameters();
114100
List<Exception>? validationFailures = null;
115101
SecurityToken? validatedToken = null;
116-
foreach (var validator in Options.SecurityTokenValidators)
102+
ClaimsPrincipal? principal = null;
103+
104+
if (Options.UseTokenHandlers)
117105
{
118-
if (validator.CanReadToken(token))
106+
foreach (var tokenHandler in Options.TokenHandlers)
119107
{
120-
ClaimsPrincipal principal;
121-
try
108+
var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tvp);
109+
if (tokenValidationResult.IsValid)
122110
{
123-
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
111+
principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity);
112+
validatedToken = tokenValidationResult.SecurityToken;
113+
break;
124114
}
125-
catch (Exception ex)
115+
else
126116
{
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)
117+
// TODO - what to log if there is no exception.
118+
if (tokenValidationResult.Exception != null)
132119
{
133-
Options.ConfigurationManager.RequestRefresh();
120+
validationFailures ??= new List<Exception>(1);
121+
RecordTokenValidationError(tokenValidationResult.Exception, validationFailures);
134122
}
135-
136-
if (validationFailures == null)
123+
}
124+
}
125+
}
126+
else
127+
{
128+
foreach (var validator in Options.SecurityTokenValidators)
129+
{
130+
if (validator.CanReadToken(token))
131+
{
132+
try
133+
{
134+
principal = validator.ValidateToken(token, tvp, out validatedToken);
135+
}
136+
catch (Exception ex)
137137
{
138-
validationFailures = new List<Exception>(1);
138+
validationFailures ??= new List<Exception>(1);
139+
RecordTokenValidationError(ex, validationFailures);
140+
continue;
139141
}
140-
validationFailures.Add(ex);
141-
continue;
142142
}
143+
}
144+
}
143145

144-
Logger.TokenValidationSucceeded();
146+
if (principal != null && validatedToken != null)
147+
{
148+
Logger.TokenValidationSucceeded();
145149

146-
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
147-
{
148-
Principal = principal,
149-
SecurityToken = validatedToken
150-
};
150+
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
151+
{
152+
Principal = principal
153+
};
151154

152-
tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo);
153-
tokenValidatedContext.Properties.IssuedUtc = GetSafeDateTime(validatedToken.ValidFrom);
155+
tokenValidatedContext.SecurityToken = validatedToken;
156+
tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo);
157+
tokenValidatedContext.Properties.IssuedUtc = GetSafeDateTime(validatedToken.ValidFrom);
154158

155-
await Events.TokenValidated(tokenValidatedContext);
156-
if (tokenValidatedContext.Result != null)
157-
{
158-
return tokenValidatedContext.Result;
159-
}
159+
await Events.TokenValidated(tokenValidatedContext);
160+
if (tokenValidatedContext.Result != null)
161+
{
162+
return tokenValidatedContext.Result;
163+
}
160164

161-
if (Options.SaveToken)
165+
if (Options.SaveToken)
166+
{
167+
tokenValidatedContext.Properties.StoreTokens(new[]
162168
{
163-
tokenValidatedContext.Properties.StoreTokens(new[]
164-
{
165-
new AuthenticationToken { Name = "access_token", Value = token }
166-
});
167-
}
168-
169-
tokenValidatedContext.Success();
170-
return tokenValidatedContext.Result!;
169+
new AuthenticationToken { Name = "access_token", Value = token }
170+
});
171171
}
172+
173+
tokenValidatedContext.Success();
174+
return tokenValidatedContext.Result!;
172175
}
173176

174177
if (validationFailures != null)
@@ -187,6 +190,11 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
187190
return AuthenticateResult.Fail(authenticationFailedContext.Exception);
188191
}
189192

193+
if (Options.UseTokenHandlers)
194+
{
195+
return AuthenticateResults.TokenHandlerUnableToValidate;
196+
}
197+
190198
return AuthenticateResults.ValidatorNotFound;
191199
}
192200
catch (Exception ex)
@@ -208,6 +216,62 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
208216
}
209217
}
210218

219+
// TODO - this method could be shared across OIDC, WsFed, JwtBearer to ensure consistency
220+
private void RecordTokenValidationError(Exception exception, List<Exception> exceptions)
221+
{
222+
if (exception != null)
223+
{
224+
Logger.TokenValidationFailed(exception);
225+
exceptions.Add(exception);
226+
}
227+
228+
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
229+
// Refreshing on SecurityTokenSignatureKeyNotFound may be redundant if Last-Known-Good is enabled, it won't do much harm, most likely will be a nop.
230+
231+
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
232+
&& exception is SecurityTokenSignatureKeyNotFoundException)
233+
{
234+
Options.ConfigurationManager.RequestRefresh();
235+
}
236+
}
237+
238+
// TODO - this method could be shared across OIDC, WsFed, JwtBearer to ensure consistency
239+
private async Task<TokenValidationParameters> SetupTokenValidationParameters()
240+
{
241+
// Clone to avoid cross request race conditions for updated configurations.
242+
var tokenValidationParameters = Options.TokenValidationParameters.Clone();
243+
244+
if (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager)
245+
{
246+
// TODO - we need to add a parameter to TokenValidationParameters for the CancellationToken.
247+
tokenValidationParameters.ConfigurationManager = baseConfigurationManager;
248+
}
249+
else
250+
{
251+
if (Options.ConfigurationManager != null)
252+
{
253+
254+
// TODO - understand existing code:
255+
// ConfigurationManager has a refresh interval of 12 hours by default and will only make an https call every 12 hours.
256+
// Not sure where it would ever get updated before
257+
258+
// if (_configuration == null && Options.ConfigurationManager != null)
259+
// {
260+
// _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
261+
// }
262+
263+
// it is safe to just call GetConfigurationAsync
264+
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
265+
266+
var issuers = new[] { _configuration.Issuer };
267+
tokenValidationParameters.ValidIssuers = (tokenValidationParameters.ValidIssuers == null ? issuers : tokenValidationParameters.ValidIssuers.Concat(issuers));
268+
tokenValidationParameters.IssuerSigningKeys = (tokenValidationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : tokenValidationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys));
269+
}
270+
}
271+
272+
return tokenValidationParameters;
273+
}
274+
211275
private static DateTime? GetSafeDateTime(DateTime dateTime)
212276
{
213277
// 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: 17 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,15 @@ 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+
TokenHandlers = new List<TokenHandler> { _defaultTokenHandler };
2528
}
2629

2730
/// <summary>
@@ -105,6 +108,11 @@ public JwtBearerOptions()
105108
/// </summary>
106109
public IList<ISecurityTokenValidator> SecurityTokenValidators { get; private set; }
107110

111+
/// <summary>
112+
/// Gets the ordered list of <see cref="TokenHandler"/> used to validate access tokens.
113+
/// </summary>
114+
public IList<TokenHandler> TokenHandlers { get; private set; }
115+
108116
/// <summary>
109117
/// Gets or sets the parameters used to validate identity tokens.
110118
/// </summary>
@@ -152,4 +160,13 @@ public bool MapInboundClaims
152160
/// Defaults to <see cref="ConfigurationManager{OpenIdConnectConfiguration}.DefaultRefreshInterval" />.
153161
/// </value>
154162
public TimeSpan RefreshInterval { get; set; } = ConfigurationManager<OpenIdConnectConfiguration>.DefaultRefreshInterval;
163+
164+
/// <summary>
165+
/// 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.
166+
/// The advantage of using the TokenHandlers is:
167+
/// <para>There is an Async model.</para>
168+
/// <para>The default token handler is a <see cref="JsonWebTokenHandler"/> which is 30 % faster when validating.</para>
169+
/// <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>
170+
/// </summary>
171+
public bool UseTokenHandlers { get; set; }
155172
}

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)