Multiple JWT authorities/issuers in Asp.Net Core

I figured out how to do it:

  1. Create an authentication builder with services.AddAuthentication(). You can set the default scheme (to "Bearer") if you want but it's not necessary.

  2. Add as many different JWT Bearer configurations as you want with authenticationBuilder.AddJwtBearer(), each with its own key (e.g "Auth0", "IS4", ...). I used a loop over an array in appsettings.json

  3. Create a policy scheme with authenticationBuilder.AddPolicyScheme and give it the scheme name "Bearer" (use JwtBearerDefaults.AuthenticationScheme to avoid having magic strings in your code) and set options.ForwardDefaultSelector in the callback to a function which returns one of the other scheme names ("Auth0", "IS4" or whatever you put) depending on some criterion. In my case it just looks for the scheme name in JWT issuer (if issuer contains "auth0" then the Auth0 scheme is used).

Code:

public static void AddMultiSchemeJwtBearerAuthentication(
    this IServiceCollection services,
    IConfiguration configuration
)
{
    // Create JWT Bearer schemes.
    var schemes = configuration
        .GetSection("Jwt")
        .GetChildren()
        .Select(s => s.Key)
        .ToList()
    ;
    var authenticationBuilder = services.AddAuthentication();
    foreach (var scheme in schemes)
    {
        authenticationBuilder.AddJwtBearer(scheme, options =>
        {
            options.Audience  = configuration[$"Jwt:{scheme}:Audience"];
            options.Authority = configuration[$"Jwt:{scheme}:Authority"];
        });
    }

    // Add scheme selector.
    authenticationBuilder.AddPolicyScheme(
        JwtBearerDefaults.AuthenticationScheme,
        "Selector",
        options =>
        {
            options.ForwardDefaultSelector = context =>
            {
                // Find the first authentication header with a JWT Bearer token whose issuer
                // contains one of the scheme names and return the found scheme name.
                var authHeaderNames = new[] {
                    HeaderNames.Authorization,
                    HeaderNames.WWWAuthenticate
                };
                StringValues headers;
                foreach (var headerName in authHeaderNames)
                {
                    if (context.Request.Headers.TryGetValue(headerName, out headers) && !StringValues.IsNullOrEmpty(headers))
                    {
                        break;
                    }
                }

                if (StringValues.IsNullOrEmpty(headers))
                {
                    // Handle error. You can set context.Response.StatusCode and write a
                    // response body. Returning null invokes default scheme which will raise
                    // an exception; not sure how to fix this so the request is rejected.
                    return null;
                }

                foreach (var header in headers)
                {
                    var encodedToken = header.Substring(JwtBearerDefaults.AuthenticationScheme.Length + 1);
                    var jwtHandler = new JwtSecurityTokenHandler();
                    var decodedToken = jwtHandler.ReadJwtToken(encodedToken);
                    var issuer = decodedToken?.Issuer?.ToLower();
                    foreach (var scheme in schemes)
                    {
                        if (issuer?.Contains(scheme.ToLower()) == true)
                        {
                            // Found the scheme.
                            return scheme;
                        }
                    }
                }
                // Handle error.
                return null;
            };
        }
    );
}

Nothing special is needed to get Ocelot to support this, just use "Bearer" as the authentication provider key and the scheme selector policy will be automatically invoked.


A working solution for .net 5.

  • This will work for multiple JWT bearer token issuers

  • Default schema will do the routing to the corresponding schema

    // Get list of domains and audience from the config
         var authorities = Configuration["Auth:Domain"].Split(',').Distinct().ToList();
         var audience = Configuration["Auth:Audience"];
         // Add default empty schema schema selection policy
         var authenticationBuilder = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(
             options =>
             {
                 // forward to corresponding schema based on token's issuer 
                 // this will read the token and check the token issues , if the token issuer is registered in config then redirect to that schema
                 options.ForwardDefaultSelector = context =>
                 {
                     string authorization = context.Request.Headers[HeaderNames.Authorization];
    
                     if (!string.IsNullOrEmpty(authorization))
                     {
                         if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
                         {
                             var token = authorization.Substring("Bearer ".Length).Trim();
    
                             var jwtHandler = new JwtSecurityTokenHandler();
                             if (jwtHandler.CanReadToken(token))
                             {
                                 var jwtToken = jwtHandler.ReadJwtToken(token);
                                 if (authorities.Contains(jwtToken.Issuer))
                                     return jwtToken.Issuer;
                             }
                         }
                     }
                     return null;
                 };
             });
    
         // Register all configured schemas 
         foreach (var auth in authorities)
         {
             authenticationBuilder.AddJwtBearer(auth, options =>
             {
    
                 options.SaveToken = true;
                 options.Audience = audience;
                 options.Authority = auth;
                 options.TokenValidationParameters = new TokenValidationParameters
                 {
                     NameClaimType = "sub",
                     ValidateIssuer = true,
                     ValidateAudience = true,
                     ValidateLifetime = true,
                     RequireSignedTokens = true,
                     ValidateIssuerSigningKey = true
                 };
             });
    
         }
    

This is working example:

public void ConfigureServices(IServiceCollection services)
{
   services.AddAuthentication(options => 
   {
       options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
   })
    //set default authentication 
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        //set the next authentication configuration to be used
        options.ForwardDefaultSelector = ctx => "idp4";

        //...rest of the options goes here
        };
    })
    .AddJwtBearer("idp4", options => 
     {
        //set the next authentication configuration to be used
        options.ForwardDefaultSelector = ctx => "okta";
        //options goes here
     })
    .AddJwtBearer("okta", options => 
     {
        //options goes here
     });