Search code examples
c#asp.net-coreidentityserver4claims

nameidentifier claim is required for external login to succeed. How to remove duplicate keys in token


I've finally managed to setup a project using IdentityServer4 to allow users to sign in with a single account into multiple apps. However, I feel like it's not completely as it should be.

This is my OAuthOptions class

public class CentralOptions : OAuthOptions
{
    public CentralOptions()
    {
        ClaimsIssuer = "https://localhost:44359";
        CallbackPath = new Microsoft.AspNetCore.Http.PathString("/signin-central");
        AuthorizationEndpoint = "https://localhost:44359/connect/authorize";
        TokenEndpoint = "https://localhost:44359/connect/token";
        UserInformationEndpoint = "https://localhost:44359/connect/userinfo";

        Scope.Add("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier");
        Scope.Add("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name");
        Scope.Add("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
        Scope.Add("openid");
        Scope.Add("profile");
        Scope.Add("email");
        Scope.Add("phone");
        Scope.Add("role");
        Scope.Add("weatherforecasts.read");
        Scope.Add("weatherforecasts.write");

        UsePkce = true;
        ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "sub");
        ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "name");
        ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "email");

        //ClaimActions.MapJsonKey("sub", "sub");
        //ClaimActions.MapJsonKey("name", "name");
        //ClaimActions.MapJsonKey("email", "email");
    }
}

As you can see, right now I have to duplicate the RequestedClaims, once for the actual claim type, once for some shortname. I've been tweaking the contents of the database for some time, but can't figure out what I have to change to only have the claims once (I suppose that http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress should not be present in the bearer token, and email should. But if I change the claims in the database and the application, the signin fails because it relies on the ClaimTypes.NameIdentifier claim to be present in the bearer token.

After I signed in, IS gives me an access token, for example:

eyJhbGciOiJSUzI1NiIsImtpZCI6IjVCOTBDN0JBNkExMjI2RjEyMEU0QzJGOEQzMjIwMzAxIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2MzcxNzkxNDAsImV4cCI6MTYzNzI2NTU0MCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTkiLCJhdWQiOiJ3ZWF0aGVyZm9yZWNhc3RzIiwiY2xpZW50X2lkIjoiU3NvQXBwbGljYXRpb25DbGllbnQiLCJjZW50cmFsLXRoZWNsaWVudCI6IlRoZSBTU08gY2xpZW50Iiwic3ViIjoiOTU5YzliZmEtZWQzMC00NjM4LTk5ODYtNjNjZjE1ODllZmY4IiwiYXV0aF90aW1lIjoxNjM3MTc5MTM3LCJpZHAiOiJsb2NhbCIsImVtYWlsIjoicGlldGVyamFuQGV4YW1wbGUuY29tIiwibmFtZSI6IlBpZXRlcmphbiIsImlkIjoiOTU5YzliZmEtZWQzMC00NjM4LTk5ODYtNjNjZjE1ODllZmY4IiwicGhvbmUiOiIrMzIxMjMvNDUuNjcuODkiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiUGlldGVyamFuIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvZW1haWxhZGRyZXNzIjoicGlldGVyamFuQGV4YW1wbGUuY29tIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbW9iaWxlcGhvbmUiOiIrMzIxMjMvNDUuNjcuODkiLCJqdGkiOiI3RjA0QTA5MDM3MUNEMjQ2MENCQzg3OUY3MDEwOTU1MyIsInNpZCI6IjA0NDYzRDlBRDNENDRCNUExQTNCQTRFOTczRUE5OTI4IiwiaWF0IjoxNjM3MTc5MTQwLCJzY29wZSI6WyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiLCJvcGVuaWQiLCJwcm9maWxlIiwiZW1haWwiLCJwaG9uZSIsInJvbGUiLCJ3ZWF0aGVyZm9yZWNhc3RzLnJlYWQiLCJ3ZWF0aGVyZm9yZWNhc3RzLndyaXRlIl0sImFtciI6WyJwd2QiXX0.KBKLezXnUs6s-bU9hme7Ab7ADZN8DEewqfUncDwR0c2_LFqAnyCw3IZ85VJC4t-NN6xJYu8ROk-cX9PDKIQzEAOWGkOrQuqeaspKfIpl_rCq4qbP7x7uflToqPO245iU6xlzxVnGuaG1o_sSILNQA_YZJV8nsmXJkdB2QonuCZwvrBh5URFXV5cZpivlWznJls9eqfRM9MjlRpWe-NCI6I7FExfCaRgPZ4b1XwyrmmQWNlaKJOmIM3qag1pQshdXBSzg3w65htj89zOKKWSNl6Go6Q_0pZzbv0FLcMUMR_GTzuw56_CFobavD40T65wQQlXxf0cfkzbrdyAx7k8tyg

Decoded it looks like this

{
  "alg": "RS256",
  "kid": "5B90C7BA6A1226F120E4C2F8D3220301",
  "typ": "at+jwt"
}
{
  "nbf": 1637179140,
  "exp": 1637265540,
  "iss": "https://localhost:44359",
  "aud": "weatherforecasts",
  "client_id": "SsoApplicationClient",
  "central-theclient": "The SSO client",
  "sub": "959c9bfa-ed30-4638-9986-63cf1589eff8",
  "auth_time": 1637179137,
  "idp": "local",
  "email": "pieterjan@example.com",
  "name": "Pieterjan",
  "id": "959c9bfa-ed30-4638-9986-63cf1589eff8",
  "phone": "+32123/45.67.89",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Pieterjan",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "pieterjan@example.com",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone": "+32123/45.67.89",
  "jti": "7F04A090371CD2460CBC879F70109553",
  "sid": "04463D9AD3D44B5A1A3BA4E973EA9928",
  "iat": 1637179140,
  "scope": [
    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
    "openid",
    "profile",
    "email",
    "phone",
    "role",
    "weatherforecasts.read",
    "weatherforecasts.write"
  ],
  "amr": [
    "pwd"
  ]
}

With this token you can send a request to

https://localhost:44359/connect/userinfo

And this gives the following response

{
    "email": "pieterjan@example.com",
    "name": "Pieterjan",
    "id": "959c9bfa-ed30-4638-9986-63cf1589eff8",
    "phone": "+32123/45.67.89",
    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Pieterjan",
    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "pieterjan@example.com",
    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone": "+32123/45.67.89",
    "sub": "959c9bfa-ed30-4638-9986-63cf1589eff8"
}

It seems to me that you're supposed to have only shortname qualifiers in the response (email, name, sub, phone), is that correct? But if I rearrange this, the response from /connect/userinfo won't contain a http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name claim, and the signin will fail at following line which looks for this exact claim in the response from this very UserInfo endpoint, and thus will fail.

I'm guessing I have to tweak the OAuthOptions.ClaimActions which currently read this:

ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "sub");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "name");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "email");

I've been playing around with them already, but the slightest modification results in all claims no longer being returned from IdentityServer.

dbo.Clients

IdentityServer clients in database

dbo.ClientGrantType

= client.allowedGrantTypes

Allowed grant-types for the SsoApplicationClient

dbo.ClientSecret

= client.ClientSecrets

Client secrets

dbo.ClientScope

= client.AllowedScopes

Allowed scopes for the SsoApplicationClient

So here you see that right now I have scopes which probably shouldn't be in the database, since they're actually claim types, but if I remove them the NameIdentifier claim type will not be present in the Identity.

dbo.ClientRedirectUri

= client.RedirectUris

Authorized redirect urls for the SsoApplicationClient

dbo.ClientClaim

= client.Claims

Claims returned by IdentityServer for each request to this client

dbo.IdentityResources

Identity resources in the database

dbo.IdentityResourceClaim

= identityResource.UserClaims

Of which the markup reads:

List of associated user claims that should be included when this resource is requested.

User claims for the identity resources

So clearly here I had to introduce double lines for the external login to start working.

dbo.AspNetUsers

Users in my test database

dbo.AspNetUserClaims

= user.Claims

Claims associated to the user at database level

How can I setup my code and database correctly so that I no longer need those duplicate claims for my application to work/my external login to succeed?

Also, should the claims be persisted at database level, or generated during login?

Thanks in advance.

Git repository


Solution

  • I would only use the shorter claim names in IdentityServer and do the necessary claims transformation or mapping in the client.

    I would look at doing the transform in the client or API using the MapUniqueJsonKey:

    options.ClaimActions.MapUniqueJsonKey("website", "website");
    options.ClaimActions.MapUniqueJsonKey("gender", "gender");
    options.ClaimActions.MapUniqueJsonKey("birthdate", "birthdate");
    

    I tink its important to understand what the ClaimsPrincipal user object contains after authentication (but before Authorization).

    For more advanced transformation needs, then do take a look at using the IClaimsTransformation interface.

    To complement this answer, I wrote a blog post that goes into more detail about this topic: Debugging OpenID Connect claim problems in ASP.NET Core

    More info: