How to add claims to access token get from IdentityServer3 using resource owner flow with javascript client

13,825

Solution 1

I have just spent a while figuring this out myself. @leastprivilege's comment on Yang's answer had the clue, this answer is just expanding on it.
It's all down to how the oAuth and OIDC specs evolved, it's not an artefact of IdentityServer (which is awesome). Firstly, here is a fairly decent discussion of the differences between identity tokens and access tokens: https://github.com/IdentityServer/IdentityServer3/issues/2015 which is worth a read.

With Resource Owner flow, like you are doing, you will always get an Access Token. By default and per the spec, you shouldn't include claims in that token (see the above link for why). But, in practice, it is very nice when you can; it saves you extra effort on both client and server.

What Leastprivilege is referring to is that you need to create a scope, something like this:

new Scope
{
    Name = "member",
    DisplayName = "member",
    Type = ScopeType.Resource,

    Claims = new List<ScopeClaim>
        {
              new ScopeClaim("role"),
              new ScopeClaim(Constants.ClaimTypes.Name),
              new ScopeClaim(Constants.ClaimTypes.Email)
        },

    IncludeAllClaimsForUser = true
}

And then you need to request that scope when you ask for the token. I.e. your line scope: "openid profile roles", should change to scope: "member", (well, I say that - scopes play a dual role here, as far as I can see - they are also a form of control, i.e. the client is asking for certain scopes and can be rejected if it is not allowed those but that is another topic).

Note the important line that eluded me for a while, which is Type = ScopeType.Resource (because Access Tokens are about controlling access to resources). This means it will apply to Access Tokens and the specified claims will be included in the token (I think, possibly, against spec but wonderfully).

Finally, in my example I have included both some specific claims as well as IncludeAllClaimsForUser which is obviously silly, but just wanted to show you some options.

Solution 2

I find I can achieve this by replacing the default IClaimsProvider of IdentityServerServiceFactory.

The cusomized IClaimsProvider is as below:

public class MyClaimsProvider : DefaultClaimsProvider
{
    public MaccapClaimsProvider(IUserService users) : base(users)
    {
    }

    public override Task<IEnumerable<Claim>> GetAccessTokenClaimsAsync(ClaimsPrincipal subject, Client client, IEnumerable<Scope> scopes, ValidatedRequest request)
    {
        var baseclaims = base.GetAccessTokenClaimsAsync(subject, client, scopes, request);

        var claims = new List<Claim>();
        if (subject.Identity.Name == "bob")
        {
            claims.Add(new Claim("role", "super_user"));
            claims.Add(new Claim("role", "asset_manager"));
        }

        claims.AddRange(baseclaims.Result);

        return Task.FromResult(claims.AsEnumerable());
    }

    public override Task<IEnumerable<Claim>> GetIdentityTokenClaimsAsync(ClaimsPrincipal subject, Client client, IEnumerable<Scope> scopes, bool includeAllIdentityClaims, ValidatedRequest request)
    {
        var rst = base.GetIdentityTokenClaimsAsync(subject, client, scopes, includeAllIdentityClaims, request);
        return rst;
    }
}

Then, replace the IClaimsProvider like this:

// custom claims provider
factory.ClaimsProvider = new Registration<IClaimsProvider>(typeof(MyClaimsProvider));

The result is that, when the request for access token is sent to token endpoint the claims are added to the access_token.

Share:
13,825
Yang Zhang
Author by

Yang Zhang

Keen on latest technologies and willing to be at the forefront of innovations. I feel like to be with people passionate on technologies and strongly motivated to drive innovations.

Updated on June 06, 2022

Comments

  • Yang Zhang
    Yang Zhang almost 2 years

    I use the resource owner flow with IdentityServer3 and send get token request to identity server token endpoint with username and password in javascript as below:

            function getToken() {
            var uid = document.getElementById("username").value;
            var pwd = document.getElementById("password").value;
            var xhr = new XMLHttpRequest();
            xhr.onload = function (e) {
                console.log(xhr.status);
                console.log(xhr.response);
                var response_data = JSON.parse(xhr.response);
                if (xhr.status === 200 && response_data.access_token) {
                    getUserInfo(response_data.access_token);
                    getValue(response_data.access_token);
                }
            }
            xhr.open("POST", tokenUrl);
            var data = {
                username: uid,
                password: pwd,
                grant_type: "password",
                scope: "openid profile roles",
                client_id: 'client_id'
            };
            var body = "";
            for (var key in data) {
                if (body.length) {
                    body += "&";
                }
                body += key + "=";
                body += encodeURIComponent(data[key]);
            }
            xhr.setRequestHeader("Authorization", "Basic " + btoa(client_id + ":" + client_secret));
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xhr.send(body);
        }
    

    The access token is returned from identity server and user is authenticated. Then I use this token to send request to my Web Api.

    The problem is that when I check if the user is assigned a role, I find the claim doesn't exist.

        [Authorize]
        // GET api/values
        public IEnumerable<string> Get()
        {
            var id = RequestContext.Principal as ClaimsPrincipal;
            bool geek = id.HasClaim("role", "Geek");  // false here
            bool asset_mgr = id.HasClaim("role", "asset_manager"); // false here
            return new string[] { "value1", "value2" };
        }
    

    Here is how the client is defined in identity server.

    new Client 
                {
                    ClientName = "Client",
                    ClientId = "client_id",
                    Flow = Flows.ResourceOwner,
                    RequireConsent = false,
                    AllowRememberConsent = false,
    
                    AllowedScopes = new List<string>
                    {
                        "openid",
                        "profile",
                        "roles",
                        "sampleApi"
                    },
                    AbsoluteRefreshTokenLifetime = 86400,
                    SlidingRefreshTokenLifetime = 43200,
                    RefreshTokenUsage = TokenUsage.OneTimeOnly,
                    RefreshTokenExpiration = TokenExpiration.Sliding,
                    ClientSecrets = new List<Secret>
                    {
                        new Secret("4C701024-0770-4794-B93D-52B5EB6487A0".Sha256())
                    },
                },
    

    and this is how the user is defined:

    new InMemoryUser
                {
                    Username = "bob",
                    Password = "secret",
                    Subject = "1",
    
                    Claims = new[]
                    {
                        new Claim(Constants.ClaimTypes.GivenName, "Bob"),
                        new Claim(Constants.ClaimTypes.FamilyName, "Smith"),
                        new Claim(Constants.ClaimTypes.Role, "Geek"),
                        new Claim(Constants.ClaimTypes.Role, "Foo")
                    }
                }
    

    How can I add claims to the access_token in this case? Thanks a lot!

  • leastprivilege
    leastprivilege over 8 years
    Besides that this is not the way to do it. Create a scope and assign the claims to it that you need. Then request the scope. Our default claims provider will then call the user service to retrieve the claims.
  • Yang Zhang
    Yang Zhang over 8 years
    @leastprivilege What if the claims are different to different users? If I associate the claims to a scope, does that mean the claims will be the same for all users?
  • Yang Zhang
    Yang Zhang about 8 years
    Great answer! Very clear and helpful. If my understanding is correct, does that mean access token has no intention to include claims in it and if one wants to check the user's claims a separate request would be more preferable option? That way the claims can be different to each user with the same scope. Am I right? The problem I wanted to solve was to provide different claims to different users.
  • Yang Zhang
    Yang Zhang about 8 years
    In my application, I end up with a solution to only put generic claims in the scope and let the client application make a separate request to an api service to find out capabilities (claims) of a user.
  • flytzen
    flytzen about 8 years
    @Yang, yes, technically access tokens aren't meant to include claims like names, so technically you are supposed to get that separately. It's just inconvenient. And yes, this solution will give each user their personal claims.
  • Justin_H
    Justin_H over 7 years
    Anyone know why this doesn't work in IdentityServer4?
  • Quan
    Quan over 3 years
    This works perfectly for InMemoryUser, but when I change to use my UserService which inherits UserServiceBase, it doesn't add the roles to the access token. In AuthenticateLocalAsync, I set as the following: context.AuthenticateResult = new AuthenticateResult(context.UserName, context.UserName, claims); Anyone knows why?
  • Quan
    Quan over 3 years
    I found the answer for Quan's question, we have to override GetProfileDataAsync and add the roles to context.IssuedClaims. More info: identityserver.github.io/Documentation/docsv2/advanced/…