Skip to content
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

Update JwtBearer and WsFederation to use updated IdentityModel #48966

Closed
wants to merge 12 commits into from
9 changes: 5 additions & 4 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<AspNetCorePatchVersion>0</AspNetCorePatchVersion>
<PreReleaseVersionIteration>7</PreReleaseVersionIteration>
<ValidateBaseline>true</ValidateBaseline>
<IdentityModelVersion>6.31.0</IdentityModelVersion>
eerhardt marked this conversation as resolved.
Show resolved Hide resolved
<!--
When StabilizePackageVersion is set to 'true', this branch will produce stable outputs for 'Shipping' packages
-->
Expand Down Expand Up @@ -254,15 +255,15 @@
<MicrosoftCodeAnalysisCSharpAnalyzerTestingXUnitVersion>1.1.2-beta1.22531.1</MicrosoftCodeAnalysisCSharpAnalyzerTestingXUnitVersion>
<MicrosoftCodeAnalysisCSharpCodeFixTestingXUnitVersion>1.1.2-beta1.22531.1</MicrosoftCodeAnalysisCSharpCodeFixTestingXUnitVersion>
<MicrosoftCssParserVersion>1.0.0-20230414.1</MicrosoftCssParserVersion>
<MicrosoftIdentityModelLoggingVersion>6.15.1</MicrosoftIdentityModelLoggingVersion>
<MicrosoftIdentityModelProtocolsOpenIdConnectVersion>6.15.1</MicrosoftIdentityModelProtocolsOpenIdConnectVersion>
<MicrosoftIdentityModelProtocolsWsFederationVersion>6.15.1</MicrosoftIdentityModelProtocolsWsFederationVersion>
<MicrosoftIdentityModelLoggingVersion>$(IdentityModelVersion)</MicrosoftIdentityModelLoggingVersion>
<MicrosoftIdentityModelProtocolsOpenIdConnectVersion>$(IdentityModelVersion)</MicrosoftIdentityModelProtocolsOpenIdConnectVersion>
<MicrosoftIdentityModelProtocolsWsFederationVersion>$(IdentityModelVersion)</MicrosoftIdentityModelProtocolsWsFederationVersion>
<MicrosoftInternalAspNetCoreH2SpecAllVersion>2.2.1</MicrosoftInternalAspNetCoreH2SpecAllVersion>
<MicrosoftNETCoreWindowsApiSetsVersion>1.0.1</MicrosoftNETCoreWindowsApiSetsVersion>
<MicrosoftOwinSecurityCookiesVersion>3.0.1</MicrosoftOwinSecurityCookiesVersion>
<MicrosoftOwinTestingVersion>3.0.1</MicrosoftOwinTestingVersion>
<MicrosoftWebAdministrationVersion>11.1.0</MicrosoftWebAdministrationVersion>
<SystemIdentityModelTokensJwtVersion>6.21.0</SystemIdentityModelTokensJwtVersion>
<SystemIdentityModelTokensJwtVersion>$(IdentityModelVersion)</SystemIdentityModelTokensJwtVersion>
<SystemComponentModelAnnotationsVersion>5.0.0</SystemComponentModelAnnotationsVersion>
<SystemNetExperimentalMsQuicVersion>5.0.0-alpha.20560.6</SystemNetExperimentalMsQuicVersion>
<SystemSecurityPrincipalWindowsVersion>5.0.0</SystemSecurityPrincipalWindowsVersion>
Expand Down
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.");
}
157 changes: 105 additions & 52 deletions src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,79 +96,86 @@ 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 SetupTokenValidationParametersAsync();
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
{
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<Exception>(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<Exception>(1);
RecordTokenValidationError(new SecurityTokenValidationException($"TokenHandler: '{tokenHandler}', threw an exception (see inner exception).", ex), validationFailures);
}
}
}
else
{
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<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 +194,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 +220,47 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
}
}

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();
}
}

private async Task<TokenValidationParameters> 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 static DateTime? GetSafeDateTime(DateTime dateTime)
{
// Assigning DateTime.MinValue or default(DateTime) to a DateTimeOffset when in a UTC+X timezone will throw
Expand Down
37 changes: 33 additions & 4 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,20 @@ 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;

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

@brentschmaltz brentschmaltz Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevinchalet i am thinking that creating the interface ITokenValidator that has a couple of methods instead of the abstract class TokenHandler, this would make it easier for users who currently have an implementation of ISecurityTokenValidator.

Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts?

IM already has TokenHandler, SecurityTokenHandler and ISecurityTokenValidator, which makes using these abstractions already quite painful. I'm not sure adding a 4th one will help 😄

WIF only had SecurityTokenHandler and it was much clearer: unifying everything in the next Wilson major version would be more than welcome 😄

Copy link
Contributor Author

@brentschmaltz brentschmaltz Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevinchalet OK let's stick with TokenHandler then as this was meant to be the replacement for SecurityTokenHandler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevinchalet we could implement ISecurityTokenValidator on JsonWebTokenHandler, but just because we can cast it to TokenHandler doesn't mean that users will not have a runtime break as the SecurityToken will change from JwtSecurityToken to JsonWebToken.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but that would mean an instance of JsonWebTokenHandler was added to SecurityTokenValidators, which, if you keep making using the new JWT stack opt-in, requires a deliberate action from the developer. And in this case, getting a JsonWebToken instead of a JwtSecurityToken is a logical result.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping to make it opt-out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: 507d241

@Tratcher default is true, use TokenHandlers.

}

/// <summary>
Expand Down Expand Up @@ -105,6 +113,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 All @@ -126,15 +139,20 @@ public JwtBearerOptions()
public bool IncludeErrorDetails { get; set; } = true;

/// <summary>
/// Gets or sets the <see cref="MapInboundClaims"/> property on the default instance of <see cref="JwtSecurityTokenHandler"/> in SecurityTokenValidators, which is used when determining
/// whether or not to map claim types that are extracted when validating a <see cref="JwtSecurityToken"/>.
/// Gets or sets the <see cref="MapInboundClaims"/> property on the default instance of <see cref="JwtSecurityTokenHandler"/> in SecurityTokenValidators, or <see cref="JsonWebTokenHandler"/> in TokenHandlers which is used when determining
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Gets or sets the <see cref="MapInboundClaims"/> property on the default instance of <see cref="JwtSecurityTokenHandler"/> in SecurityTokenValidators, or <see cref="JsonWebTokenHandler"/> in TokenHandlers which is used when determining
/// Gets or sets the <see cref="MapInboundClaims"/> property on the default instance of <see cref="JwtSecurityTokenHandler"/> in SecurityTokenValidators, or <see cref="JsonWebTokenHandler"/> in TokenHandlers, which is used when determining

/// whether or not to map claim types that are extracted when validating a <see cref="JwtSecurityToken"/> or a <see cref="JsonWebToken"/>.
/// <para>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.</para>
/// <para>The default value is true.</para>
/// </summary>
public bool MapInboundClaims
{
get => _defaultHandler.MapInboundClaims;
set => _defaultHandler.MapInboundClaims = value;
get => _mapInboundClaims;
set
{
_mapInboundClaims = value;
_defaultHandler.MapInboundClaims = value;
_defaultTokenHandler.MapInboundClaims = value;
}
}

/// <summary>
Expand All @@ -152,4 +170,15 @@ public bool MapInboundClaims
/// Defaults to <see cref="ConfigurationManager{OpenIdConnectConfiguration}.DefaultRefreshInterval" />.
/// </value>
public TimeSpan RefreshInterval { get; set; } = ConfigurationManager<OpenIdConnectConfiguration>.DefaultRefreshInterval;

/// <summary>
/// Gets or sets whether <see cref="TokenHandlers"/> or <see cref="SecurityTokenValidators"/> will be used to validate the inbound token.
/// </summary>
/// <remarks>
/// The advantage of using TokenHandlers are:
/// <para>There is an Async model.</para>
/// <para>The default token handler is a <see cref="JsonWebTokenHandler"/> which is faster than a <see cref="JwtSecurityTokenHandler"/>.</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>
Comment on lines +181 to +184
Copy link
Member

@eerhardt eerhardt Jul 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should rethink these doc comments as they might not make full sense now that we've inverted the boolean to be UseSecurityTokenValidators.

Maybe we should add why someone would want to use SecurityTokenValidators as well.

/// </remarks>
public bool UseTokenHandlers { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public bool UseTokenHandlers { get; set; }
public bool UseTokenHandlers { get; set; } = true;

+1 for making this the default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: 507d241

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/azp run

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to have been reverted. Was that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt yes, upon reflection, this would be breaking for some users.
@Tratcher would like to see this 'true' by default, but we have time to change this decision if we choose.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brentschmaltz was the default set to true?

}
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
#nullable enable
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.JwtBearerHandler(Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions!>! 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.IdentityModel.Tokens.TokenHandler!>!
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.get -> bool
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseTokenHandlers.set -> void
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
#nullable enable
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationHandler.WsFederationHandler(Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions!>! 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.IdentityModel.Tokens.TokenHandler!>!
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.get -> bool
Microsoft.AspNetCore.Authentication.WsFederation.WsFederationOptions.UseTokenHandlers.set -> void
Loading