diff --git a/src/Security/Authentication/JwtBearer/src/AuthenticateResults.cs b/src/Security/Authentication/JwtBearer/src/AuthenticateResults.cs index 5f0e8bb6b914..c66ea3304f1d 100644 --- a/src/Security/Authentication/JwtBearer/src/AuthenticateResults.cs +++ b/src/Security/Authentication/JwtBearer/src/AuthenticateResults.cs @@ -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."); } diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs index ff4767430d57..6dcb3fa9ed8d 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs @@ -96,79 +96,82 @@ protected override async Task 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? 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(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(1); + validationFailures ??= new List(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) @@ -187,6 +190,11 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Fail(authenticationFailedContext.Exception); } + if (Options.UseTokenHandlers) + { + return AuthenticateResults.TokenHandlerUnableToValidate; + } + return AuthenticateResults.ValidatorNotFound; } catch (Exception ex) @@ -208,6 +216,62 @@ protected override async Task HandleAuthenticateAsync() } } + // TODO - this method could be shared across OIDC, WsFed, JwtBearer to ensure consistency + private void RecordTokenValidationError(Exception exception, List 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 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 diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs index db007691f3b9..c5c10a95dfc8 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerOptions.cs @@ -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; @@ -15,6 +16,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer; public class JwtBearerOptions : AuthenticationSchemeOptions { private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler(); + private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler(); /// /// Initializes a new instance of . @@ -22,6 +24,7 @@ public class JwtBearerOptions : AuthenticationSchemeOptions public JwtBearerOptions() { SecurityTokenValidators = new List { _defaultHandler }; + TokenHandlers = new List { _defaultTokenHandler }; } /// @@ -105,6 +108,11 @@ public JwtBearerOptions() /// public IList SecurityTokenValidators { get; private set; } + /// + /// Gets the ordered list of used to validate access tokens. + /// + public IList TokenHandlers { get; private set; } + /// /// Gets or sets the parameters used to validate identity tokens. /// @@ -152,4 +160,13 @@ public bool MapInboundClaims /// Defaults to . /// public TimeSpan RefreshInterval { get; set; } = ConfigurationManager.DefaultRefreshInterval; + + /// + /// Gets of sets the property to control if or will be used to validate the inbound token. + /// The advantage of using the TokenHandlers is: + /// There is an Async model. + /// The default token handler is a which is 30 % faster when validating. + /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. + /// + public bool UseTokenHandlers { get; set; } } diff --git a/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt b/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt index c9aa49918342..e97b1aba38ad 100644 --- a/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt +++ b/src/Security/Authentication/JwtBearer/src/PublicAPI.Shipped.txt @@ -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.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenHandlers.get -> System.Collections.Generic.IList! 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 diff --git a/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt b/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt index 3af5868ef305..b5c4afeada66 100644 --- a/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt +++ b/src/Security/Authentication/WsFederation/src/PublicAPI.Shipped.txt @@ -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.WsFederation.WsFederationOptions.StateDataFormat.set -> void +Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.get -> System.Collections.Generic.ICollection! +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 diff --git a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs index ca52664613ba..0ef76933ee8d 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs @@ -176,6 +176,7 @@ protected override async Task HandleRemoteAuthenticateAsync return HandleRequestResults.NoMessage; } + List? validationFailures = null; try { // Retrieve our cached redirect uri @@ -241,42 +242,78 @@ protected override async Task HandleRemoteAuthenticateAsync wsFederationMessage = securityTokenReceivedContext.ProtocolMessage; properties = messageReceivedContext.Properties!; - if (_configuration == null) + var tvp = await SetupTokenValidationParameters(); + ClaimsPrincipal? principal = null; + SecurityToken? validatedToken = null; + if (Options.UseTokenHandlers) { - _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); + foreach (var tokenHandler in Options.TokenHandlers) + { + var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tvp); + if (tokenValidationResult.IsValid) + { + principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity); + validatedToken = tokenValidationResult.SecurityToken; + break; + } + else + { + if (tokenValidationResult.Exception != null) + { + validationFailures ??= new List(1); + validationFailures.Add(tokenValidationResult.Exception); + } + } + } } - - // Copy and augment to avoid cross request race conditions for updated configurations. - var tvp = Options.TokenValidationParameters.Clone(); - var issuers = new[] { _configuration.Issuer }; - tvp.ValidIssuers = (tvp.ValidIssuers == null ? issuers : tvp.ValidIssuers.Concat(issuers)); - tvp.IssuerSigningKeys = (tvp.IssuerSigningKeys == null ? _configuration.SigningKeys : tvp.IssuerSigningKeys.Concat(_configuration.SigningKeys)); - - ClaimsPrincipal? principal = null; - SecurityToken? parsedToken = null; - foreach (var validator in Options.SecurityTokenHandlers) + else { - if (validator.CanReadToken(token)) + + foreach (var validator in Options.SecurityTokenHandlers) { - principal = validator.ValidateToken(token, tvp, out parsedToken); - break; + if (validator.CanReadToken(token)) + { + try + { + principal = validator.ValidateToken(token, tvp, out validatedToken); + } + catch (Exception ex) + { + validationFailures ??= new List(1); + validationFailures.Add(ex); + continue; + } + break; + } } } if (principal == null) { - throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound); + // TODO - need new string for TokenHandler + if (validationFailures == null || validationFailures.Count == 0) + { + throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound); + } + else if (validationFailures.Count == 1) + { + throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound, validationFailures[0]); + } + else + { + throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound, new AggregateException(validationFailures)); + } } - if (Options.UseTokenLifetime && parsedToken != null) + if (Options.UseTokenLifetime && validatedToken != null) { // Override any session persistence to match the token lifetime. - var issued = parsedToken.ValidFrom; + var issued = validatedToken.ValidFrom; if (issued != DateTime.MinValue) { properties.IssuedUtc = issued.ToUniversalTime(); } - var expires = parsedToken.ValidTo; + var expires = validatedToken.ValidTo; if (expires != DateTime.MinValue) { properties.ExpiresUtc = expires.ToUniversalTime(); @@ -287,7 +324,7 @@ protected override async Task HandleRemoteAuthenticateAsync var securityTokenValidatedContext = new SecurityTokenValidatedContext(Context, Scheme, Options, principal, properties) { ProtocolMessage = wsFederationMessage, - SecurityToken = parsedToken, + SecurityToken = validatedToken, }; await Events.SecurityTokenValidated(securityTokenValidatedContext); @@ -327,6 +364,43 @@ protected override async Task HandleRemoteAuthenticateAsync } } + // TODO - this method could be shared across OIDC, WsFed, JwtBearer to ensure consistency + private async Task 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; + } + /// /// Handles Signout /// diff --git a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs index 81ec1f385bd5..944dd75b9552 100644 --- a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs +++ b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.WsFederation; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens.Saml; using Microsoft.IdentityModel.Tokens.Saml2; @@ -24,6 +25,14 @@ public class WsFederationOptions : RemoteAuthenticationOptions new SamlSecurityTokenHandler(), new JwtSecurityTokenHandler() }; + + private ICollection _tokenHandlers = new Collection() + { + new Saml2SecurityTokenHandler(), + new SamlSecurityTokenHandler(), + new JsonWebTokenHandler() + }; + private TokenValidationParameters _tokenValidationParameters = new TokenValidationParameters(); /// @@ -108,6 +117,21 @@ public ICollection SecurityTokenHandlers } } + /// + /// Gets or sets the collection of used to read and validate the s. + /// + public ICollection TokenHandlers + { + get + { + return _tokenHandlers; + } + set + { + _tokenHandlers = value ?? throw new ArgumentNullException(nameof(TokenHandlers)); + } + } + /// /// Gets or sets the type used to secure data handled by the middleware. /// @@ -181,4 +205,13 @@ public TokenValidationParameters TokenValidationParameters /// [EditorBrowsable(EditorBrowsableState.Never)] public new bool SaveTokens { get; set; } + + /// + /// Gets of sets the property to control if or will be used to validate the inbound token. + /// The advantage of using the TokenHandlers is: + /// There is an Async model. + /// The default token handler is a which is 30 % faster when validating. + /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors. + /// + public bool UseTokenHandlers { get; set; } }