dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.6k stars 10.06k forks source link

Move aspnetcore to leverage JsonWebToken and JsonWebTokenHandler #49469

Closed jennyf19 closed 1 year ago

jennyf19 commented 1 year ago

Background and Motivation

Improvements in JsonWebToken and JsonWebTokenHandler have been made in Microsoft.IdentityModel, which include a 30% performance improvement over JwtSecurityToken which is currently used in ASP.NET today. In later versions of Microsoft.IdentityModel 7.x (before .NET8 RC1), we will enable AOT support by having fully trimmable assemblies in Microsoft.IdentityModel and remove the dependency of Newtonsoft, enabling a smaller dll for AOT.

Microsoft.IdentityModel offers two generations of JSON web token (JWT) handling, which are in two assemblies:

Microsoft.IdentityModel also has two abstractions for Token Handlers:

The following assemblies have a dependency on JwtSecurityToken, or JwtSecurityTokenHandler:

Proposed API

We introduce a new boolean, UseTokenHandlers, in the JwtBearer and WsFederation options to enable developers to decide whether the access token validation will be done with the new TokenHandlers (more performant, resilient and async) or with the legacy SecurityTokenValidators. We also expose the list of TokenHandlers used to validate the token. Developers can decide to add their own TokenHandlers (token type) for each protocol (JwtBearer/WsFed). By default JwtBearerOptions.TokenHandlers contains an instance of the JsonWebTokenHandler and WsFederationOptions.TokenHandlers contains handlers for SAML1, SAML2, and JsonWebTokenHandler.

PR for reference

Additions in JwtBearer:

namespace Microsoft.AspNetCore.Authentication.JwtBearer;

public class JwtBearerOptions: AuthenticationSchemeOptions
{
+   public IList<TokenHandler> TokenHandlers { get; }
+   public bool UseTokenHandlers { get; set; } = true;
}

Additions in WsFederation:

namespace Microsoft.AspNetCore.Authentication.WsFederation;

public class WsFederationOptions: RemoteAuthenticationOptions
{
+   public IList<TokenHandler> TokenHandlers { get; }
+   public bool UseTokenHandlers { get; set; } = true;
}

Additions in OpenIdConnect: We introduce a new boolean, UseTokenHandler, in the OpenIdConnect options to enable developers to decide whether the ID token validation will be done with the new TokenHandler (more performant, resilient and async) or with the legacy SecurityTokenValidator.

PR for reference

namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;

public class OpenIdConnectOptions: RemoteAuthenticationOptions
{
+   public TokenHandler TokenHandler { get; }
+   public bool UseTokenHandler { get; set; } = true;
+   public bool MapInboundClaimsTokenHandler {get; set;}
}

Usage Examples

By default, ASP.NET Core in .NET 8 uses the new TokenHandler. If a developer wants to use the legacy validators, they can set UseTokenHandlers = false.

services.Configure<JwtBearerOptions>(JwtBearerDefault.AuthenticationScheme, options => { options.UseTokenHandlers = false; });

If a developer wants to have their own TokenHandler, they can add it to the list of TokenHandlers:

services.Configure<JwtBearerOptions>(JwtBearerDefault.AuthenticationScheme, options => { options.TokenHandlers.Add( new MyTokenHandler()); });

Alternative Designs

Alternative designs were discussed with @Tratcher, @eerhardt, @halter73 . We went with Option A_1, but the alternatives discussed were:

Option A_1: Have the same assemblies as today with a dual dependency on System.IdentityModel.Tokens.Jwt and Microsoft.IdentityModel.Tokens.JsonWebToken, and offer both interfaces (Jwt for compatibility, whereas the processing is done with JsonWebToken). In practice, note that the Jwt Wilson assembly already depends on the JsonWebToken Wilson assembly, so there would not be any additional dependencies than today. Additionally, to leverage the new generation of Wilson assemblies without breaking changes, both ISecurityTokenValidator and TokenHandler members would have to be in ASP.NET core’s surface area. Validation would need to be done on the options such that when using a new generation of Jwt classes, the new generation of the TokenHandlers would also need be used.

Option A_2: Duplicate the current ASP.NET Core assemblies, and have a new generation (let’s name them Microsoft.AspNetCore.Authentication.JwtBearer2. Microsoft.AspNetCore.Authentication.OpenIdConnect2, Microsoft.AspNetCore.Authentication.WsFederation2, Microsoft.AspNetCore.Authentication2 for now, until we have a better name), and have these new generation depend only on JsonWebToken, and only expose these concepts. Letting the old generation leverage only Jwt. Additionally, these new assemblies would only rely on TokenHandler and drop support for ISecurityTokenValidator In practice, we could, for this Option A2, use the same codebase, but add the files of the old projects as links in the new project, with conditional projects)

Option A_3: Breaking changes in ASP.NET to rely only on the Microsoft.IdentityModel.Tokens.JsonWebToken assembly for Jwts and only TokenHandler for token handler abstractions. In practice, these breaking changes should only affect users that leverage the extensibility features. We need to understand how large of a population this would affect.

Also gathering customer feedback in the GitHub discussion in Microsoft.IdentityModel repo and the two above mentioned PRs.

Risks

When setting UseTokenHandlers or UseTokenHandler to true, the SecurityToken passed in the context of the TokenValidated event needs to be downcast to JsonWebToken instead of JwtSecurityToken for users who were already doing this, which is not a common scenario, but for more advanced users. Mitigation for the risk is to have an implicit operator.

Initial feedback on 7.0.0-preview of Microsoft.IdentityModel from @kevinchalet: "FYI, I tested the 7.0.0-preview packages with OpenIddict and haven't seen any particular regression. Good job"

jennyf19 commented 1 year ago
  • Microsoft.IdentityModel.Tokens.Jwt is the old generation. Notable types are JwtSecurityToken and JwtSecurityTokenHandler. This is the assembly currently used by ASP.NET Core.

System.IdentityModel.Tokens.Jwt you mean?

Yes! Great catch! Have fixed, thanks for pointing that out @cakescience

halter73 commented 1 year ago

API Review Notes:

API Approved!

namespace Microsoft.AspNetCore.Authentication.JwtBearer;

public class JwtBearerOptions : AuthenticationSchemeOptions
{
+   [Obsolete("SecurityTokenValidators is no longer used by default. Use TokenHandlers instead. To continue using SecurityTokenValidators, set UseSecurityTokenValidators to true.")]
    public IList<ISecurityTokenValidator> SecurityTokenValidators { get; }

+   public IList<TokenHandler> TokenHandlers { get; }
+   public bool UseSecurityTokenValidators { get; set; }
}

namespace Microsoft.AspNetCore.Authentication.WsFederation;

public class WsFederationOptions : RemoteAuthenticationOptions
{
+   [Obsolete("SecurityTokenValidators is no longer used by default. Use TokenHandlers instead. To continue using SecurityTokenValidators, set UseSecurityTokenValidators to true.")]
    public IList<ISecurityTokenValidator> SecurityTokenValidators { get; }

+   public IList<TokenHandler> TokenHandlers { get; }
+   public bool UseSecurityTokenValidators { get; set; }
}

namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;

public class OpenIdConnectOptions : RemoteAuthenticationOptions
{
+   [Obsolete("SecurityTokenValidator is no longer used by default. Use TokenHandler instead. To continue using SecurityTokenValidators, set UseSecurityTokenValidator to true.")]
    public ISecurityTokenValidator SecurityTokenValidator { get; set; }

+   public TokenHandler TokenHandler { get; set; }
+   public bool UseSecurityTokenValidator { get; set; }
}

~This is not for ASP.NET Core, but in System.IdentityModel.Token.Jwt we'd like to see an explicit conversion.~ Edit: This doesn't work because the declared type is just SecurityToken not JwtSecurityToken.

namespace System.IdentityModel.Tokens.Jwt;

public class JwtSecurityToken : SecurityToken
{
        // Edit: This was suggested as an addition earlier. I'm now putting a '-' instead of a '+' to make it extra clear not to add it.
-      public static explicit operator JsonWebToken(JwtSecurityToken token);
}