4

Ideal functionality: A user is logged in and authenticated to website A. They click a button, the backend looks up the ID of the account in website B from the database, then send this information to IdentityServer to create a JWT that contains the "user_id" field. This is then used to call a REST endpoint on website B and is authenticated, then the "user_id" is used to create a log in cookie which is sent back to website A. User is then redirected.

We are running IdentityServer 4, but communicating to it using IdentityServer3 as our main codebase is on .NET Framework. I've tried including the "user_id" field in the extras parameter, but this doesn't appear to do anything.

var client = new TokenClient(requestPath, CLIENT_ID, CLIENT_SECRET, 
  AuthenticationStyle.PostValues);


var test = new Dictionary<string, string>
{
  { "user_id", "123123" }
};

// request token
var tokenResponse = await client
  .RequestClientCredentialsAsync(apiScope, test)
  .ConfigureAwait(false);

I've also tried using client.RequestCustomAsync and client.RequestAsync with no luck.

I receive a token without issue, but it doesn't include the user_id information - only the normal audience, scope, expiration times, etc.

Evan
  • 41
  • 1
  • 4
  • Is "user_id" a custom claim which you are trying to add and include in access token? – Aparna Gadgil Sep 09 '19 at 14:48
  • It seems like that would be the easiest way to accomplish this. I have access to the user_id I want to include in the access token when requesting the token, so I was trying to avoid having to do something in IdentityServer to have to make a call back to website A to get the user_id – Evan Sep 09 '19 at 15:17
  • Isn't the user-id not already present in the token via the sub claim ? – Frederik Gheysels Sep 09 '19 at 15:32
  • You should reconsider your design. Websites shouldn't have access to identity tables, this is the responsibility of IdentityServer. For safety, users login on the IdentityServer website. Clients should be ignorant of user's credentials. The only exception is the resource owner flow, but that is generally not the recommended flow. Client Credentials is for machine to machine communication. It doesn't contain the sub claim for that reason. If you want it to be part of the token, use a different flow, e.g. [hybrid flow](http://docs.identityserver.io/en/latest/topics/grant_types.html). –  Sep 09 '19 at 16:36
  • I'll look more into the hybrid flow, thanks. If it helps, since we own both of these websites there is no storing of a user's credentials for website B - instead, we have a database table that says "website A account 123 has access to website B account 456". Account 456 is what I'm trying to include in the token that is passed over. – Evan Sep 09 '19 at 17:49
  • Instead of adding a `user_id` you can simply use the `sub` claim. Just make sure you use the correct flow where the `sub` claim is available. Since authorization seems seperated, you may want to take a look at the [PolicyServer](https://policyserver.io/), which is also a product of the creators of IdentityServer. –  Sep 09 '19 at 18:05

3 Answers3

0

For custom issue fields, you need to create a inherited class of IProfileService and then implement method GetProfileDataAsync. It looks like:

public class IdentityProfileService : IProfileService
{
    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var sub = context.Subject.GetSubjectId();
        // Call UserManager or any database to get more fields
        var user = await _userManager.FindByIdAsync(sub);

        // Set returned claims (System.Security.Claims.Claim) by setting context.IssuedClaims
        context.IssuedClaims = claims;
    }

    public async Task IsActiveAsync(IsActiveContext context)
    {
        context.IsActive = true;
    }
}

And in your registering Identity Service 4, you need to declare it (a sample is in .NET Core, same with .NET Framework)

var identityBuilder = services.AddIdentityServer().AddProfileService<IdentityProfileService>();
Johnathan Le
  • 675
  • 3
  • 8
  • I have seen this recommendation in a few places, but where I have access to the user_id I want to include in the token at the time I'm requesting it, I was trying to avoid having to go from website A to IdentityServer back to website A just to get the user_id – Evan Sep 09 '19 at 15:18
  • 6
    Client Credentials has no user, so the ProfileService won't be used. –  Sep 09 '19 at 16:10
  • this is wrong , profile service dont fore in case of Client Credentials – IbraHim M. Nada Oct 06 '22 at 07:22
0

I think you should check ApiResource configuration. Whatever claims you add in UserClaims property of ApiResource configuration, those claims will appear in access token. e.g

 public IEnumerable<ApiResource> GetApiResources()
 {
      return new List<ApiResource>
      {
            new ApiResource("api1")
            {
                UserClaims = new[] { "CustomClaim1", "CustomClaim2"},
            }, 
       }
 }

In above code, access code will contain CustomClaim1 and CustomClaim2. Hence if you don't mention them, they won't appear in access token.

Aparna Gadgil
  • 410
  • 4
  • 9
  • I'm using the Admin UI to create API resources rather than code, but I did create a user_id claim that I included in the list of claims on both the API claims as well as the scope claims. They are still not included in the access token – Evan Sep 09 '19 at 15:44
  • Have you included them in UserClaims where you register your API? – Aparna Gadgil Sep 09 '19 at 15:46
0

Here is what worked for me, largely following this example

In IdentityServer, create a new class UserInfoGrant that implements IExtensionGrantValidator, extracts the custom claims from the request, adds them to the claims, and then continues

public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
  var userId = context.Request.Raw[UserIdKey];
  ...
  var claims = new List<Claim>
  {
    new Claim(UserIdKey, userId)
  }
  context.Result = new GrantValidationResult(sub, GrantType, claims);
}

I then added the class to the Dependency Injection

builder.AddExtensionGrantValidator<UserInfoGrant>();

I also have a class ProfileService that implements IProfileService which adds the claims to the token that is returned

public virtual async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
  var authenticationType = context.Subject.Identities.First().AuthenticationType;
  var isCustomAuthenticationType = authenticationType.Equals(CustomGrantName,
    StringComparison.OrdinalIgnoreCase);
  if (isCustomAuthenticationType)
  {
    var claimsToAdd = context.Subject.Identities.First().Claims;
    context.IssuedClaims = claimsToAdd.ToList();
  }
  else { ... }

This ProfileService was also added to DI

builder.AddProfileService<Helpers.ProfileService<TUser>>();

I also added the custom grant type to the client that would be using it.

Finally, in the calling code for Website A, I request the token with this:

var tokenResponse = await client.RequestTokenAsync(new TokenRequest {
                    Address = disco.TokenEndpoint,
                    ClientId = CLIENTID,
                    ClientSecret = CLIENTSECRET,
                    GrantType = "custom_grant_name",
                    Parameters = {
                        { "scope", PROTECTED_RESOURCE_NAME },
                        { "user_id", "26616" }
                      }
                    }).ConfigureAwait(false);
Evan
  • 41
  • 1
  • 4