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.