Skip to content

Added JsonWebTokenHandler and TokenHandlers #48857

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer;
internal static class AuthenticateResults
{
internal static AuthenticateResult ValidatorNotFound = AuthenticateResult.Fail("No SecurityTokenValidator available for token.");
internal static AuthenticateResult TokenHandlerUnableToValidate = AuthenticateResult.Fail("No TokenHandler was able to validate the token.");
}
172 changes: 118 additions & 54 deletions src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,79 +96,82 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
}
}

if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}

var validationParameters = Options.TokenValidationParameters.Clone();
if (_configuration != null)
{
var issuers = new[] { _configuration.Issuer };
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;

validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys)
?? _configuration.SigningKeys;
}

var tvp = await SetupTokenValidationParameters();
List<Exception>? validationFailures = null;
SecurityToken? validatedToken = null;
foreach (var validator in Options.SecurityTokenValidators)
ClaimsPrincipal? principal = null;

if (Options.UseTokenHandlers)
{
if (validator.CanReadToken(token))
foreach (var tokenHandler in Options.TokenHandlers)
{
ClaimsPrincipal principal;
try
var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tvp);
if (tokenValidationResult.IsValid)
{
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity);
validatedToken = tokenValidationResult.SecurityToken;
break;
}
catch (Exception ex)
else
{
Logger.TokenValidationFailed(ex);

// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
&& ex is SecurityTokenSignatureKeyNotFoundException)
// TODO - what to log if there is no exception.
if (tokenValidationResult.Exception != null)
{
Options.ConfigurationManager.RequestRefresh();
validationFailures ??= new List<Exception>(1);
RecordTokenValidationError(tokenValidationResult.Exception, validationFailures);
}

if (validationFailures == null)
}
}
}
else
{
foreach (var validator in Options.SecurityTokenValidators)
{
if (validator.CanReadToken(token))
{
try
{
principal = validator.ValidateToken(token, tvp, out validatedToken);
}
catch (Exception ex)
{
validationFailures = new List<Exception>(1);
validationFailures ??= new List<Exception>(1);
RecordTokenValidationError(ex, validationFailures);
continue;
}
validationFailures.Add(ex);
continue;
}
}
}

Logger.TokenValidationSucceeded();
if (principal != null && validatedToken != null)
{
Logger.TokenValidationSucceeded();

var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
{
Principal = principal,
SecurityToken = validatedToken
};
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
{
Principal = principal
};

tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo);
tokenValidatedContext.Properties.IssuedUtc = GetSafeDateTime(validatedToken.ValidFrom);
tokenValidatedContext.SecurityToken = validatedToken;
tokenValidatedContext.Properties.ExpiresUtc = GetSafeDateTime(validatedToken.ValidTo);
tokenValidatedContext.Properties.IssuedUtc = GetSafeDateTime(validatedToken.ValidFrom);

await Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
{
return tokenValidatedContext.Result;
}
await Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
{
return tokenValidatedContext.Result;
}

if (Options.SaveToken)
if (Options.SaveToken)
{
tokenValidatedContext.Properties.StoreTokens(new[]
{
tokenValidatedContext.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = "access_token", Value = token }
});
}

tokenValidatedContext.Success();
return tokenValidatedContext.Result!;
new AuthenticationToken { Name = "access_token", Value = token }
});
}

tokenValidatedContext.Success();
return tokenValidatedContext.Result!;
}

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

if (Options.UseTokenHandlers)
{
return AuthenticateResults.TokenHandlerUnableToValidate;
}

return AuthenticateResults.ValidatorNotFound;
}
catch (Exception ex)
Expand All @@ -208,6 +216,62 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
}
}

// TODO - this method could be shared across OIDC, WsFed, JwtBearer to ensure consistency
private void RecordTokenValidationError(Exception exception, List<Exception> exceptions)
{
if (exception != null)
{
Logger.TokenValidationFailed(exception);
exceptions.Add(exception);
}

// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
// Refreshing on SecurityTokenSignatureKeyNotFound may be redundant if Last-Known-Good is enabled, it won't do much harm, most likely will be a nop.

if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
&& exception is SecurityTokenSignatureKeyNotFoundException)
{
Options.ConfigurationManager.RequestRefresh();
}
}

// TODO - this method could be shared across OIDC, WsFed, JwtBearer to ensure consistency
private async Task<TokenValidationParameters> SetupTokenValidationParameters()
{
// Clone to avoid cross request race conditions for updated configurations.
var tokenValidationParameters = Options.TokenValidationParameters.Clone();

if (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager)
{
// TODO - we need to add a parameter to TokenValidationParameters for the CancellationToken.
tokenValidationParameters.ConfigurationManager = baseConfigurationManager;
}
else
{
if (Options.ConfigurationManager != null)
{

// TODO - understand existing code:
// ConfigurationManager has a refresh interval of 12 hours by default and will only make an https call every 12 hours.
// Not sure where it would ever get updated before

// if (_configuration == null && Options.ConfigurationManager != null)
// {
// _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
// }

// it is safe to just call GetConfigurationAsync
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);

var issuers = new[] { _configuration.Issuer };
tokenValidationParameters.ValidIssuers = (tokenValidationParameters.ValidIssuers == null ? issuers : tokenValidationParameters.ValidIssuers.Concat(issuers));
tokenValidationParameters.IssuerSigningKeys = (tokenValidationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : tokenValidationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys));
}
}

return tokenValidationParameters;
}

private static DateTime? GetSafeDateTime(DateTime dateTime)
{
// Assigning DateTime.MinValue or default(DateTime) to a DateTimeOffset when in a UTC+X timezone will throw
Expand Down
17 changes: 17 additions & 0 deletions src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Net.Http;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.AspNetCore.Authentication.JwtBearer;
Expand All @@ -15,13 +16,15 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer;
public class JwtBearerOptions : AuthenticationSchemeOptions
{
private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler();
private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler();

/// <summary>
/// Initializes a new instance of <see cref="JwtBearerOptions"/>.
/// </summary>
public JwtBearerOptions()
{
SecurityTokenValidators = new List<ISecurityTokenValidator> { _defaultHandler };
TokenHandlers = new List<TokenHandler> { _defaultTokenHandler };
}

/// <summary>
Expand Down Expand Up @@ -105,6 +108,11 @@ public JwtBearerOptions()
/// </summary>
public IList<ISecurityTokenValidator> SecurityTokenValidators { get; private set; }

/// <summary>
/// Gets the ordered list of <see cref="TokenHandler"/> used to validate access tokens.
/// </summary>
public IList<TokenHandler> TokenHandlers { get; private set; }

/// <summary>
/// Gets or sets the parameters used to validate identity tokens.
/// </summary>
Expand Down Expand Up @@ -152,4 +160,13 @@ public bool MapInboundClaims
/// Defaults to <see cref="ConfigurationManager{OpenIdConnectConfiguration}.DefaultRefreshInterval" />.
/// </value>
public TimeSpan RefreshInterval { get; set; } = ConfigurationManager<OpenIdConnectConfiguration>.DefaultRefreshInterval;

/// <summary>
/// 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.
/// The advantage of using the TokenHandlers is:
/// <para>There is an Async model.</para>
/// <para>The default token handler is a <see cref="JsonWebTokenHandler"/> which is 30 % faster when validating.</para>
/// <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>
/// </summary>
public bool UseTokenHandlers { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.RequireHttpsMetad
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SaveToken.get -> bool
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SaveToken.set -> void
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.SecurityTokenValidators.get -> System.Collections.Generic.IList<Microsoft.IdentityModel.Tokens.ISecurityTokenValidator!>!
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenHandlers.get -> System.Collections.Generic.IList<Microsoft.IdentityModel.Tokens.TokenHandler!>!
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters!
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenValidationParameters.set -> void
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.get -> bool
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.set -> void
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.JwtBearerPostConfigureOptions() -> void
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,14 @@ Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.SkipUnrecog
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.SkipUnrecognizedRequests.set -> void
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.StateDataFormat.get -> Microsoft.AspNetCore.Authentication.ISecureDataFormat<Microsoft.AspNetCore.Authentication.AuthenticationProperties!>!
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.StateDataFormat.set -> void
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.get -> System.Collections.Generic.ICollection<Microsoft.IdentityModel.Tokens.TokenHandler!>!
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.set -> void
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters!
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenValidationParameters.set -> void
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenLifetime.get -> bool
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenLifetime.set -> void
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.get -> bool
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.set -> void
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.Wreply.get -> string?
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.Wreply.set -> void
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.WsFederationOptions() -> void
Expand Down
Loading