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, WsFed, and OIDC handlers to use identity model 7 #49541

Merged
merged 21 commits into from
Jul 21, 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
4 changes: 2 additions & 2 deletions eng/Version.Details.xml
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,9 @@
<Uri>https://github.com/dotnet/runtime</Uri>
<Sha>40480e8e82b734a54c210f656361ff073353ffbe</Sha>
</Dependency>
<Dependency Name="Microsoft.SourceBuild.Intermediate.source-build-externals" Version="8.0.0-alpha.1.23362.1">
<Dependency Name="Microsoft.SourceBuild.Intermediate.source-build-externals" Version="8.0.0-alpha.1.23368.1">
<Uri>https://github.com/dotnet/source-build-externals</Uri>
<Sha>76026f9224bd83ede7b2f494912694a30169c233</Sha>
<Sha>844e2cd86e7525d7eb32358e63a0c554187eb26b</Sha>
<SourceBuild RepoName="source-build-externals" ManagedOnly="true" />
</Dependency>
<Dependency Name="Microsoft.SourceBuild.Intermediate.symreader" Version="2.0.0-beta-23228-03">
Expand Down
11 changes: 6 additions & 5 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>7.0.0-preview</IdentityModelVersion>
<!--
When StabilizePackageVersion is set to 'true', this branch will produce stable outputs for 'Shipping' packages
-->
Expand Down Expand Up @@ -162,7 +163,7 @@
<!-- Packages from dotnet/source-link -->
<MicrosoftSourceLinkGitHubVersion>8.0.0-beta.23361.2</MicrosoftSourceLinkGitHubVersion>
<!-- Packages from dotnet/source-build-externals -->
<MicrosoftSourceBuildIntermediatesourcebuildexternalsVersion>8.0.0-alpha.1.23362.1</MicrosoftSourceBuildIntermediatesourcebuildexternalsVersion>
<MicrosoftSourceBuildIntermediatesourcebuildexternalsVersion>8.0.0-alpha.1.23368.1</MicrosoftSourceBuildIntermediatesourcebuildexternalsVersion>
<!-- Packages from dotnet/source-build-reference-packages -->
<MicrosoftSourceBuildIntermediatesourcebuildreferencepackagesVersion>8.0.0-alpha.1.23362.3</MicrosoftSourceBuildIntermediatesourcebuildreferencepackagesVersion>
<!-- Packages from dotnet/symreader -->
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.");
}
162 changes: 107 additions & 55 deletions src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -20,8 +19,6 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer;
/// </summary>
public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions>
{
private OpenIdConnectConfiguration? _configuration;

/// <summary>
/// Initializes a new instance of <see cref="JwtBearerHandler"/>.
/// </summary>
Expand Down Expand Up @@ -96,79 +93,88 @@ 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.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<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(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<Exception>(1);
validationFailures ??= new List<Exception>(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)
Expand All @@ -187,6 +193,11 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
return AuthenticateResult.Fail(authenticationFailedContext.Exception);
}

if (!Options.UseSecurityTokenValidators)
{
return AuthenticateResults.TokenHandlerUnableToValidate;
}

return AuthenticateResults.ValidatorNotFound;
}
catch (Exception ex)
Expand All @@ -208,6 +219,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.
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
Expand Down
42 changes: 38 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,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;

/// <summary>
/// Initializes a new instance of <see cref="JwtBearerOptions"/>.
/// </summary>
public JwtBearerOptions()
{
#pragma warning disable CS0618 // Type or member is obsolete
SecurityTokenValidators = new List<ISecurityTokenValidator> { _defaultHandler };
#pragma warning restore CS0618 // Type or member is obsolete
TokenHandlers = new List<TokenHandler> { _defaultTokenHandler };
}

/// <summary>
Expand Down Expand Up @@ -103,8 +113,14 @@ public JwtBearerOptions()
/// <summary>
/// Gets the ordered list of <see cref="ISecurityTokenValidator"/> used to validate access tokens.
/// </summary>
[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<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 +142,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
/// 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 +173,17 @@ 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 advantages 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>
/// SecurityTokenValidators can be used when <see cref="TokenValidatedContext.SecurityToken"/> needs a <see cref="JwtSecurityToken"/>.
/// When using TokenHandlers, <see cref="TokenValidatedContext.SecurityToken"/> will be a <see cref="JsonWebToken"/>.
/// </remarks>
public bool UseSecurityTokenValidators { get; set; }
}
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.UseSecurityTokenValidators.get -> bool
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.UseSecurityTokenValidators.set -> void
Loading