163

Is it possible to support multiple JWT Token issuers in ASP.NET Core 2? I want to provide an API for external service and I need to use two sources of JWT tokens - Firebase and custom JWT token issuers. In ASP.NET core I can set the JWT authentication for Bearer auth scheme, but only for one Authority:

  services
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Authority = "https://securetoken.google.com/my-firebase-project"
            options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidIssuer = "my-firebase-project"
                    ValidateAudience = true,
                    ValidAudience = "my-firebase-project"
                    ValidateLifetime = true
                };
        }

I can have multiple issuers and audiences, but I can't set several Authorities.

Anis Tissaoui
  • 834
  • 1
  • 7
  • 26
Sane
  • 2,334
  • 2
  • 17
  • 20
  • 2
    AFAIK you may add any number of properties to a JWT. So, there is nothing stopping you from recording two issuer names in a JWT. The problem comes in that your application would need to know both keys, if each issuer were using a different key to sign. – Tim Biegeleisen Apr 06 '18 at 13:58

4 Answers4

388

You can totally achieve what you want:

services
    .AddAuthentication()
    .AddJwtBearer("Firebase", options =>
    {
        options.Authority = "https://securetoken.google.com/my-firebase-project";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "my-firebase-project",
            ValidateAudience = true,
            ValidAudience = "my-firebase-project",
            ValidateLifetime = true
        };
    })
    .AddJwtBearer("Custom", options =>
    {
        // Configuration for your custom
        // JWT tokens here
    });

services
    .AddAuthorization(options =>
    {
        options.DefaultPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes("Firebase", "Custom")
            .Build();
    });

Let's go through the differences between your code and that one.

AddAuthentication has no parameter

If you set a default authentication scheme, then on every single request the authentication middleware will try to run the authentication handler associated with the default authentication scheme. Since we now have two possible authentication schemes, there's no point in running one of them.

Use another overload of AddJwtBearer

Every single AddXXX method to add an authentication has several overloads:

  • One where the default authentication scheme associated with the authentication method is used, as you can see here for cookies authentication
  • One where you pass, in addition to the configuration of the options, the name of the authentication scheme, as on this overload

Now, because you use the same authentication method twice but authentication schemes must be unique, you need to use the second overload.

Update the default policy

Since the requests won't be authenticated automatically anymore, putting [Authorize] attributes on some actions will result in the requests being rejected and an HTTP 401 will be issued.

Since that's not what we want because we want to give the authentication handlers a chance to authenticate the request, we change the default policy of the authorization system by indicating both the Firebase and Custom authentication schemes should be tried to authenticate the request.

That doesn't prevent you from being more restrictive on some actions; the [Authorize] attribute has an AuthenticationSchemes property that allows you to override which authentication schemes are valid.

If you have more complex scenarios, you can make use of policy-based authorization. I find the official documentation is great.

Let's imagine some actions are only available to JWT tokens issued by Firebase and must have a claim with a specific value; you could do it this way:

// Authentication code omitted for brevity

services
    .AddAuthorization(options =>
    {
        options.DefaultPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes("Firebase", "Custom")
            .Build();

        options.AddPolicy("FirebaseAdministrators", new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes("Firebase")
            .RequireClaim("role", "admin")
            .Build());
    });

You could then use [Authorize(Policy = "FirebaseAdministrators")] on some actions.

A final point to note: If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed. Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.

UPDATE - .net core 6

for newer versions of .net core, you need to specify a default authorisation, so .AddAuthentication() wont work.

an example

// Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Audience = "https://localhost:5000/";
            options.Authority = "https://localhost:5000/identity/";
        })
        .AddJwtBearer("AzureAD", options =>
        {
            options.Audience = "https://localhost:5000/";
            options.Authority = "https://login.microsoftonline.com/eb971100-7f436/";
        });

// Authorization
builder.Services.AddAuthorization(options =>
{
    var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
        JwtBearerDefaults.AuthenticationScheme,
        "AzureAD");
    defaultAuthorizationPolicyBuilder =
        defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
    options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});

see https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-6.0#use-multiple-authentication-schemes for more

dgamma3
  • 2,333
  • 4
  • 26
  • 47
Mickaël Derriey
  • 12,796
  • 1
  • 53
  • 57
  • 12
    Does this require that the header value be changed from firebase or custom solution? ie. instead of `Authorization : Bearer ` that the header be `Authorization : Firebase ` for example? When tried this solution I got the error: "No authentication handler is registered for the scheme 'Bearer'." – Rush Frisby Dec 14 '18 at 20:38
  • 7
    No, the headers don't need to change. The error message suggests that you're referring to a non-existent authentication scheme (Bearer). In our examples, the two registered schemes are Firebase and Custom, which are the first arguments of the `.AddJwtBearer` method calls. – Mickaël Derriey Dec 15 '18 at 06:40
  • 1
    It was because someone had put [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] on the controller methods. This works after I changed it to just be [Authorize] – Rush Frisby Dec 16 '18 at 04:08
  • 10
    Hi. Was looking for just this solution. Unfortunately I am getting a "No authenticationScheme was specified, and there was no DefaultChallengeScheme found" exception. options.DefaultPolicy is set ok. Any ideas? – terjetyl Mar 05 '19 at 09:25
  • 32
    This was a supremely helpful answer, and put together a lot of what I've seen in pieces all over the place. – Aron Aug 02 '19 at 18:11
  • I too found this answer SUPER USEFUL. If I could vote for most supreme answer I've seen in a while, it would be this one. I have it working but have to clean up the code. I had a few extra bits that might be useful for other readers so will leave it as an additional answer. In ASP.Net Core 3.1 you get a few nasty grams that clutter up the log but after reading them, you can see they make sense and are nothing to really worry about. https://stackoverflow.com/questions/58856735/idx10501-signature-validation-failed-unable-to-match-keys has the nasty-grams I'm seeing. – No Refunds No Returns Feb 12 '20 at 02:32
  • 1
    I get the following error in core 3.1 "No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found". If i understand correctly you dont want to specify a default schema. How to solve this – Pieter Feb 18 '20 at 10:28
  • 1
    @Pieter my best guess is that either the `[Authorize]` attribute or the authorisation policy doesn't explicitly specify the authentication schemes to use for the authentication. – Mickaël Derriey Feb 18 '20 at 22:17
  • In core 3.1 .AddAuthentication() need to have an default scheme. auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; And btw, the order in de AddAuthorization does matter if i am correct – Pieter Feb 20 '20 at 15:40
  • 1
    @Pieter that's not correct, default schemes are not required, as pointed out by my answer. Your schemes also indicate that your setup is in no way related to the question. Please ask a new question if needed. – Mickaël Derriey Feb 21 '20 at 19:42
  • The authorization policy dictates which authentication schemes are being "tried". Taking the last code sample above, an empty `[Authorize]` attribute would use the default policy, hence would try both schemes, whereas `[Authorize(Policy = "FirebaseAdministrators")]` would only try the Firebase scheme. No policy at the action, controller or global level means the endpoint is accessible to anonymous requests, so no scheme would be tried. – Mickaël Derriey Apr 25 '20 at 05:16
  • In core 3.1, the DefaultPolicy is only used when each endpoint type in your Startup UseEndpoints sets RequireAuthorization(). (e.g. `.UseEndpoints(rb => rb.MapControllers().RequireAuthorization())`) – TylerOhlsen Apr 27 '20 at 16:54
  • 4
    @TylerOhlsen that's not correct; while it will be used in the case you describe, it's not the only one. It will also be used if you don't specify an authorization requirement at the endpoint level, but decorate MVC controllers and/or actions with an empty `[Authorize]` attribute. – Mickaël Derriey Apr 27 '20 at 22:11
  • https://gist.github.com/mac2000/ff95fc54bdc684646bb1de24ef07b333 just in case anyone will need some self contained example, approach is working also there is almost same in docs - https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-3.1#use-multiple-authentication-schemes – mac May 18 '20 at 11:27
  • The experience I am getting is both scheme configurations are being checked and any one failing brings the whole request down. The first one matches the supplied token and I can see OnTokenValidated is executed. Then for some dumb reason it proceeds to validate again using the second scheme which of course fails and a 401 is created. No idea what scenario this is intended to support. Who issues token that support multiple schemes??! – Sam Jul 07 '20 at 03:14
  • @Sam I created a brand new projet following the concepts of my response and I can't reproduce what you're experiencing. Yes, both schemes are checked, which is expeted, but because one of them succeeds, the overall authorization is considered successful. See https://gist.github.com/mderriey/994d72a81f9f4324b32e68fec0705a2e for the simplest example I could come up with. – Mickaël Derriey Jul 10 '20 at 10:53
  • @MickaëlDerriey We are doing something similar to the above. It seems to work for authentication, but we end up with a scenario where the ClaimsPrincipal is either incorrectly set, or the claims and identities on the ClaimsPrincipal aren't set. Any ideas why this would be? – Jano du Toit Oct 27 '20 at 11:49
  • this answer definitely put me in the correct direction but I couldn't get it to work then I followed this article. setting default worked https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-3.1#use-multiple-authentication-schemes – Kumar Garapati Nov 03 '20 at 20:38
  • FYI, you may need a custom middleware to populate context.User, see https://github.com/aspnet/Security/issues/1708#issuecomment-376567491 – Pedro Costa Mar 20 '21 at 16:20
  • How can you tell which scheme created the [HttpContext.User Property](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user?view=aspnetcore-5.0#Microsoft_AspNetCore_Http_HttpContext_User)? – Jossean Yamil Sep 08 '21 at 21:09
  • 1
    @JosseanYamil If both schemes run on the same request, it's possible that HttpContext.User contains 2 identities, one for each scheme. You can set options.TokenValidationParameters.AuthenticationType which will flow to ClaimsIdentity.AuthenticationType, which then allows you to know which identity comes from which scheme. – Mickaël Derriey Sep 09 '21 at 01:29
  • Understood, I see now the AuthenticationType field. Thanks for all the assistance, you have no idea the amount of help you have provided me @MickaëlDerriey – Jossean Yamil Sep 09 '21 at 16:28
  • Thanks for the kind words, they're much appreciated ‍♂️ – Mickaël Derriey Sep 09 '21 at 17:16
  • Tip: Using two schemas, we can decide which one should be applied on controllers using `[Authorize(AuthenticationSchemes = "Firebase")]` – Jacek Labuda Nov 21 '21 at 14:17
  • Any hint on how to get the "Authenticated Schema" in a custom AuthorizationHandler ? – Dekim Mar 10 '22 at 01:51
  • @Dekim I think there's no way to access it by default. I'd also suggest there might be a design problem if you need it. In my opinion, a requirement should be handled the same whether it's for the authN scheme A or B. If you do really need it, a potential solution would be to pass it through the authZ requirement, then you could access it in the authZ handler. – Mickaël Derriey Mar 10 '22 at 05:18
  • Somehow this magically made both authentication types (one with `AddJwtBearer` and one via `AddMicrosoftIdentityWebApi`) work when both use Bearer as authentication header. Not sure if this is perfect, but it's a start after a half day of debugging, thanks! – CularBytes Aug 19 '22 at 16:01
  • @MickaëlDerriey One of the best answer I've ever seen on SO :+1: – Christophe Blin Dec 08 '22 at 09:38
  • What if each of the authentication schemes uses different scope? The scope are set in the authorization policies (`RequireScope`), and for that reason I think I need two policies. So I have tried without default policy, without default scope, without **both**, and with both default policy **and** scope. Testing this, I run two clients locally with the different scopes each. – RK_97 Aug 01 '23 at 17:52
  • @RK_97 I see two options. One is to detect which provider the user was authenticated from, and require the appropriate scope. Another is to transform the claims/scopes on the way in to a unified set of claims the application code understands. I haven't tested this code, so please don't treat it as working, or production-quality: https://gist.github.com/mderriey/4be63dc62e3cb194141961c4b5b74f1e – Mickaël Derriey Aug 02 '23 at 14:59
  • @Dekim It's been a long time, I hope you've found a solution. But I realised my answer to you in March 22 was wrong, you can access the authentication provider name from a requirement. For this you need to set the `options.TokenValidationOptions.AuthenticationType` to a specific value, which you then will find in `ClaimsIdentity.AuthenticationType`. See the gist I linked just above this reply. – Mickaël Derriey Aug 02 '23 at 15:01
12

This is an extension of Mickaël Derriey's answer.

Our app has a custom authorization requirement that we resolve from an internal source. We were using Auth0 but are switching to Microsoft Account authentication using OpenID. Here is the slightly edited code from our ASP.Net Core 2.1 Startup. For future readers, this works as of this writing for the versions specified. The caller uses the id_token from OpenID on incoming requests passed as a Bearer token. Hope it helps someone else trying to do an identity authority conversion as much as this question and answer helped me.

const string Auth0 = nameof(Auth0);
const string MsaOpenId = nameof(MsaOpenId);

string domain = "https://myAuth0App.auth0.com/";
services.AddAuthentication()
        .AddJwtBearer(Auth0, options =>
            {
                options.Authority = domain;
                options.Audience = "https://myAuth0Audience.com";
            })
        .AddJwtBearer(MsaOpenId, options =>
            {
                options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
                {
                    ValidateAudience = true,
                    ValidAudience = "00000000-0000-0000-0000-000000000000",

                    ValidateIssuer = true,
                    ValidIssuer = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0",

                    ValidateIssuerSigningKey = true,
                    RequireExpirationTime = true,
                    ValidateLifetime = true,
                    RequireSignedTokens = true,
                    ClockSkew = TimeSpan.FromMinutes(10),
                };
                options.MetadataAddress = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0/.well-known/openid-configuration";
            }
        );

services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes( Auth0, MsaOpenId )
        .Build();

    var approvedPolicyBuilder =  new AuthorizationPolicyBuilder()
           .RequireAuthenticatedUser()
           .AddAuthenticationSchemes(Auth0, MsaOpenId)
           ;

    approvedPolicyBuilder.Requirements.Add(new HasApprovedRequirement(domain));

    options.AddPolicy("approved", approvedPolicyBuilder.Build());
});
No Refunds No Returns
  • 8,092
  • 4
  • 32
  • 43
3

The solution to your question, is available in following blog post https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme

Basically the solutions exists of overriding the regular JWTBearer handler with you own generic handler that can check through the JWTBearerConfig if the issuer in the cfg is the same to the isseur in your token.

The blog post suggests to use seperate handlers for each scheme, that doesn't seem to be needed, a generic class JWTAuthenticationHandler that overrides the HandleAuthenticateAsync method seems to suffice!

Code wise you could implement your startup like this:

 //Using multiple schemes can cause issues when validating the issuesSigningKey therefore we need to implement seperate handlers for each scheme! => cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
        services.AddAuthentication()
        //Set the authenticationScheme by using the identityServer helper methods (we are using a Bearer token)
        .AddScheme<JwtBearerOptions, JWTAuthenticationHandler>(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
        {
            //TO DO Get the origin url's from configuration file, instead of setting all url's here 
            options.Authority = _identityServerSettings.Authority;
            options.Audience = _identityServerSettings.Audience;

            options.Events = new JwtBearerEvents
            {
                OnChallenge = context =>
                {
                    return Task.CompletedTask;
                },
                //When using multiple JwtBearer schemes we can run into "OnAuthenticationFailed" for instance when logging in via IdentityServer the AuthenticationHandler will still check in these events, this can be ignored...
                //Cfr => https://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication
                //If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed.Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
                //We managed to fix this issue by adding seperate AuthenticationHandlers for each type of bearer token... cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
                OnAuthenticationFailed = context =>
                {
                    return Task.CompletedTask;
                },
                OnMessageReceived = context =>
                {
                    return Task.CompletedTask;
                },
                OnForbidden = context =>
                {
                    return Task.CompletedTask;
                },
                OnTokenValidated = context =>
                {
                    return Task.CompletedTask;
                }

            };
        })
        //Set the authentication scheme for the AzureAd integration (we are using a bearer token)
        .AddScheme<JwtBearerOptions, JWTAuthenticationHandler>("AzureAD", "AzureAD", options =>
         {
            options.Audience = _azureAdSettings.Audience;   //ClientId 
            options.Authority = _azureAdSettings.Authority; //"https://login.microsoftonline.com/{tenantId}/v2.0/"

            options.TokenValidationParameters = new TokenValidationParameters
             {
                //Set built in claimTypes => Role
                RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
             };

             options.Events = new JwtBearerEvents
             {
                 OnChallenge = context =>
                 {
                     return Task.CompletedTask;
                 },
                 //When using multiple JwtBearer schemes we can run into "OnAuthenticationFailed" for instance when logging in via IdentityServer the AuthenticationHandler will still check in these events, this can be ignored...
                 //Cfr => https://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication
                 //A final point to note: If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed.Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
                 //We managed to fix this issue by adding seperate AuthenticationHandlers for each type of bearer token... cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
                 OnAuthenticationFailed = context =>
                 {
                     return Task.CompletedTask;
                 },
                 OnMessageReceived = context =>
                 {
                     return Task.CompletedTask;
                 },
                 OnForbidden = context =>
                 {
                     return Task.CompletedTask;
                 },
                 OnTokenValidated = context =>
                 {
                     return Task.CompletedTask;
                 }
                 
             };
         });
    }

The JWTAuthenticationHandlerClass can look like this

  using Microsoft.AspNetCore.Authentication;
  using Microsoft.AspNetCore.Authentication.JwtBearer;
  using Microsoft.Extensions.Logging;
  using Microsoft.Extensions.Options;
  using System;
  using System.IdentityModel.Tokens.Jwt;
  using System.Text.Encodings.Web;
  using System.Threading.Tasks;

  namespace WebAPI.Auth
  {
    public class JWTAuthenticationHandler: JwtBearerHandler
    {
    public JWTAuthenticationHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    { }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        //Fetch OIDC configuration for the IDP we are handling
        var authorityConfig = await this.Options.ConfigurationManager.GetConfigurationAsync(this.Context.RequestAborted);
        //Determine the issuer from the configuration
        var authorityIssuer = authorityConfig.Issuer;

        var jwtToken = this.ReadTokenFromHeader();
        var jwtHandler = new JwtSecurityTokenHandler();

        //Check if we can read the token as a valid JWT, if not let the JwtBearerHandler do it's thing...
        if (jwtHandler.CanReadToken(jwtToken))
        {
            //Read the token and determine if the issuer in config is the same as the one in the token, if this is true we know we want to let the JwtBearerHandler continue, if not we skip and return noResult
            //This way the next IDP configuration will pass here until we find a matching issuer and then we know that is the IDP we are dealing with
            var token = jwtHandler.ReadJwtToken(jwtToken);
            if (string.Equals(token.Issuer, authorityIssuer, StringComparison.OrdinalIgnoreCase))
            {
                return await base.HandleAuthenticateAsync();
            }
            else
            {
                // return NoResult since the issuer in cfg did not match the one in the token, so no need to proceed to tokenValidation
                this.Logger.LogDebug($"Skipping jwt token validation because token issuer was {token.Issuer} but the authority issuer is: {authorityIssuer}");
                return AuthenticateResult.NoResult();
            }
        }

        return await base.HandleAuthenticateAsync();
    }

    //Fetch the bearer token from the authorization header on the request!
    private string ReadTokenFromHeader()
    {
        string token = null;

        string authorization = Request.Headers["Authorization"];

        //If we don't find the authorization header return null
        if (string.IsNullOrEmpty(authorization))
        {
            return null;
        }

        //get the token from the auth header
        if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
        {
            token = authorization.Substring("Bearer ".Length).Trim();
        }

        return token;
    }
}

}

Pieter
  • 111
  • 12
1

One thing that was missing in Mickael's answer is that scheme needs to be specified in Authorize attribute (If you want to use authorization)

[Authorize(AuthenticationSchemes = "Firebase,Custom", Policy ="FirebaseAdministrators")]

Without AuthenticationSchemes provided, and AddAuthentication() has no parameter, NetCore fails to Authenticate and Request.HttpContext.User.Identity.IsAuthenticated is set to false

  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 14 '22 at 01:28