Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple JWT authorities/issuers in Asp.Net Core

I'm trying to get JWT bearer authentication in an ASP.Net API gateway using Ocelot to work with multiple authorities/issuers. One issuer is Auth0 and the other is an in-house authentication server based on IdentityServer4; we are trying to migrate away from Auth0 but have external clients that still depend on it, so we would like to support both until everything is fully tested for them to switch.

According to this MSDN blog post, it should be possible to use multiple authorities by setting TokenValidationParameters.ValidIssuers instead of JwtBearerOptions.Authority. However, I have tested this with and without Ocelot and no authentication occurs if the Authority is not set to the authority who issued the token, no matter the contents of TokenValidationParameters.ValidIssuers.

Does anybody know how to get this working? This is how I'm setting up authentication. It works only if the commented line is uncommented (and only for tokens issued by that single authority). I'm expecting Ocelot or ASP.Net Core to get the key from the issuing server; both provide JWKs by .well-known/openid-configuration which works with the ASP.Net Core middleware.

public static void AddJwtBearerAuthentication(this IServiceCollection services, IConfiguration configuration)
{
    services
        .AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
        {
            //options.Authority = configuration["Jwt:Authority"];
            options.Audience  = configuration["Jwt:Audience"];
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer           = true,
                ValidateIssuerSigningKey = true,
                ValidateAudience         = true,
                ValidAudience            = configuration["Jwt:Audience"],
                ValidIssuers             = configuration
                    .GetSection("Jwt:Authorities")
                    .AsEnumerable()
                    .Select(kv => kv.Value)
                    .Where(s => !string.IsNullOrEmpty(s))
                    .ToArray()
            };
        });
}

The output of Ocelot when a client who has the wrong issuer (or when we're using TokenValidationParameters.ValidIssuer/ValidIssuers) connects is:

[16:35:37 WRN] requestId: _____, previousRequestId: no previous request id, message: Error Code: UnauthenticatedError Message: Request for authenticated route _____ by  was unauthenticated errors found in ResponderMiddleware. Setting error response for request path:_____, request method: POST

This is a client_credentials authentication hence the lack of a username after "by". As you can see Ocelot doesn't say what the exact problem is. The ASP.Net Core JWT bearer middleware (without Ocelot) just says the signature is invalid. I suspect it either isn't looking at the TokenValidationParameters, or I've misunderstood the purpose of them.

like image 276
Chris Swinchatt Avatar asked Oct 23 '18 15:10

Chris Swinchatt


2 Answers

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.

like image 55
Chris Swinchatt Avatar answered Sep 26 '22 08:09

Chris Swinchatt


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
     });
like image 45
Roi Shabtai Avatar answered Sep 23 '22 08:09

Roi Shabtai