0

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 :)

4
  • Does this answer your question? How to include claims only in id_token but not in access_token IdentityServer4 Commented Apr 11, 2020 at 19:30
  • @RuardvanElburg not really. I saw that thread before asking the question. An ideal solution would be to call the endpoint more only once, not modify the response based on caller. I was expecting some sort of middleware, or event where those claims would be grabbed from access_token and put to id token, or any other leads on how are others doing this Commented Apr 11, 2020 at 20:07
  • That's not how it works. IdentityServer issues tokens and for each token the method is called with a different context. You can configure the client to only request an acces token, but you can't change this token into an identity token. If you want both, you'll need two calls. Commented Apr 11, 2020 at 20:17
  • If that's the case it seems like I'll keep it like this, as I need both of them. Commented Apr 11, 2020 at 20:23

4 Answers 4

4

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 :)

Sign up to request clarification or add additional context in comments.

Comments

1

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.

Comments

0

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.

1 Comment

thanks, this is the way I'm accessing the protected resource from mvc, and it works, but I'm trying to get this claim from context user, claims principal like User.Identity.Claims in mvc
0

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.

1 Comment

yeah you are right about the url length restrictions, I'll just keep it as is, better get them explicitly then fail somewhere :P thanks anyway :)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.