Supporting multiple valid issuers in identity server with JWT tokens

Changes in software are inevitable. Every part - even the most permanent - of a system might change at some point. The issuers of JWT tokens in your identity server is an example of such a part.

When we look at an openid configuration one can find various information about the identity setup; including information about what is going to appear on the issued token. An excerpt of the configuration might look something like this.

{
  "issuer": "https://identity.important.stuff",
  "jwks_uri": "https://identity.important.stuff/.well-known/openid-configuration/jwks",
  "authorization_endpoint": "https://identity.important.stuff/connect/authorize",
  ...
}

Identity server will validate against this configuration, which contains a single issuer, by default. It will compare iss in the issued token against the issuer value in the openid configuration.

{
  "nbf": 1591278499,
  "exp": 1591314499,
  "iss": "https://identity.important.stuff",
  ...
}

How do we support multiple issuers?

Let's say that the new address for issuing tokens is "https://identity.newaddress.important.stuff" and we need to support the old address and the new address. In the authentication path of your startup code, you might have a snippet of code that looks like this.

services.AddAuthentication("Bearer")
    .AddIdentityServerAuthentication(options =>
    {
        options.Authority = "https://identity.important.stuff";
        options.ApiName = "someservice";
    });

options will be an action of type IdentityServerAuthenticationOptions. This is a contract for both JWT and reference tokens. The options don't have a property for valid issuers. If we only use JWT tokens in authenticating users an alternative is to use the AddJwtBearer extension instead of IdentityServerAuthenticationOptions. These options expose TokenValidationParameters, which allows multiple valid issuers.

services.AddAuthentication("Bearer")
    .AddJwtBearer(options =>
    {
        options.Authority = "https://identity.important.stuff";
        options.Audience = "someservice";
        options.TokenValidationParameters.ValidIssuers = new []{"https://identity.important.stuff", "https://identity.newaddress.important.stuff"};
    })

Problem solved, right?

Not exactly. When we try to authenticate against the API now, we end up getting 403 (forbidden) status code on tokens containing custom claims. Taking a deep dive into the setup provided by the AddIdentityServerAuthentication extension method, we can see that some default configuration is missing from the AddJwtBearer extension method. To make roles work, we need to set the role claim type to "role". Also, there's some functionality to not map the inbound claims. This is what's happening according to the docs.

Gets or sets the MapInboundClaims property which is used when determining whether or not to map claim types that are extracted when validating a JwtSecurityToken.
If this is set to true, the Type is set to the JSON claim 'name' after translating using this mapping. Otherwise, no mapping occurs.
The default value is true.

Let's go ahead and add the missing setup to our action.

services.AddAuthentication("Bearer")
    .AddJwtBearer(options =>
    {
        options.Authority = "https://identity.important.stuff";
        options.Audience = "someservice";
        options.TokenValidationParameters.ValidIssuers = new []{"https://identity.important.stuff", "https://identity.newaddress.important.stuff"};
        options.SecurityTokenValidators.Clear();
        options.SecurityTokenValidators.Add(new JwtSecurityTokenHandler
        {
            MapInboundClaims = false
        });
        options.TokenValidationParameters.NameClaimType = "name";
        options.TokenValidationParameters.RoleClaimType = "role";
    })

After that everything should work as expected. This is meant as a temporary solution to migrating users into a new issuer without forcing a logout. It's nice to see that the possibility is there to have multiple valid issuers, however, beware of the pitfalls when moving to a more direct configuration.