简体   繁体   中英

How to access custom claim in aspnet core application authorized using Identity Server

I'm following Identity Server quickstart template, and trying to setup the following

  • Identity server aspnet core app
  • Mvc client, that authenticates to is4 and also calls webapi client which is a protected api resource.

The ApplicationUser has an extra column which I add into claims from ProfileService like this:

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            if (user == null)
                return;

            var principal = await _claimsFactory.CreateAsync(user);
            if (principal == null)
                return;

            var claims = principal.Claims.ToList();

            claims.Add(new Claim(type: "clientidentifier", user.ClientId ?? string.Empty));

            // ... add roles and so on

            context.IssuedClaims = claims;
        }

And finally here's the configuration in Mvc Client app ConfigureServices method:

            JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            }).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;

                options.ClientId = "mvc";
                options.ClientSecret = "mvc-secret";
                options.ResponseType = "code";

                options.SaveTokens = true;

                options.Scope.Add("openid");
                options.Scope.Add("profile");
                options.Scope.Add("offline_access");

                options.Scope.Add("api1");

                options.GetClaimsFromUserInfoEndpoint = true;

                options.ClaimActions.MapUniqueJsonKey("clientidentifier", "clientidentifier");
            });

With GetClaimsFromUserInfoEndpoint set to true I can access the custom claim in User.Identity , but this results in 2 calls for ProfileService .

If I remove or set to false then this claim is still part of access_token, but not part of id_token, and then I can't access this specific claim from context User.

Is there a better way I can access this claim from User principal without resulting in 2 calls (as it's now)? or perhaps reading access_token from context and updating user claims once the token is retrieved?

thanks:)

Turns out that Client object in identity server has this property that does the job:

        //
        // Summary:
        //     When requesting both an id token and access token, should the user claims always
        //     be added to the id token instead of requring the client to use the userinfo endpoint.
        //     Defaults to false.
        public bool AlwaysIncludeUserClaimsInIdToken { get; set; }

As explained in the lib metadata setting this to true for a client, then it's not necessary for the client to go and re-get the claims from endpoint

thanks everybody:)

I am assuming you are passing Authorization header with Bearer JWT token while calling the API. You can read access_token from HttpContext in your API Controller.

  var accessToken = await this.HttpContext.GetTokenAsync("access_token");
    var handler = new JwtSecurityTokenHandler();

    if (handler.ReadToken(accessToken) is JwtSecurityToken jt && (jsonToken.Claims.FirstOrDefault(claim => claim.Type == "sub") != null))
    {
        var subID = jt.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
    }

NOTE: GetClaimsFromUserInfoEndpoint no need to set explicitly.

Here is a bit of extra info on the subject. By default, IdentityServer doesn't include identity claims in the identity token. It is allowed by setting the AlwaysIncludeUserClaimsInIdToken setting on the client configuration to true. But it is not recommended. The initial identity token is returned from the authorization endpoint via front‑channel communication either through a form post or through the URI. If it's returned via the URI and the token becomes too big, you might hit URI length restrictions, which are still dependent on the browser. Most modern browsers don't have issues with long URIs, but older browsers like Internet Explorer might. This may or may not be of concern to you. Looks like my project is similar to yours. Good luck.

If you want to access custom claims in client side over those added in identity server just follow these steps, it worked for me. I imagine you implement both client and identity server as separated projects in asp.net core and they are ready, you now want to play with claims or maybe want to authorize by role-claim and so on, alright let's go

  1. create a class that inherits from "IClaimsTransformation" like this:
public class MyClaimsTransformation : IClaimsTransformation
    {
        public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
        {
            var userName = principal.Identity.Name;
            var clone = principal.Clone();
            var newIdentity = (ClaimsIdentity)clone.Identity;
            var user = config.GetTestUsers().Where(p => p.Username == userName).First();
            if (user != null)
            {
                var lstUserClaims = user.Claims.Where(p => p.Type == JwtClaimTypes.Role).ToList();
                foreach (var item in lstUserClaims)
                    if (!newIdentity.Claims.Where(p => p.ValueType == item.ValueType && p.Value == item.Value).Select(p => true).FirstOrDefault())
                        newIdentity.AddClaim(item);
            }
            return Task.FromResult(principal);
        }
    }

But be aware this class will call multiple times over user authentication so i added a simple code to prevent multiple duplicate claim. also you have user name of authenticated user too.

  1. Next create another class like this:
public class ProfileService : IProfileService
    {
        //private readonly UserManager<ApplicationUser> userManager;

        public ProfileService(/*UserManager<ApplicationUser> userManager*/ /*, SignInManager<ApplicationUser> signInManager*/)
        {
            //this.userManager = userManager;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            context.AddRequestedClaims(context.Subject.Claims);

            var collection = context.Subject.Claims.Where(p => p.Type == JwtClaimTypes.Role).ToList();
            foreach (var item in collection)
            {
                var lst = context.IssuedClaims.Where(p => p.Value == item.Value).ToList();
                if (lst.Count == 0)
                    context.IssuedClaims.Add(item);
            }

            await Task.CompletedTask;
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            //context.IsActive = true;
            await Task.FromResult(0); /*Task.CompletedTask;*/
        }
    }

This class will call by several context but it's okay cause we added our custom claim(s) at part #1 at this code

foreach (var item in lstUserClaims)
   if (!newIdentity.Claims.Where(p => p.ValueType == item.ValueType && p.Value == item.Value).Select(p => true).FirstOrDefault())
        newIdentity.AddClaim(item);
  1. This is your basic startup.cs at identity server side:
public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(options => options.EnableEndpointRouting = false);
            services.AddTransient<Microsoft.AspNetCore.Authentication.IClaimsTransformation, MyClaimsTransformation>();
            services.AddIdentityServer().AddDeveloperSigningCredential()
                    .AddInMemoryApiResources(config.GetApiResources())
                    .AddInMemoryIdentityResources(config.GetIdentityResources())
                    .AddInMemoryClients(config.GetClients())
                    .AddTestUsers(config.GetTestUsers())
                    .AddInMemoryApiScopes(config.GetApiScope())
                    .AddProfileService<ProfileService>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseIdentityServer();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();

        }
    }

Pay attention to .AddProfileService<ProfileService>(); and services.AddTransient<Microsoft.AspNetCore.Authentication.IClaimsTransformation, MyClaimsTransformation>();

  1. Now at client side go to startup.cs and do as follows:
.AddOpenIdConnect("oidc", options =>
{
   //other code
   options.GetClaimsFromUserInfoEndpoint = true;
   options.ClaimActions.Add(new JsonKeyClaimAction(JwtClaimTypes.Role, null, JwtClaimTypes.Role));
})

for my sample i tried to use "Role" and authorize users by my custom roles.

  1. Next at your controller class do like this: [Authorize(Roles = "myCustomClaimValue")] or you can create a class for custom authorization filter.

Note that you define test user in config file in your identity server project and the user has a custom claim like this new claim(JwtClaimTypes.Role, "myCustomClaimValue") and this will be back at lstUserClaims variable.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM