identityserver4userinfo

IdentityServer4 - access token only contains sub claim


I've finally been able to get an access token from IdentityServer, and use it to retrieve the UserInfo.

However it seems that the access token I'm getting from IdentityServer only contains the sub claim.

Here is an example token

eyJhbGciOiJSUzI1NiIsImtpZCI6IjVCOTBDN0JBNkExMjI2RjEyMEU0QzJGOEQzMjIwMzAxIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2MzcwNTI5MDIsImV4cCI6MTYzNzEzOTMwMiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTkiLCJhdWQiOiJ3ZWF0aGVyZm9yZWNhc3RzIiwiY2xpZW50X2lkIjoiU3NvQXBwbGljYXRpb25DbGllbnQiLCJjZW50cmFsLXRoZWNsaWVudCI6IlRoZSBTU08gY2xpZW50Iiwic3ViIjoiYTUxYjBkZWMtODQyYS00ZWMyLTgwMGEtMzRmYWQyNTRjZTBlIiwiYXV0aF90aW1lIjoxNjM3MDUyMDcyLCJpZHAiOiJsb2NhbCIsImp0aSI6IkQ3Rjc2MzgwQzNERDkzMzVERDVFMTE1NjE4MDkzNEUwIiwic2lkIjoiOEY1OUZFMjRDNkY1NTM2ODhEQzVCMTM5QjNFQUU1MTMiLCJpYXQiOjE2MzcwNTI5MDIsInNjb3BlIjpbIm9wZW5pZCIsInByb2ZpbGUiLCJlbWFpbCIsIndlYXRoZXJmb3JlY2FzdHMucmVhZCIsIndlYXRoZXJmb3JlY2FzdHMud3JpdGUiXSwiYW1yIjpbInB3ZCJdfQ.MiDmgc7AzbVpogbp8ID3WvJ0eo4a30_taxR9EI9ylyJASSdOiNSsk-sGuW-YnJIzf668EQGkTym6FMIvOyTxem9DxxIs8nI_rboHLvuvj4e7CtJeELwbZyraZtAxjVjm9tn0BVRZxuskzb6XSq4xGrt2ag_E0Re5MeQOjtyL0EeMS5md5IEywfD7ThH7pIu8SofFvV5tAYbwO-OPd5YyqpPGKXslRtFlyc7lj9faQh-e2CRMql5rSwhJRqCiaIaLxvXk8ZwISfdhmuyzHA88xrzXkqTK_RElhq4PY_GqpRe64nMvIBrkSeoOGLzlQNE9wa58UypZFFV4l8Cpy3_P2Q

Here is the decoded token

Decoded Bearer token received from IdentityServer

I should be able to use this token to call the /connect/userinfo endpoint

Response from /connect/userinfo

And I can see this in the Output window:

IdentityServer4.Hosting.EndpointRouter: Debug: Request path /connect/userinfo matched to endpoint type Userinfo
IdentityServer4.Hosting.EndpointRouter: Debug: Endpoint enabled: Userinfo, successfully created handler: IdentityServer4.Endpoints.UserInfoEndpoint
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.UserInfoEndpoint for /connect/userinfo
IdentityServer4.Endpoints.UserInfoEndpoint: Debug: Start userinfo request
IdentityServer4.Validation.BearerTokenUsageValidator: Debug: Bearer token found in header
IdentityServer4.Endpoints.UserInfoEndpoint: Trace: Calling into userinfo request validator: IdentityServer4.Validation.UserInfoRequestValidator
IdentityServer4.Validation.TokenValidator: Trace: Start access token validation
IdentityServer4.EntityFramework.Stores.ClientStore: Debug: SsoApplicationClient found in database: True
IdentityServer4.Stores.ValidatingClientStore: Trace: Calling into client configuration validator: IdentityServer4.Validation.DefaultClientConfigurationValidator
IdentityServer4.Stores.ValidatingClientStore: Debug: client configuration validation for client SsoApplicationClient succeeded.
IdentityServer4.EntityFramework.Stores.ClientStore: Debug: SsoApplicationClient found in database: True
IdentityServer4.Stores.ValidatingClientStore: Trace: Calling into client configuration validator: IdentityServer4.Validation.DefaultClientConfigurationValidator
IdentityServer4.Stores.ValidatingClientStore: Debug: client configuration validation for client SsoApplicationClient succeeded.
IdentityServer4.Validation.TokenValidator: Debug: Calling into custom token validator: IdentityServer4.Validation.DefaultCustomTokenValidator
IdentityServer4.Validation.TokenValidator: Debug: Token validation success
{
  "ValidateLifetime": true,
  "AccessTokenType": "Jwt",
  "ExpectedScope": "openid",
  "JwtId": "2FB8F8A941528DAF18D8C523BCC9A770",
  "Claims": {
    "nbf": 1637062004,
    "exp": 1637148404,
    "iss": "https://localhost:44359",
    "aud": "weatherforecasts",
    "client_id": "SsoApplicationClient",
    "central-theclient": "The SSO client",
    "sub": "959c9bfa-ed30-4638-9986-63cf1589eff8",
    "auth_time": 1637060908,
    "idp": "local",
    "jti": "2FB8F8A941528DAF18D8C523BCC9A770",
    "sid": "75C294FC15A544FE60E361B495EE5BCA",
    "iat": 1637062004,
    "scope": [
      "openid",
      "profile",
      "email",
      "weatherforecasts.read",
      "weatherforecasts.write"
    ],
    "amr": "pwd"
  }
}
IdentityServer4.Endpoints.UserInfoEndpoint: Trace: Calling into userinfo response generator: IdentityServer4.ResponseHandling.UserInfoResponseGenerator
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Debug: Creating userinfo response
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Debug: Scopes in access token: openid profile email weatherforecasts.read weatherforecasts.write
IdentityServer4.EntityFramework.Stores.ResourceStore: Debug: Found openid, profile, email identity scopes in database
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Debug: Requested claim types: 
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Information: Profile service returned the following claim types: 
IdentityServer4.Endpoints.UserInfoEndpoint: Debug: End userinfo request

You can even see my custom dedicated client claims being appended during the token validation. But yet only the sub claim is returned from the identityserver userinfo endpoint...

How can I fix this?

Edit

I added the ProfileService, and it's being called. But as Dejan already mentioned, RequestedClaimTypes is empty. However I updated the database by adding

[{"type":"email","identityResourceId":1003},{"type":"sub","identityResourceId":1003}]

records to the IdentityResourceClaim table. In the output window I can now see that the email and sub claims are requested:

IdentityServer4.Validation.TokenValidator: Debug: Token validation success
{
  "ValidateLifetime": true,
  "AccessTokenType": "Jwt",
  "ExpectedScope": "openid",
  "JwtId": "C37AF164BF3A7DE6A28FA7538683248F",
  "Claims": {
    "nbf": 1637069285,
    "exp": 1637155685,
    "iss": "https://localhost:44359",
    "aud": "weatherforecasts",
    "client_id": "SsoApplicationClient",
    "central-theclient": "The SSO client",
    "sub": "959c9bfa-ed30-4638-9986-63cf1589eff8",
    "auth_time": 1637067659,
    "idp": "local",
    "email": "pieterjan@example.com",
    "name": "Pieterjan",
    "jti": "C37AF164BF3A7DE6A28FA7538683248F",
    "sid": "BBFA9FD0A06824FA4E982DB1D3669A86",
    "iat": 1637069285,
    "scope": [
      "openid",
      "profile",
      "email",
      "weatherforecasts.read",
      "weatherforecasts.write"
    ],
    "amr": "pwd"
  }
}
IdentityServer4.Endpoints.UserInfoEndpoint: Trace: Calling into userinfo response generator: IdentityServer4.ResponseHandling.UserInfoResponseGenerator
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Debug: Creating userinfo response
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Debug: Scopes in access token: openid profile email weatherforecasts.read weatherforecasts.write
IdentityServer4.EntityFramework.Stores.ResourceStore: Debug: Found openid, profile, email identity scopes in database
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Debug: Requested claim types: email sub
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Information: Profile service returned the following claim types: email name
IdentityServer4.Endpoints.UserInfoEndpoint: Debug: End userinfo request
IdentityServer4.Hosting.IdentityServerMiddleware: Trace: Invoking result: IdentityServer4.Endpoints.Results.UserInfoResult

The code finally enters the ExternalLoginCallback, but the await signInManager.GetExternalLoginInfoAsync() returns null now.

The response from https://localhost:44359/connect/userinfo is

{
    "email": "pieterjan@example.com",
    "name": "Pieterjan",
    "sub": "959c9bfa-ed30-4638-9986-63cf1589eff8"
}

Solution

  • The userinfo endpoint calls GetProfileDataAsync from IdentityServer4.Services.IProfileService to obtain the requested claim values. So the simple solution would be to implement that service.

    Assuming you have your user manager defined as IMyUserManager and that it has the methods referenced here (GetClaimsForUser and IsActive), the simplified implementation might look like this:

    public class MyProfileService : IProfileService
    {
        private readonly IMyUserManager userManager;
    
        public MyProfileService(IMyUserManager usermanager)
        {
            this.userManager = userManager;
        }
    
        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            // context.RequestedClaimTypes will contain the claims you requested when invoking the token endpoint
            var myClaims = await userManager.GetClaimsForUser(context.Subject, context.RequestedClaimTypes);
            context.IssuedClaims = myClaims;
        }
    
        public async Task IsActiveAsync(IsActiveContext context)
        {
            context.IsActive = await userManager.IsActive(context.Subject);
        }
    }
    

    You would then need to configure IdentityServer4 to use this implementation, by calling AddProfileService at startup like this:

    services.AddIdentityServer()
        // ...
        .AddProfileService<MyProfileService>();
    

    I should also add that, in order for your wanted claim to be present in context.RequestedClaimTypes, it needs to be associated with the scopes you directly request when making a call to token endpoint.

    At startup, when configuring IdentityServer4, make sure the identity resources you supplied to your AddIdentityResources call contains the required claim.