diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 5321d385e2f9..2691385519be 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -185,9 +185,9 @@
https://github.com/dotnet/runtime40480e8e82b734a54c210f656361ff073353ffbe
-
+ https://github.com/dotnet/source-build-externals
- 76026f9224bd83ede7b2f494912694a30169c233
+ 844e2cd86e7525d7eb32358e63a0c554187eb26b
diff --git a/eng/Versions.props b/eng/Versions.props
index 17702f1b2145..20e7d209e43a 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -11,6 +11,7 @@
07true
+ 7.0.0-preview
@@ -162,7 +163,7 @@
8.0.0-beta.23361.2
- 8.0.0-alpha.1.23362.1
+ 8.0.0-alpha.1.23368.18.0.0-alpha.1.23362.3
@@ -254,15 +255,15 @@
1.1.2-beta1.22531.11.1.2-beta1.22531.11.0.0-20230414.1
- 6.15.1
- 6.15.1
- 6.15.1
+ $(IdentityModelVersion)
+ $(IdentityModelVersion)
+ $(IdentityModelVersion)2.2.11.0.13.0.13.0.111.1.0
- 6.21.0
+ $(IdentityModelVersion)5.0.05.0.0-alpha.20560.65.0.0
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..c880cb555380 100644
--- a/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs
+++ b/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs
@@ -9,7 +9,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Net.Http.Headers;
@@ -20,8 +19,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer;
///
public class JwtBearerHandler : AuthenticationHandler
{
- private OpenIdConnectConfiguration? _configuration;
-
///
/// Initializes a new instance of .
///
@@ -96,79 +93,88 @@ 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 SetupTokenValidationParametersAsync();
List? validationFailures = null;
SecurityToken? validatedToken = null;
- foreach (var validator in Options.SecurityTokenValidators)
+ ClaimsPrincipal? principal = null;
+
+ if (!Options.UseSecurityTokenValidators)
{
- if (validator.CanReadToken(token))
+ foreach (var tokenHandler in Options.TokenHandlers)
{
- ClaimsPrincipal principal;
try
{
- principal = validator.ValidateToken(token, validationParameters, out validatedToken);
+ var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tvp);
+ if (tokenValidationResult.IsValid)
+ {
+ principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity);
+ validatedToken = tokenValidationResult.SecurityToken;
+ break;
+ }
+ else
+ {
+ validationFailures ??= new List(1);
+ RecordTokenValidationError(tokenValidationResult.Exception ?? new SecurityTokenValidationException($"The TokenHandler: '{tokenHandler}', was unable to validate the Token."), validationFailures);
+ }
}
catch (Exception ex)
{
- 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)
+ validationFailures ??= new List(1);
+ RecordTokenValidationError(ex, validationFailures);
+ }
+ }
+ }
+ else
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ foreach (var validator in Options.SecurityTokenValidators)
+ {
+ if (validator.CanReadToken(token))
+ {
+ try
{
- Options.ConfigurationManager.RequestRefresh();
+ principal = validator.ValidateToken(token, tvp, out validatedToken);
}
-
- if (validationFailures == null)
+ catch (Exception ex)
{
- validationFailures = new List(1);
+ validationFailures ??= new List(1);
+ RecordTokenValidationError(ex, validationFailures);
+ continue;
}
- validationFailures.Add(ex);
- continue;
}
+ }
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
- 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 +193,11 @@ protected override async Task HandleAuthenticateAsync()
return AuthenticateResult.Fail(authenticationFailedContext.Exception);
}
+ if (!Options.UseSecurityTokenValidators)
+ {
+ return AuthenticateResults.TokenHandlerUnableToValidate;
+ }
+
return AuthenticateResults.ValidatorNotFound;
}
catch (Exception ex)
@@ -208,6 +219,47 @@ protected override async Task HandleAuthenticateAsync()
}
}
+ 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();
+ }
+ }
+
+ private async Task SetupTokenValidationParametersAsync()
+ {
+ // Clone to avoid cross request race conditions for updated configurations.
+ var tokenValidationParameters = Options.TokenValidationParameters.Clone();
+
+ if (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager)
+ {
+ tokenValidationParameters.ConfigurationManager = baseConfigurationManager;
+ }
+ else
+ {
+ if (Options.ConfigurationManager != null)
+ {
+ // GetConfigurationAsync has a time interval that must pass before new http request will be issued.
+ var 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..f5b56ed32531 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,13 +16,22 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer;
public class JwtBearerOptions : AuthenticationSchemeOptions
{
private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler();
+ private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler
+ {
+ MapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims
+ };
+
+ private bool _mapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims;
///
/// Initializes a new instance of .
///
public JwtBearerOptions()
{
+#pragma warning disable CS0618 // Type or member is obsolete
SecurityTokenValidators = new List { _defaultHandler };
+#pragma warning restore CS0618 // Type or member is obsolete
+ TokenHandlers = new List { _defaultTokenHandler };
}
///
@@ -103,8 +113,14 @@ public JwtBearerOptions()
///
/// Gets the ordered list of used to validate access tokens.
///
+ [Obsolete("SecurityTokenValidators is no longer used by default. Use TokenHandlers instead. To continue using SecurityTokenValidators, set UseSecurityTokenValidators to true. See https://aka.ms/aspnetcore8/security-token-changes")]
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.
///
@@ -126,15 +142,20 @@ public JwtBearerOptions()
public bool IncludeErrorDetails { get; set; } = true;
///
- /// Gets or sets the property on the default instance of in SecurityTokenValidators, which is used when determining
- /// whether or not to map claim types that are extracted when validating a .
+ /// Gets or sets the property on the default instance of in SecurityTokenValidators, or in TokenHandlers, which is used when determining
+ /// whether or not to map claim types that are extracted when validating a or a .
/// If this is set to true, the Claim Type is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs.
/// The default value is true.
///
public bool MapInboundClaims
{
- get => _defaultHandler.MapInboundClaims;
- set => _defaultHandler.MapInboundClaims = value;
+ get => _mapInboundClaims;
+ set
+ {
+ _mapInboundClaims = value;
+ _defaultHandler.MapInboundClaims = value;
+ _defaultTokenHandler.MapInboundClaims = value;
+ }
}
///
@@ -152,4 +173,17 @@ public bool MapInboundClaims
/// Defaults to .
///
public TimeSpan RefreshInterval { get; set; } = ConfigurationManager.DefaultRefreshInterval;
+
+ ///
+ /// Gets or sets whether or will be used to validate the inbound token.
+ ///
+ ///
+ /// The advantages of using TokenHandlers are:
+ /// There is an Async model.
+ /// The default token handler is a which is faster than a .
+ /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors.
+ /// SecurityTokenValidators can be used when needs a .
+ /// When using TokenHandlers, will be a .
+ ///
+ public bool UseSecurityTokenValidators { get; set; }
}
diff --git a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt
index d9d3a043b473..4626d5e95af8 100644
--- a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt
+++ b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt
@@ -1,2 +1,5 @@
#nullable enable
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.JwtBearerHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.UrlEncoder! encoder) -> void
+Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.TokenHandlers.get -> System.Collections.Generic.IList!
+Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseSecurityTokenValidators.get -> bool
+Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseSecurityTokenValidators.set -> void
diff --git a/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs b/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs
index 3c063b244100..7c53f799c443 100644
--- a/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs
+++ b/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs
@@ -266,7 +266,7 @@ await WriteHtmlAsync(response, async res =>
// Persist the new acess token
props.UpdateTokenValue("access_token", payload.RootElement.GetString("access_token"));
props.UpdateTokenValue("refresh_token", payload.RootElement.GetString("refresh_token"));
- if (payload.RootElement.TryGetProperty("expires_in", out var property) && property.TryGetInt32(out var seconds))
+ if (payload.RootElement.TryGetProperty("expires_in", out var property) && int.TryParse(property.GetString(), out var seconds))
{
var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(seconds);
props.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
@@ -283,7 +283,7 @@ await WriteHtmlAsync(response, async res =>
await WriteTableHeader(res, new string[] { "Token Type", "Value" }, props.GetTokens().Select(token => new string[] { token.Name, token.Value }));
await res.WriteAsync("
Payload:
");
- await res.WriteAsync(HtmlEncoder.Default.Encode(payload.ToString()).Replace(",", ", ") + " ");
+ await res.WriteAsync(HtmlEncoder.Default.Encode(payload.RootElement.ToString()).Replace(",", ", ") + " ");
});
}
diff --git a/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs b/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs
index c0cc19bbf12a..3be7240ab4a7 100644
--- a/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs
+++ b/src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs
@@ -164,4 +164,10 @@ internal static partial class LoggingExtensions
[LoggerMessage(55, LogLevel.Error, "The remote signout request was ignored because the 'iss' parameter didn't match " +
"the expected value, which may indicate an unsolicited logout.", EventName = "RemoteSignOutIssuerInvalid")]
public static partial void RemoteSignOutIssuerInvalid(this ILogger logger);
+
+ [LoggerMessage(56, LogLevel.Error, "Unable to validate the 'id_token', no suitable TokenHandler was found for: '{IdToken}'.", EventName = "UnableToValidateIdTokenFromHandler")]
+ public static partial void UnableToValidateIdTokenFromHandler(this ILogger logger, string idToken);
+
+ [LoggerMessage(57, LogLevel.Error, "The Validated Security Token must be of type JsonWebToken, but instead its type is: '{SecurityTokenType}.'", EventName = "InvalidSecurityTokenTypeFromHandler")]
+ public static partial void InvalidSecurityTokenTypeFromHandler(this ILogger logger, Type? securityTokenType);
}
diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
index 1f2c620def33..4b074f5621e3 100644
--- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
+++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
@@ -17,8 +17,10 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
+using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
+using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames;
namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;
@@ -649,7 +651,17 @@ protected override async Task HandleRemoteAuthenticateAsync
if (!string.IsNullOrEmpty(authorizationResponse.IdToken))
{
Logger.ReceivedIdToken();
- user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt);
+
+ if (!Options.UseSecurityTokenValidator)
+ {
+ var tokenValidationResult = await ValidateTokenUsingHandlerAsync(authorizationResponse.IdToken, properties, validationParameters);
+ user = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity);
+ jwt = JwtSecurityTokenConverter.Convert(tokenValidationResult.SecurityToken as JsonWebToken);
+ }
+ else
+ {
+ user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt);
+ }
nonce = jwt.Payload.Nonce;
if (!string.IsNullOrEmpty(nonce))
@@ -717,7 +729,19 @@ protected override async Task HandleRemoteAuthenticateAsync
// At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response.
// And we'll want to validate the new JWT in ValidateTokenResponse.
- var tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out var tokenEndpointJwt);
+ ClaimsPrincipal tokenEndpointUser;
+ JwtSecurityToken tokenEndpointJwt;
+
+ if (!Options.UseSecurityTokenValidator)
+ {
+ var tokenValidationResult = await ValidateTokenUsingHandlerAsync(tokenEndpointResponse.IdToken, properties, validationParameters);
+ tokenEndpointUser = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity);
+ tokenEndpointJwt = JwtSecurityTokenConverter.Convert(tokenValidationResult.SecurityToken as JsonWebToken);
+ }
+ else
+ {
+ tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out tokenEndpointJwt);
+ }
// Avoid reading & deleting the nonce cookie, running the event, etc, if it was already done as part of the authorization response validation.
if (user == null)
@@ -1244,11 +1268,13 @@ private async Task RunAuthenticationFailedEventAsyn
// Note this modifies properties if Options.UseTokenLifetime
private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, out JwtSecurityToken jwt)
{
+#pragma warning disable CS0618 // Type or member is obsolete
if (!Options.SecurityTokenValidator.CanReadToken(idToken))
{
Logger.UnableToReadIdToken(idToken);
throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken));
}
+#pragma warning restore CS0618 // Type or member is obsolete
if (_configuration != null)
{
@@ -1259,7 +1285,9 @@ private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties p
?? _configuration.SigningKeys;
}
+#pragma warning disable CS0618 // Type or member is obsolete
var principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out SecurityToken validatedToken);
+#pragma warning restore CS0618 // Type or member is obsolete
if (validatedToken is JwtSecurityToken validatedJwt)
{
jwt = validatedJwt;
@@ -1294,6 +1322,61 @@ private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties p
return principal;
}
+ // Note this modifies properties if Options.UseTokenLifetime
+ private async Task ValidateTokenUsingHandlerAsync(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters)
+ {
+ if (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager)
+ {
+ validationParameters.ConfigurationManager = baseConfigurationManager;
+ }
+ else if (_configuration != null)
+ {
+ var issuer = new[] { _configuration.Issuer };
+ validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuer) ?? issuer;
+
+ validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys)
+ ?? _configuration.SigningKeys;
+ }
+
+ var validationResult = await Options.TokenHandler.ValidateTokenAsync(idToken, validationParameters);
+
+ if (validationResult.Exception != null)
+ {
+ throw validationResult.Exception;
+ }
+
+ var validatedToken = validationResult.SecurityToken;
+
+ if (!validationResult.IsValid || validatedToken == null)
+ {
+ Logger.UnableToValidateIdTokenFromHandler(idToken);
+ throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateTokenFromHandler, idToken));
+ }
+
+ if (validatedToken is not JsonWebToken)
+ {
+ Logger.InvalidSecurityTokenTypeFromHandler(validatedToken?.GetType());
+ throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJsonWebToken, validatedToken?.GetType()));
+ }
+
+ if (Options.UseTokenLifetime)
+ {
+ var issued = validatedToken.ValidFrom;
+ if (issued != DateTime.MinValue)
+ {
+ properties.IssuedUtc = issued;
+ }
+
+ var expires = validatedToken.ValidTo;
+ if (expires != DateTime.MinValue)
+ {
+ properties.ExpiresUtc = expires;
+ }
+ }
+
+ return validationResult;
+ }
+
///
/// Build a redirect path if the given path is a relative path.
///
diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs
index 7fada340c927..0047d67ea828 100644
--- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs
+++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs
@@ -4,6 +4,7 @@
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
using Microsoft.AspNetCore.Http;
+using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
@@ -17,6 +18,12 @@ public class OpenIdConnectOptions : RemoteAuthenticationOptions
{
private CookieBuilder _nonceCookieBuilder;
private readonly JwtSecurityTokenHandler _defaultHandler = new JwtSecurityTokenHandler();
+ private readonly JsonWebTokenHandler _defaultTokenHandler = new JsonWebTokenHandler
+ {
+ MapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims
+ };
+
+ private bool _mapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims;
///
/// Initializes a new
@@ -37,7 +44,10 @@ public OpenIdConnectOptions()
CallbackPath = new PathString("/signin-oidc");
SignedOutCallbackPath = new PathString("/signout-callback-oidc");
RemoteSignOutPath = new PathString("/signout-oidc");
+#pragma warning disable CS0618 // Type or member is obsolete
SecurityTokenValidator = _defaultHandler;
+#pragma warning restore CS0618 // Type or member is obsolete
+ TokenHandler = _defaultTokenHandler;
Events = new OpenIdConnectEvents();
Scope.Add("openid");
@@ -253,8 +263,17 @@ public override void Validate()
///
/// Gets or sets the used to validate identity tokens.
///
+ [Obsolete("SecurityTokenValidator is no longer used by default. Use TokenHandler instead. To continue using SecurityTokenValidator, set UseSecurityTokenValidator to true. See https://aka.ms/aspnetcore8/security-token-changes")]
public ISecurityTokenValidator SecurityTokenValidator { get; set; }
+ ///
+ /// Gets or sets the used to validate identity tokens.
+ ///
+ /// This will be used instead of if is .
+ ///
+ ///
+ public TokenHandler TokenHandler { get; set; }
+
///
/// Gets or sets the parameters used to validate identity tokens.
///
@@ -353,14 +372,33 @@ public override CookieOptions Build(HttpContext context, DateTimeOffset expiresF
public TimeSpan RefreshInterval { get; set; } = ConfigurationManager.DefaultRefreshInterval;
///
- /// Gets or sets the property on the default instance of in SecurityTokenValidator, which is used when determining
+ /// Gets or sets the property on the default instance of in SecurityTokenValidator
+ /// and default instance of in TokenHandler, which is used when determining
/// whether or not to map claim types that are extracted when validating a .
/// If this is set to true, the Claim Type is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs.
/// The default value is true.
///
public bool MapInboundClaims
{
- get => _defaultHandler.MapInboundClaims;
- set => _defaultHandler.MapInboundClaims = value;
+ get => _mapInboundClaims;
+ set
+ {
+ _mapInboundClaims = value;
+ _defaultHandler.MapInboundClaims = value;
+ _defaultTokenHandler.MapInboundClaims = value;
+ }
}
+
+ ///
+ /// Gets or sets whether to use the or the for validating identity tokens.
+ ///
+ ///
+ /// The advantages of using TokenHandler are:
+ /// There is an Async model.
+ /// The default token handler is a which is faster than a .
+ /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors.
+ /// SecurityTokenValidator can be used when needs a .
+ /// When using TokenHandler, will be a .
+ ///
+ public bool UseSecurityTokenValidator { get; set; }
}
diff --git a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt
index 8ff5e3305e99..d6dbc14fed1e 100644
--- a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt
+++ b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt
@@ -1,2 +1,6 @@
#nullable enable
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.OpenIdConnectHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.HtmlEncoder! htmlEncoder, System.Text.Encodings.Web.UrlEncoder! encoder) -> void
+Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.get -> Microsoft.IdentityModel.Tokens.TokenHandler!
+Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.TokenHandler.set -> void
+Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseSecurityTokenValidator.get -> bool
+Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseSecurityTokenValidator.set -> void
diff --git a/src/Security/Authentication/OpenIdConnect/src/Resources.resx b/src/Security/Authentication/OpenIdConnect/src/Resources.resx
index 7f790fef43f5..b2a996626273 100644
--- a/src/Security/Authentication/OpenIdConnect/src/Resources.resx
+++ b/src/Security/Authentication/OpenIdConnect/src/Resources.resx
@@ -135,4 +135,10 @@
Cannot process the message. Both id_token and code are missing.
+
+ The Validated Security Token must be of type JsonWebToken, but instead its type is '{0}'.
+
+
+ Unable to validate the 'id_token', no suitable TokenHandler was found for: '{0}'."
+
\ No newline at end of file
diff --git a/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt
index 6586d9439638..473f251c9c8a 100644
--- a/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt
+++ b/src/Security/Authentication/WsFederation/src/PublicAPI.Unshipped.txt
@@ -1,2 +1,5 @@
#nullable enable
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationHandler.WsFederationHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.UrlEncoder! encoder) -> void
+Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.TokenHandlers.get -> System.Collections.Generic.ICollection!
+Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseSecurityTokenHandlers.get -> bool
+Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseSecurityTokenHandlers.set -> void
diff --git a/src/Security/Authentication/WsFederation/src/Resources.resx b/src/Security/Authentication/WsFederation/src/Resources.resx
index e2edafb671bd..d006fa4ea32e 100644
--- a/src/Security/Authentication/WsFederation/src/Resources.resx
+++ b/src/Security/Authentication/WsFederation/src/Resources.resx
@@ -121,7 +121,7 @@
The service descriptor is missing.
- No token validator was found for the given token.
+ No token validator or token handler was found for the given token.The '{0}' option must be provided.
diff --git a/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs b/src/Security/Authentication/WsFederation/src/WsFederationHandler.cs
index ca52664613ba..d00103ddb26a 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,87 @@ protected override async Task HandleRemoteAuthenticateAsync
wsFederationMessage = securityTokenReceivedContext.ProtocolMessage;
properties = messageReceivedContext.Properties!;
- if (_configuration == null)
+ var tvp = await SetupTokenValidationParametersAsync();
+ ClaimsPrincipal? principal = null;
+ SecurityToken? validatedToken = null;
+ if (!Options.UseSecurityTokenHandlers)
{
- _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
+ foreach (var tokenHandler in Options.TokenHandlers)
+ {
+ try
+ {
+ var tokenValidationResult = await tokenHandler.ValidateTokenAsync(token, tvp);
+ if (tokenValidationResult.IsValid)
+ {
+ principal = new ClaimsPrincipal(tokenValidationResult.ClaimsIdentity);
+ validatedToken = tokenValidationResult.SecurityToken;
+ break;
+ }
+ else
+ {
+ validationFailures ??= new List(1);
+ Exception exception = tokenValidationResult.Exception ?? new SecurityTokenValidationException($"The TokenHandler: '{tokenHandler}', was unable to validate the Token.");
+ validationFailures.Add(exception);
+ RequestRefresh(exception);
+ }
+ }
+ catch (Exception ex)
+ {
+ validationFailures ??= new List(1);
+ validationFailures.Add(new SecurityTokenValidationException($"TokenHandler: '{tokenHandler}', threw an exception (see inner exception).", ex));
+ RequestRefresh(ex);
+ }
+ }
}
-
- // 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))
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ 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;
+ }
}
+#pragma warning restore CS0618 // Type or member is obsolete
}
if (principal == null)
{
- throw new SecurityTokenException(Resources.Exception_NoTokenValidatorFound);
+ 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 +333,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);
@@ -306,17 +352,13 @@ protected override async Task HandleRemoteAuthenticateAsync
{
Logger.ExceptionProcessingMessage(exception);
- // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification.
- if (Options.RefreshOnIssuerKeyNotFound && exception is SecurityTokenSignatureKeyNotFoundException)
- {
- Options.ConfigurationManager.RequestRefresh();
- }
-
+ RequestRefresh(exception);
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
ProtocolMessage = wsFederationMessage,
Exception = exception
};
+
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
@@ -327,6 +369,41 @@ protected override async Task HandleRemoteAuthenticateAsync
}
}
+ private async Task SetupTokenValidationParametersAsync()
+ {
+ // Clone to avoid cross request race conditions for updated configurations.
+ var tokenValidationParameters = Options.TokenValidationParameters.Clone();
+
+ if (Options.ConfigurationManager is BaseConfigurationManager baseConfigurationManager)
+ {
+ tokenValidationParameters.ConfigurationManager = baseConfigurationManager;
+ }
+ else
+ {
+ if (Options.ConfigurationManager != null)
+ {
+ // GetConfigurationAsync has a time interval that must pass before new http request will be issued.
+ _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 void RequestRefresh(Exception exception)
+ {
+ // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification.
+ // 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 && exception is SecurityTokenSignatureKeyNotFoundException)
+ {
+ Options.ConfigurationManager.RequestRefresh();
+ }
+ }
+
///
/// Handles Signout
///
diff --git a/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs b/src/Security/Authentication/WsFederation/src/WsFederationOptions.cs
index 81ec1f385bd5..ae043451d7bb 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,7 @@ public class WsFederationOptions : RemoteAuthenticationOptions
new SamlSecurityTokenHandler(),
new JwtSecurityTokenHandler()
};
+
private TokenValidationParameters _tokenValidationParameters = new TokenValidationParameters();
///
@@ -37,6 +39,13 @@ public WsFederationOptions()
// If you manage to get it configured, then you can set RemoteSignOutPath accordingly.
RemoteSignOutPath = "/signin-wsfed";
Events = new WsFederationEvents();
+
+ TokenHandlers = new Collection()
+ {
+ new Saml2SecurityTokenHandler(),
+ new SamlSecurityTokenHandler(),
+ new JsonWebTokenHandler{ MapInboundClaims = JwtSecurityTokenHandler.DefaultMapInboundClaims }
+ };
}
///
@@ -96,6 +105,7 @@ public override void Validate()
///
/// Gets or sets the collection of used to read and validate the s.
///
+ [Obsolete("SecurityTokenHandlers is no longer used by default. Use TokenHandlers instead. To continue using SecurityTokenHandlers, set UseSecurityTokenHandlers to true. See https://aka.ms/aspnetcore8/security-token-changes")]
public ICollection SecurityTokenHandlers
{
get
@@ -108,6 +118,14 @@ public ICollection SecurityTokenHandlers
}
}
+ ///
+ /// Gets the collection of used to read and validate the s.
+ ///
+ public ICollection TokenHandlers
+ {
+ get; private set;
+ }
+
///
/// Gets or sets the type used to secure data handled by the middleware.
///
@@ -181,4 +199,17 @@ public TokenValidationParameters TokenValidationParameters
///
[EditorBrowsable(EditorBrowsableState.Never)]
public new bool SaveTokens { get; set; }
+
+ ///
+ /// Gets or sets whether or will be used to validate the inbound token.
+ ///
+ ///
+ /// The advantages of using the TokenHandlers are:
+ /// There is an Async model.
+ /// The default token handler for JsonWebTokens is a which is faster than a .
+ /// There is an ability to make use of a Last-Known-Good model for metadata that protects applications when metadata is published with errors.
+ /// SecurityTokenHandlers can be used when needs a when the security token is a JWT.
+ /// When using TokenHandlers, will be a when the security token is a JWT.
+ ///
+ public bool UseSecurityTokenHandlers { get; set; }
}
diff --git a/src/Security/Authentication/test/JwtBearerTests.cs b/src/Security/Authentication/test/JwtBearerTests.cs
index c6c9268cca26..502afab2db02 100755
--- a/src/Security/Authentication/test/JwtBearerTests.cs
+++ b/src/Security/Authentication/test/JwtBearerTests.cs
@@ -18,6 +18,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
namespace Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -70,6 +71,7 @@ public async Task BearerTokenValidation()
ValidAudience = "audience.contoso.com",
IssuerSigningKey = key,
};
+ o.UseSecurityTokenValidators = true;
});
var newBearerToken = "Bearer " + tokenText;
@@ -107,6 +109,7 @@ public async Task SaveBearerToken()
ValidAudience = "audience.contoso.com",
IssuerSigningKey = key,
};
+ o.UseSecurityTokenValidators = true;
});
var newBearerToken = "Bearer " + tokenText;
@@ -121,9 +124,20 @@ public void MapInboundClaimsDefaultsToTrue()
{
var options = new JwtBearerOptions();
Assert.True(options.MapInboundClaims);
+
+#pragma warning disable CS0618 // Type or member is obsolete
var jwtHandler = options.SecurityTokenValidators.First() as JwtSecurityTokenHandler;
+#pragma warning restore CS0618 // Type or member is obsolete
Assert.NotNull(jwtHandler);
Assert.True(jwtHandler.MapInboundClaims);
+
+ var tokenHandler = options.TokenHandlers.First() as JsonWebTokenHandler;
+ Assert.NotNull(tokenHandler);
+ Assert.True(tokenHandler.MapInboundClaims);
+
+ options.MapInboundClaims = false;
+ Assert.False(jwtHandler.MapInboundClaims);
+ Assert.False(tokenHandler.MapInboundClaims);
}
[Fact]
@@ -132,7 +146,9 @@ public void MapInboundClaimsCanBeSetToFalse()
var options = new JwtBearerOptions();
options.MapInboundClaims = false;
Assert.False(options.MapInboundClaims);
+#pragma warning disable CS0618 // Type or member is obsolete
var jwtHandler = options.SecurityTokenValidators.First() as JwtSecurityTokenHandler;
+#pragma warning restore CS0618 // Type or member is obsolete
Assert.NotNull(jwtHandler);
Assert.False(jwtHandler.MapInboundClaims);
}
@@ -173,8 +189,11 @@ public async Task ThrowAtAuthenticationFailedEvent()
return Task.FromResult(0);
}
};
+ o.UseSecurityTokenValidators = true;
+#pragma warning disable CS0618 // Type or member is obsolete
o.SecurityTokenValidators.Clear();
o.SecurityTokenValidators.Insert(0, new InvalidTokenValidator());
+#pragma warning restore CS0618 // Type or member is obsolete
},
async (context, next) =>
{
@@ -218,6 +237,7 @@ public async Task CustomHeaderReceived()
return Task.FromResult