Search code examples
asp.net-core.net-coreidentityserver4

Add claims to access token depending on clients


I have the situation when one web app needs access token with one claim and another web app needs another claim.

For example, client_1 should have access token with claim is_admin and client_2 should have claim stores.

What I want to know - is it normal to add such information in such a manner in access token, or is there a better alternative?

And if to add this info in token - can you suggest how to distinguish which claim to add in IProfileService implementation depending on a client? Is it good to use ClientProperties for this purpose?

UPDATE: I use Azure AD as external identity provider and users don't have those claims - I need to retrieve it from other sources


Solution

  • In your configuration add an IdentityResource that represents the scope of the client app, like client_1_scope, including the IdentityClaim is_admin.

    Do the same for client_2_scope including IdentityClaim stores. Also allow the client to request the defined scope (add a record in ClientScopes).

    In client_1 request the scope client_1_scope like this:

    options.Scope.Add("client_1_scope");
    

    And in client_2 like this:

    options.Scope.Add("client_2_scope");
    

    When the user has a claim is_admin or stores, then the claim will be included as part of the requested scope only.

    Add this line to make sure the claims are added:

    options.GetClaimsFromUserInfoEndpoint = true;
    

    In the client_1 app the configuration could look something like this:

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    
    services
        .AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";
                options.GetClaimsFromUserInfoEndpoint = true;
                options.ClaimActions.MapAll();
    
                options.Scope.Add("client_1_scope");
    
                options.Authority = "";
                options.ClientId = "";
                options.ClientSecret = "";
                options.ResponseType = "code id_token";
            });
    

    The information will be saved in the cookie. If you want claims to be part of an access token that is send to an Api then you should use the Api~ tables to configure the claims.


    If those other sources already exist then you can implement IProfileService where you can add the dynamic claims, based on the scope as described above, so you still need to request the scope.

    Something like:

    using IdentityServer4.Models;
    using IdentityServer4.Services;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Threading.Tasks;
    
    public class MyProfileService : IProfileService
    {
        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            // Include configured claims.
            context.AddRequestedClaims(context.Subject.Claims);
    
            // The service gets called multipe times. In this case we need
            // the UserInfo endpoint because that's where the scope is defined.
            if (context.Caller == "UserInfoEndpoint")
            {
                // Get the value from somewhere and transform to a list of claims.
                // You can filter by requested scopes
                List<Claim> userClaims = GetUserClaims(context.RequestedResources.IdentityResources);
    
                if (userClaims.Any())
                    context.IssuedClaims.AddRange(userClaims);
            }
        }
    
        public async Task IsActiveAsync(IsActiveContext context)
        {
            context.IsActive = true;
        }
    }
    

    If you want to add claims to an access token (used for api's) then context.Caller is ClaimsProviderAccessToken and you should look at context.RequestedResources.ApiResources.

    Register the service in startup:

    .AddProfileService<MyProfileService>()
    

    Please note, this is just an example. I didn't test the code.


    Having a seperate source, you can also look at PolicyServer. In that case you can keep IdentityServer for authentication and the PolicyServer for 'opt-in' authorization.