Identity Server 4: adding claims to access token
Asked Answered
O

3

37

I am using Identity Server 4 and Implicit Flow and want to add some claims to the access token, the new claims or attributes are "tenantId" and "langId".

I have added langId as one of my scopes as below and then requesting that through identity server, but i get the tenantId also. How can this happen?

This the list of scopes and client configuration:

  public IEnumerable<Scope> GetScopes()
    {
        return new List<Scope>
        {
             // standard OpenID Connect scopes
            StandardScopes.OpenId,
            StandardScopes.ProfileAlwaysInclude,
            StandardScopes.EmailAlwaysInclude,

            new Scope
            {
                Name="langId",
                 Description = "Language",
                Type= ScopeType.Resource,
                Claims = new List<ScopeClaim>()
                {
                    new ScopeClaim("langId", true)
                }
            },
            new Scope
            {
                Name = "resourceAPIs",
                Description = "Resource APIs",
                Type= ScopeType.Resource
            },
            new Scope
            {
                Name = "security_api",
                Description = "Security APIs",
                Type= ScopeType.Resource
            },
        };
    }

Client:

  return new List<Client>
        {
            new Client
            {
                ClientName = "angular2client",
                ClientId = "angular2client",
                AccessTokenType = AccessTokenType.Jwt,
                AllowedGrantTypes = GrantTypes.Implicit,
                AllowAccessTokensViaBrowser = true,
                RedirectUris = new List<string>(redirectUris.Split(',')), 
                PostLogoutRedirectUris = new List<string>(postLogoutRedirectUris.Split(',')),
                AllowedCorsOrigins = new List<string>(allowedCorsOrigins.Split(',')),

                AllowedScopes = new List<string>
                {
                   "openid",
                   "resourceAPIs",
                   "security_api",         
                   "role",
                  "langId"
                }
            }
        };

I have added the claims in the ProfileService:

 public class ProfileService : IdentityServer4.Services.IProfileService
{
    private readonly SecurityCore.ServiceContracts.IUserService _userService;


    public ProfileService(SecurityCore.ServiceContracts.IUserService userService)
    {
        _userService = userService;
    }

    public Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
       //hardcoded them just for testing purposes
        List<Claim> claims = new List<Claim>() { new Claim("langId", "en"), new Claim("tenantId", "123") };

        context.IssuedClaims = claims;


        return Task.FromResult(0);
    }

This is what i am requesting to get the token, the problem is i am only requesting the langId but I am getting both the tenantId and langId in the access token

http://localhost:44312/account/login?returnUrl=%2Fconnect%2Fauthorize%2Flogin%3Fresponse_type%3Did_token%2520token%26client_id%3Dangular2client%26redirect_uri%3Dhttp%253A%252F%252Flocalhost:5002%26scope%3DresourceAPIs%2520notifications_api%2520security_api%2520langId%2520navigation_api%2520openid%26nonce%3DN0.73617935552798141482424408851%26state%3D14824244088510.41368537145696305%26

Decoded access token:

 {
  "nbf": 1483043742,
  "exp": 1483047342,
  "iss": "http://localhost:44312",
  "aud": "http://localhost:44312/resources",
  "client_id": "angular2client",
  "sub": "1",
  "auth_time": 1483043588,
  "idp": "local",
  "langId": "en",
  "tenantId": "123",
  "scope": [
    "resourceAPIs",     
    "security_api",
    "langId",
    "openid"
  ],
  "amr": [
    "pwd"
  ]
}
Orator answered 29/12, 2016 at 20:57 Comment(5)
What version of IdentityServer4 is this?Aryan
"IdentityServer4": "1.0.0-rc1-update2",Orator
Any chance you've updated this for IS4 1.0 final or version 1.2?Aryan
Not yet, Why? Is there any problem?Orator
For the access token, check your ApiResource's UserClaims. Source: github.com/IdentityServer/IdentityServer4/issues/…Matless
F
8

You should check context.RequestedClaimTypes and filter out claims, that were not requested.

Ferric answered 29/12, 2016 at 22:23 Comment(2)
You are right, I should do that, but this is not my problem because if i didn't request the langId scope in the request url above. The access token returned does't contain neither langId nor tenantId (so filtering here is not the problem). And that's what i need to understand, why when requesting the langId as in the question, the tenantId was included in the access token.Orator
That is because other resource scopes don't have any claims assigned to them, so when you remove langId scope this github.com/IdentityServer/IdentityServer4/blob/dev/src/… condition is not firing.Ferric
N
58

This answer was written for Identityserver4 on .Net core 2 to use it for .Net core 3, this answer may help you, but you need to test and change a few things.


I am using asp.net Identity and Entity Framework with Identityserver4.

This is my sample code, works well and JWT contains all roles and claims

You can see how to implement Identityserver4 with ASP.Net core identity here http://docs.identityserver.io/en/release/quickstarts/6_aspnet_identity.html https://github.com/IdentityServer/IdentityServer4.Samples/tree/dev/Quickstarts/6_AspNetIdentity

1- identity server startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddMvc();

            services.AddTransient<IEmailSender, AuthMessageSender>();
            services.AddTransient<ISmsSender, AuthMessageSender>();

            //Add IdentityServer services
            //var certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "LocalhostCert.pfx"), "123456");
            services.AddIdentityServer()
                    .AddTemporarySigningCredential()
                    .AddInMemoryIdentityResources(Configs.IdentityServerConfig.GetIdentityResources())
                    .AddInMemoryApiResources(Configs.IdentityServerConfig.GetApiResources())
                    .AddInMemoryClients(Configs.IdentityServerConfig.GetClients())
                    .AddAspNetIdentity<ApplicationUser>()
                    .AddProfileService<Configs.IdentityProfileService>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                //app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseIdentity();

            // Adds IdentityServer
            app.UseIdentityServer();

            // Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Account}/{action=Login}/{id?}");
            });

        }

2- IdentityServerConfig.cs

using IdentityServer4;
    using IdentityServer4.Models;
    using System.Collections.Generic;

    namespace IdentityAuthority.Configs
    {

        public class IdentityServerConfig
        {

            // scopes define the resources in your system
            public static IEnumerable<IdentityResource> GetIdentityResources()
            {
                return new List<IdentityResource>
                {
                    new IdentityResources.OpenId(),
                    new IdentityResources.Profile()
                };
            }

            // scopes define the API resources
            public static IEnumerable<ApiResource> GetApiResources()
            {
                //Create api resource list
                List<ApiResource> apiResources = new List<ApiResource>();

                //Add Application Api API resource
                ApiResource applicationApi = new ApiResource("ApplicationApi", "Application Api");
                applicationApi.Description = "Application Api resource.";
                apiResources.Add(applicationApi);

                //Add Application Api API resource
                ApiResource definitionApi = new ApiResource("DefinitionApi", "Definition Api");
                definitionApi.Description = "Definition Api.";
                apiResources.Add(definitionApi);

                //Add FF API resource
                ApiResource ffApi = new ApiResource("FFAPI", "Fule .netfx API");
                ffApi.Description = "Test using .net 4.5 API application with IdentityServer3.AccessTokenValidation";
                apiResources.Add(ffApi);

                return apiResources;
            }

            // client want to access resources (aka scopes)
            public static IEnumerable<Client> GetClients()
            {
                //Create clients list like webui, console applications and...
                List<Client> clients = new List<Client>();

                //Add WebUI client
                Client webUi = new Client();
                webUi.ClientId = "U2EQlBHfcbuxUo";
                webUi.ClientSecrets.Add(new Secret("TbXuRy7SSF5wzH".Sha256()));
                webUi.ClientName = "WebUI";
                webUi.AllowedGrantTypes = GrantTypes.HybridAndClientCredentials;
                webUi.RequireConsent = false;
                webUi.AllowOfflineAccess = true;
                webUi.AlwaysSendClientClaims = true;
                webUi.AlwaysIncludeUserClaimsInIdToken = true;
                webUi.AllowedScopes.Add(IdentityServerConstants.StandardScopes.OpenId);
                webUi.AllowedScopes.Add(IdentityServerConstants.StandardScopes.Profile);
                webUi.AllowedScopes.Add("ApplicationApi");
                webUi.AllowedScopes.Add("DefinitionApi");
                webUi.AllowedScopes.Add("FFAPI");
                webUi.ClientUri = "http://localhost:5003";
                webUi.RedirectUris.Add("http://localhost:5003/signin-oidc");
                webUi.PostLogoutRedirectUris.Add("http://localhost:5003/signout-callback-oidc");
                clients.Add(webUi);

                //Add IIS test client
                Client iisClient = new Client();
                iisClient.ClientId = "b8zIsVfAl5hqZ3";
                iisClient.ClientSecrets.Add(new Secret("J0MchGJC8RzY7J".Sha256()));
                iisClient.ClientName = "IisClient";
                iisClient.AllowedGrantTypes = GrantTypes.HybridAndClientCredentials;
                iisClient.RequireConsent = false;
                iisClient.AllowOfflineAccess = true;
                iisClient.AlwaysSendClientClaims = true;
                iisClient.AlwaysIncludeUserClaimsInIdToken = true;
                iisClient.AllowedScopes.Add(IdentityServerConstants.StandardScopes.OpenId);
                iisClient.AllowedScopes.Add(IdentityServerConstants.StandardScopes.Profile);
                iisClient.AllowedScopes.Add("ApplicationApi");
                iisClient.AllowedScopes.Add("DefinitionApi");
                iisClient.AllowedScopes.Add("FFAPI");
                iisClient.ClientUri = "http://localhost:8080";
                iisClient.RedirectUris.Add("http://localhost:8080/signin-oidc");
                iisClient.PostLogoutRedirectUris.Add("http://localhost:8080/signout-callback-oidc");
                clients.Add(iisClient);

                return clients;
            }

        }
    }

3 - IdentityProfileService.cs

using IdentityServer4.Services;
using System;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityAuthority.Models;
using Microsoft.AspNetCore.Identity;
using IdentityServer4.Extensions;
using System.Linq;

namespace IdentityAuthority.Configs
{
    public class IdentityProfileService : IProfileService
    {

        private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
        private readonly UserManager<ApplicationUser> _userManager;

        public IdentityProfileService(IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory, UserManager<ApplicationUser> userManager)
        {
            _claimsFactory = claimsFactory;
            _userManager = userManager;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            if (user == null)
            {
                throw new ArgumentException("");
            }

            var principal = await _claimsFactory.CreateAsync(user);
            var claims = principal.Claims.ToList();

            //Add more claims like this
            //claims.Add(new System.Security.Claims.Claim("MyProfileID", user.Id));

            context.IssuedClaims = claims;
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            context.IsActive = user != null;
        }
    }

}

4 - In my client mvc core project I added 3 nuget packages

.Microsoft.AspNetCore.Authentication.Cookies

.Microsoft.AspNetCore.Authentication.OpenIdConnect

.IdentityModel

5- This is my startup.cs in my client mvc core project

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                //app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            //Setup OpenId and Identity server
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationScheme = "Cookies",
                AutomaticAuthenticate = true
            });

            app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
            {
                Authority = "http://localhost:5000",
                ClientId = "U2EQlBHfcbuxUo",
                ClientSecret = "TbXuRy7SSF5wzH",
                AuthenticationScheme = "oidc",
                SignInScheme = "Cookies",
                SaveTokens = true,
                RequireHttpsMetadata = false,
                GetClaimsFromUserInfoEndpoint = true,
                ResponseType = "code id_token",
                Scope = { "ApplicationApi", "DefinitionApi", "FFAPI", "openid", "profile", "offline_access" },
                TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role"
                }
            });

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

6 - In my API I added this nuget package

.IdentityServer4.AccessTokenValidatio

and my startup.cs is like this

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            //IdentityServer4.AccessTokenValidation
            app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
            {
                Authority = "http://localhost:5000",
                RequireHttpsMetadata = false,
                ApiName = "ApplicationApi"
            });

            app.UseMvc();
        }

Now I can use [Authorize(Role="SuperAdmin, Admin")] in both client web app and API app.

User.IsInRole("Admin")

also I have access to claims

HttpContext.User.Claims 

var q = (from p in HttpContext.User.Claims where p.Type == "role" select p.Value).ToList();

var q2 = (from p in HttpContext.User.Claims where p.Type == "sub" select p.Value).First();
Nightlong answered 12/4, 2017 at 12:10 Comment(7)
Cheers mucker. I don't love many but you float my boat with that answer. Identity claim pain averted.Notarial
I'm adding a Claim in IdentityProfileService and it's in the token, do you know how to added only in a specific client and not in all clients?. This is the claim //Add more claims like this //claims.Add(new System.Security.Claims.Claim("Balance", user.Balance.ToString()));Matless
Hi William, Yes, if you don't like to add claims or a claim for a client, you can check the "context.Client.ClientId". I am using this to find my TenantId add a tenantId in JWS.Nightlong
@Mirak Don`t you have these code up on GitHub or a way to share it? I am trying to do roles in Identity Server 4 and I am a little bit lost so that would help me to follow your code easierProrate
@AlexandraDamaschin No, we have our own private TFS. I have changed this code ages ago because we needed to support multi-tenancy and we wanted to read Identity-Server settings from Database, also we migrated from core 1.0 to 2.0.1. this code was my first R&D about Identity-Server.Nightlong
Hey @Notarial The IdentityProfileService.cs class (third step) add all roles and claims to the JWT also you can add more claims. Don't forget to bind it on your startup.csNightlong
thanks to this line : var principal = await _claimsFactory.CreateAsync(user);Ferebee
W
11

I would like to provide my own answer after some rigorous research:

During the login process, the server will issue an authentication cookie with some of the claims of the user.

Then, the client will request an access token while providing the claims from the cookie, and the profile service will use the cookie claims to generate the access token claims.

Next, the client will request an id token, but this time it will use the claims from the access token.

Now the thing is, the default profile service of identity server populates the claims of the id token just by using the claims in the access token, while the default profile service of ASP.Net Identity, does look up all the user claims from the database store. This is a point of confusion.

For the identity server implementation, which claims end up in the access token? The claims associated with scopes which are API resources, as opposed to the claims in the id token, which are those associated with scopes which are identity resources.

Summary

Without ASP.NET Identity:

  1. Login - identity server issues a cookie with some claims
  2. Access token query - identity server adds claims from the cookie based on requested api scopes
  3. Id token query - identity server adds claims from the access token based on requested identity scopes

With ASP.NET Identity:

  1. Login - identity server issues a cookie with some claims
  2. Access token query - identity server adds claims from the cookie based on requested api scopes
  3. Id token query - identity server adds claims from the access token and the claims in the user store based on requested identity scopes
Who answered 10/9, 2020 at 5:48 Comment(0)
F
8

You should check context.RequestedClaimTypes and filter out claims, that were not requested.

Ferric answered 29/12, 2016 at 22:23 Comment(2)
You are right, I should do that, but this is not my problem because if i didn't request the langId scope in the request url above. The access token returned does't contain neither langId nor tenantId (so filtering here is not the problem). And that's what i need to understand, why when requesting the langId as in the question, the tenantId was included in the access token.Orator
That is because other resource scopes don't have any claims assigned to them, so when you remove langId scope this github.com/IdentityServer/IdentityServer4/blob/dev/src/… condition is not firing.Ferric

© 2022 - 2024 — McMap. All rights reserved.