Use Multiple Jwt Bearer Authentication

Use multiple JWT Bearer Authentication

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.

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.

Multiple JWT authorization token in Web API

This pattern can be good from the viewpoints of both good security and simple code standard if it works like this:

  • Confidential access tokens in an unreadable format are issued to web and mobile clients so that no sensitive data is revealed to them

  • APIs receive JWTs containing claims and scopes then use them for authorization, including all user context needed

  • When the client calls the API, messages are routed via the gateway, which swaps confidential tokens for JWTs

INDUSTRY STANDARDS

At my company we call this The Phantom Token Pattern. It is worth seeing if you can steer things in this direction - eg forward my answer to an architect if you work with one.

Your API should not have to work with 2 types of token or non standard header values, since this will mean security libraries fail to work.

CUSTOM AUTHENTICATOR

As a last resort you can customize .Net and retrieve the token differently. Some sample code of mine shows how to customize the .Net stack, but only for learning purposes. It is best to keep your security code simple and standard.

Multiple JWT bearers for authorization and authentication

You can only have use IdentityServerAuthentication and after a successful authentication add the claims from Application User to Current User. You can do this by using OnTokenValidated

services.AddAuthentication()
.AddIdentityServerAuthentication(DEFAULT_AUTH_SCHEME, options =>
{
options.Authority = Configuration["IdentityServer:Url"];
options.RequireHttpsMetadata = false;
options.ApiName = Configuration["IdentityServer:ApiName"];
options.SupportedTokens = SupportedTokens.Both;
options.SaveToken = false;
options.EnableCaching = false;
options.CacheDuration = TimeSpan.FromMinutes(10);
options.JwtBearerEvents.OnTokenValidated = async context =>
{
// get subject from authenticated principal
var subject = context.Principal.FindFirst("sub");

// get claims from your database for the subject
var claims = new List<Claim> { new Claim("someclaim", "aaaa") };

// change the principal
context.Principal = new System.Security.Claims.ClaimsPrincipal(claims);
};
});

How add two different tokens in ASP.NET Core Web API

You can set multiple JWT Bearer Authentication with different schema name :

services.AddAuthentication()
.AddJwtBearer("ADFS",options =>
{
options.Audience = Configuration["Adfs:Audience"];
options.Authority = Configuration["Adfs:Issuer"];
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
})
.AddJwtBearer("AAD", options =>
{
//AAD jwt validation configuration
});

If you want to make your controller/action to accept two jwt tokens , tokens from AAD or ADFS are ok to access your controller/action , you can make a policy to let both the AAD and ADFS authentication schemes tried to authenticate the request :

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

In addition , if you want to know which schema the token is from , you can check the particular claim in user's identity , or directly add authentication schema value to user claims in events :

options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents
{
OnTokenValidated = (context) =>
{
var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
//add your custom claims here
claimsIdentity.AddClaim(new Claim("schema", "AAD"));

return Task.FromResult(0);
}
};

And get in action after authentication :

var result = User.Claims.Where(c=>c.Type=="schema").FirstOrDefault().Value;


Related Topics



Leave a reply



Submit