Migrating IdentityServer4 to v4

Introduction

IdentityServer4 announced v4 on mid-June. Checkout the release notes here. The big new features added for this release are listed on leastprivilege blog post.

This week I got a chance to migrate my samples repo to v4. In this post I want to talk about some issues I faced during migration.

API resource and scope handling/validation

In the list of big new features there is an item as Re-worked API resource and scope handling/validation. To fix issues caused by this change migration steps to v4 is suggested to do as follow:

As described above, starting with v4, scopes have their own definition and can optionally be referenced by resources. Before v4, scopes where always contained within a resource.

To migrate to v4 you need to split up scope and resource registration, typically by first registering all your scopes (e.g. using the AddInMemoryApiScopes method), and then register the API resources (if any) afterwards. The API resources will then reference the prior registered scopes by name.

But let’s see what does this mean in action. I believe going through what we have on IdentityServer4 v3 before migration, will help to understand migration’s required changes much better.

Before migration:

Let’s assume we have an IdentityServer4 v3 instance in place, an MVC app with OpenId Connect authentication, and an API with bearer authorization. All working with v3 before migration. Let’s go through current setups:

Here is the IdentityServer setup:

var builder = services.AddIdentityServer(    
   .AddInMemoryIdentityResources(Config.Ids)
   .AddInMemoryApiResources(Config.Apis)
   .AddInMemoryClients(Config.Clients)
   .AddTestUsers(TestUsers.Users);

The API resources are:

public static IEnumerable<ApiResource> Apis =>
   new ApiResource[] 
   {
      new ApiResource("api1"),
      new ApiResource("api2")
   };

With above setup, scopes_supported property on discovery document(/.well-known/openid-configuration) contains api1 and api2:

"scopes_supported": [
    "openid",
    "profile",
    "api1",
    "api2",
    "offline_access"
  ],

And a generated access_token contains aud property with values as api1 and api2. Also scope property contains api1 and api2. Here is access_token decrypted using https://jwt.ms/:

{
  "typ": "at+jwt"
}.{
  "iss": "http://localhost:5000",
  "aud": [
    "api1",
    "api2"
  ],
  "client_id": "mvcclient",
  "sub": "818727",
  "scope": [
    "openid",
    "profile",
    "api1",
    "api2",
    "offline_access"
  ]
}

As mentioned above we have an MVC app, here is MVC app client’s config on IdentityServer:

new Client
{
   ClientName = "MVC website",
   ClientId = "mvcclient",
   ClientSecrets =
   {
      new Secret("secret2".Sha256())
   },
   AllowedGrantTypes = GrantTypes.Code,
   RequireConsent = false,
   RequirePkce = true,
   RedirectUris = { "http://localhost:5002/signin-oidc" },
   PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
   AllowedScopes = {"openid", "profile", "offline_access", "api1", "api2" },
   AllowOfflineAccess = true,
},

And OpenId Connect setup on MVC app itself:

.AddOpenIdConnect("oidc", options =>
{
   options.Authority = "http://localhost:5000";
   options.RequireHttpsMetadata = false;
   options.ClientId = "mvcclient";
   options.ClientSecret = "secret2";
   options.ResponseType = "code";
   options.SaveTokens = true;
   options.Scope.Add("api1");
   options.Scope.Add("api2");
   options.Scope.Add("offline_access");
   options.GetClaimsFromUserInfoEndpoint = true;
});

And I have an API with bearer authorization. It works with access_token like what we had above. In-fact the API authorization as setup bellow, works with any access_token which is issued by http://localhost:5000 and contains aud equals api1.

services.AddAuthentication("Bearer").AddJwtBearer("Bearer",
   options =>
   {
      options.Authority = "http://localhost:5000";
      options.Audience = "api1";
      options.RequireHttpsMetadata = false;
   });

Find full code for above snippets on: IdentityServer, MVC app, API

After migration:

Now its time to update IdentityServer4 nuget package to v4. I kept IdentityServer4 setup unchanged. Browsing discovery document(/.well-known/openid-configuration) shows that api1 and api2 are removed from scopes_supported on new version:

 "scopes_supported": [
    "openid",
    "profile",
    "offline_access"
  ],

When decrypting a generated access_token using https://jwt.ms/ it shows that api1 and api2 values are removed from scope. And aud property is gone.

{
  "alg": "RS256",
  "kid": "EBD033C780FAF28B3066CB8CF5E5301D",
  "typ": "at+jwt"
}.{
  "nbf": 1593755831,
  "exp": 1593759431,
  "iss": "http://localhost:5000",
  "client_id": "mvcclient",
  "sub": "818727",
  "auth_time": 1593755831,
  "idp": "local",
  "jti": "EDFB6AC9D69F642B2E349CAC9CE18217",
  "sid": "D7CFD1895C29825DAA7DA6DBAD2AA7A7",
  "iat": 1593755831,
  "scope": [
    "openid",
    "profile",
    "offline_access"
  ],
  "amr": [
    "pwd"
  ]
}.[Signature]

As a result, on MVC app client I got invalid_scope error.

Sorry, there was an error : invalid_scope
Invalid scope

I should mention that we didn’t make any changes to MVC client’s config or OpenId Connect setup.

The invalid_acope error reason is that api1 and api2 are listed as scope on OpenID Connect setup, but we already know that they have been removed from supported scopes on discovery document(/.well-known/openid-configuration). Read more here

How we can fix this? Lets take a peek at migration steps to v4.

starting with v4, scopes have their own definition and can optionally be referenced by resources. Before v4, scopes where always contained within a resource.

To migrate to v4 you need to split up scope and resource registration, typically by first registering all your scopes (e.g. using the AddInMemoryApiScopes method), and then register the API resources (if any) afterwards. The API resources will then reference the prior registered scopes by name.

To add the scopes to the list scopes_supported we need to add api scopes by calling AddInMemoryApiScopes.

public static IEnumerable<ApiScope> ApiScopes =>
   new ApiScope[]
   { 
      new ApiScope("api1"),
      new ApiScope("api2"),
   };

public static IEnumerable<ApiResource> ApiResources =>
   new ApiResource[]
   {
      new ApiResource("api1"),
      new ApiResource("api2")
   };
var builder = services.AddIdentityServer()
   .AddInMemoryIdentityResources(Config.IdentityResources)
   .AddInMemoryApiScopes(Config.ApiScopes)
   .AddInMemoryApiResources(Config.ApiResources)
   .AddInMemoryClients(Config.Clients)
   .AddTestUsers(TestUsers.Users);

After this change api1 and api2 are listed on scopes_supported of discovery document(/.well-known/openid-configuration). This will fix the invalid_scope issue on MVC app and login will work fine.

"scopes_supported": [
    "openid",
    "profile",
    "api1",
    "api2",
    "offline_access"
  ],

But I’m still experiencing 401 Unauthorized error when trying to call the API using the access_token.

API default behaviour is to ask for aud claim as api1 in the token when the set up is options.Audience = "api1". Whereas access token does not contain aud claim.

To fix the API we have two options, I start with the easier one:

Option 1: We can change the API to remove audience validation. API authorisation will work, as long as access_token is issued by Authority equals "http://localhost:5000".I suggest to make the API secure by adding scope validation via Authorization policies .

Code change to remove audience validation is by setting ValidateAudience = false:

services.AddAuthentication("Bearer").AddJwtBearer("Bearer",
   options =>
   {
      options.Authority = "http://localhost:5000";
      options.Audience = "api1";
      options.RequireHttpsMetadata = false;
      options.TokenValidationParameters = new 
         TokenValidationParameters()
         {
            ValidateAudience = false
         };
   });

Option 2: Other option is to add the aud claim to the access token. Easiest fix would be to change like this:

return new List<ApiResource>()
{
    new ApiResource("api1")
    {
       Scopes = new []{ "api1" }
    },
    new ApiResource("api2")
    {
       Scopes = new []{ "api2" }
    }
};

The new generated access_token has a property as aud with value as api1.

{
  "alg": "RS256",
  "kid": "EBD033C780FAF28B3066CB8CF5E5301D",
  "typ": "at+jwt"
}.{
  "nbf": 1593792433,
  "exp": 1593796033,
  "iss": "http://localhost:5000",
  "aud": [
    "api1",
    "api2"
  ],
  "client_id": "mvcclient",
  "sub": "818727",
  "auth_time": 1593792432,
  "idp": "local",
  "jti": "9DE3C1DB5ABBBAC69627DFCF1C1CD028",
  "sid": "2B49D2DA2DCD8D5F0E93BA3CF865F2BE",
  "iat": 1593792433,
  "scope": [
    "openid",
    "profile",
    "api1",
    "api2",
    "offline_access"
  ],
  "amr": [
    "pwd"
  ]
}.[Signature]

Option 2 is the only solution if you are using IdentityServer authentication handler on API. On authentication handler, the ApiName property checks if the token has a matching audience. Read more here

I should say that what I suggested on option 2 is not reflecting best practice for setting API resources and scopes. I just have quick fix for the issues caused by migration. For best practices I recommend to follow docs.

Find full code for above snippets on: IdentityServer, MVC app, API

Closing

Feel free to contact me if you still face troubles going through migration. There is also an open issue on IdentityServer repo to report all migration issues you run into.

See also in IdentityServer4

comments powered by Disqus