Hello,
I have a persisted data store which maps roles to Windows users within my company's domain. These roles correspond to the access rights these users have within an MVC app. I am expecting that these roles will be available in User.Claims within the context of MVC pages so I can then evaluate whether the current user has access to the current page.
After much trial and error, I am beginning to think my understanding of how authentication/authorization _should_ work in my IdentityServer implementation is flawed. I will walk through my implementation and am requesting any feedback as to why this is not working as expected (please tell me if I am thinking about the approach incorrectly).
Identity Server.Startup.ConfigureServices

Here, I am adding IdentityServer, the developer signing cert, a custom user store implementation (see below), and resources/clients from a config (also below). Furthermore, since I am targeting Windows authentication, I have configured IIS options per IdentityServer documentation.
AddUserStore() and UserProfileService
In the above chain, I am adding a custom user store and IProfileService implementation. Roles are retrieved from the user store and added to IssuedClaims on the ProfileDataRequestContext object.


Resource configuration
In my configuration file, I am defining the following resources and client. I am adding a resource that will contain user roles and permissions (RoleKey == "roles"). In this resource, I am adding a single claim type - JwtClaimTypes.Role ("role").
I would expect that roles should be placed in User.Claims within the context of an MVC page. Is this correct?


As you can see below, I am adding the RoleKey ("roles") scope to options.Scope. It is important to note that I am also setting GetClaimsFromUserEndpoint to 'true'.



Breakpoint hit on "OnTokenValidated". I can validate that claims exist in the context token, but the role claims do not. This IS expected as this token is the ID token and NOT the access token. Correct?
Breakpoint hit on "OnUserInformationReceived". Roles WERE retrieved from the UserInfo endpoint. This is good!


Role claims are not there! Here is the code:

You have too many questions in this issue, and this issue tracker is not meant to be a general purpose help forum. I don't quite know where to start, but perhaps you should watch this for more high level info on how all this is supposed to work: https://mva.microsoft.com/en-US/training-courses/introduction-to-identityserver-for-aspnet-core-17945
@brockallen I have watched that. All of my questions are driving the primary question: Why are the roles not being injected into the User.Claims object?
@brockallen I have watched that. All of my questions are driving the primary question: Why are the roles not being injected into the User.Claims object?
Unfortunately I don't know and this is the wrong channel for those questions. Sorry. I'd suggest stackoverflow, or our commercial support offerings.
Take a look here: https://github.com/aspnet/Security/issues/1449 - You probable need to map the claims yourself.
I've tested role mapping with following ClaimAction modified to working with array: (https://github.com/tstojecki/Security/blob/ca24b79b2cc2c50bc2d09407204a253b22389b88/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs)
.AddOpenIdConnect("oidc", options =>
{
...
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("roles");
options.ClaimActions.Add(new JsonKeyClaimAction("role", "role", "role"));
});
Works fine.
@Jenan
Thanks so much. I am now able to access my role claims in User.Claims.
@brockallen / @leastprivilege - This recent change has caused this unexpected behavior in IdentityServer4. Perhaps this should be documented in the quick starts?
Anyway, I believe the change also leaves claims mapped with options.ClaimActions as unprotected. For example, consider the scenario I have just tested:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
// !!! REMOVING ROLES RESOURCE !!!
// new IdentityResource(RolesResourceName, "Your roles", new [] { JwtClaimTypes.Role })
};
}
return new List<Client>
{
new Client
{
ClientId = ModuleType.Administration.ToString(),
ClientName = "Administration Module Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets = { new Secret(IdentitySecret.Sha256()) },
// where to redirect to after login
RedirectUris = { "https://localhost:44310/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:44310/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
SystemApiName,
// !!! REMOVING ALLOWED SCOPE FOR ROLES RESOURCE !!!
// RolesResourceName
},
AllowOfflineAccess = true
}
};
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = IdentityConfiguration.AuthorityUrl;
options.RequireHttpsMetadata = true;
options.ClientId = ModuleType.Administration.ToString();
options.ClientSecret = IdentityConfiguration.IdentitySecret;
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add(IdentityConfiguration.SystemApiName);
options.Scope.Add("offline_access");
// !!! REMOVING ROLES SCOPE !!!
// options.Scope.Add(IdentityConfiguration.RolesResourceName);
// !!! KEEP CLAIM ACTION MAPPING !!!
options.ClaimActions.Add(new JsonKeyArrayClaimAction(JwtClaimTypes.Role, JwtClaimTypes.Role, JwtClaimTypes.Role));
});
With these changes, the claims are still populated in User.Claims. Shouldn't they be excluded as the "role" claim no longer belongs to any protected resources? Is this an issue?
I've tested your scenario as you mentioned and I don't have a role claim in my user claims in MVC. It was returned only what I am able to get. It seems work fine.
@jenan Are you able to share your implementation? On the consent screen, if I uncheck "Profile", even profile claims still appear in User.Claims. Perhaps it is a configuration issue on my end.
@nicbavetta I have used the Quickstart 5 - here is repo: https://github.com/Jenan/IdentityServer4.ClaimActions
Yea, I just commented on a similar issue here: https://github.com/IdentityServer/IdentityServer4/issues/1657#issuecomment-343610578
All set on this issue -- can we close?
@brockallen Yes, we are all good. Closing now.
Thx a lot, It's working for me :)
I had found IMO a better solution here https://github.com/aspnet/Security/issues/1383 using OnUserInformationReceived event
private static Task SetUserInformationReceived(UserInformationReceivedContext context)
{
if (context.User.TryGetValue(JwtClaimTypes.Role, value: out var roles)) // (@) IdentityServer returns multiple claim values as JSON arrays, which break the authentication handler https://github.com/aspnet/Security/issues/1383
{
var claims = new List<Claim>();
if (roles.Type != JTokenType.Array)
{
claims.Add(new Claim(JwtClaimTypes.Role, (string) roles));
}
else
{
claims.AddRange(roles.Select(role => new Claim(JwtClaimTypes.Role, (string) role)));
}
var id = context.Principal.Identity as ClaimsIdentity;
id?.AddClaims(claims);
}
return Task.CompletedTask;
}
@ahedreville
Perfect.. Short and sweet..
Thanks..
JsonKeyClaimAction is in the Microsoft.AspNetCore.Authentication.OAuth v2.1.1 out of the box
Usage: options.ClaimActions.Add(new JsonKeyClaimAction(JwtClaimTypes.Role, null, JwtClaimTypes.Role));
I just can't understand why they pass "role" value to the valueType parameter. Like here: https://github.com/aspnet/Security/issues/1449#issuecomment-332767846
I expected something like "String".
Actually I have dig to the sources and this value is used for the Claim object creation.
/// <param name="valueType">The claim value type. If this parameter is null, then <see cref="F:System.Security.Claims.ClaimValueTypes.String"></see> is used.</param>
So it should be rather null than "role"
And "role" value passed to the valueType parameter obviously leads to the following:

Thanks a lot for this guys options.ClaimActions.Add(new JsonKeyClaimAction(JwtClaimTypes.Role, null, JwtClaimTypes.Role)); this is the simple solution i have been looking for.
Editing my comment to answer my question, this comment cleared up a TON of my beginner questions on ClaimActions https://github.com/aspnet/Security/issues/1449#issuecomment-332359065
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
Take a look here: https://github.com/aspnet/Security/issues/1449 - You probable need to map the claims yourself.
I've tested role mapping with following
ClaimActionmodified to working with array: (https://github.com/tstojecki/Security/blob/ca24b79b2cc2c50bc2d09407204a253b22389b88/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs)Works fine.