I managed to sort this out; so basically I already had roles set up. I just didn't understand how I had done it in the past. The main thing to do was set up the ApiResource which I did like this:
new ApiResource(IdentityConstants.ApiResources.IdentityServer, "Identity Server", new[] {JwtClaimTypes.Role, IdentityConstants.ClaimTypes.Permission})
My constants class just has a few static strings set up like this:
public static class SituIdentityConstants { public static class ApiResources { public const string Sxp = "sxp"; public const string IdentityServer = "identity-server"; } public static class ClaimTypes { public const string Permission = "permission"; } }
Once that was done; I updated my seed and included this:
private static void CreateApiResources(ConfigurationDbContext context) { var scopes = ListApiScopes(); var resources = ListApiResources(); foreach (var scope in scopes) if (!context.ApiScopes.Any(m => m.Name.Equals(scope.Name))) context.ApiScopes.Add(scope.ToEntity()); foreach (var resource in resources) { if (context.ApiResources.Any(m => m.Name.Equals(resource.Name))) continue; var entity = resource.ToEntity(); entity.Scopes = scopes.Select(m => new ApiResourceScope { Scope = m.Name }).ToList(); context.Add(entity); } context.SaveChanges(); }
Which created all the API Resources, scopes and any ApiResourceClaims (which is what populates RequestedClaimTypes in the IProfileService.
Btw, for reference, here is my ProfileService:
public class ProfileService: IProfileService { private readonly IMediator _mediator; private readonly UserManager<User> _userManager; private readonly RoleManager<Role> _roleManager; public ProfileService(IMediator mediator, UserManager<User> userManager, RoleManager<Role> roleManager) { _mediator = mediator; _userManager = userManager; _roleManager = roleManager; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var user = await _userManager.FindByIdAsync(context.Subject.GetSubjectId()); var rolesAttempt = await _mediator.Send(new ListRoles()); if (rolesAttempt.Failure) return; var roles = rolesAttempt.Result; var issuedClaims = new List<System.Security.Claims.Claim>(); foreach (var role in roles) { if (!user.Roles.Any(m => m.RoleId.Equals(role.Id))) continue; issuedClaims.Add(new System.Security.Claims.Claim(JwtClaimTypes.Role, role.Name)); var roleClaims = await _roleManager.GetClaimsAsync(new Role {Id = role.Id}); issuedClaims.AddRange(roleClaims.Where(m => context.RequestedClaimTypes.Any(x => x.Equals(m.Type)))); } context.IssuedClaims = issuedClaims; } public async Task IsActiveAsync(IsActiveContext context) { var sub = context.Subject.GetSubjectId(); var user = await _userManager.FindByIdAsync(sub); var active = (user != null && (!user.LockoutEnabled || user.LockoutEnd == null)) || (user != null && user.LockoutEnabled && user.LockoutEnd != null && DateTime.UtcNow > user.LockoutEnd); context.IsActive = active; } }
I actually found this to be a pain to set up. I tried the way the documentation states:
services.AddIdentityServer() .AddDeveloperSigningCredential() // Stores clients and resources .AddConfigurationStore(options => options.ConfigureDbContext = ConfigureDbContext) // Stores tokens, consents, codes, etc .AddOperationalStore(options => options.ConfigureDbContext = ConfigureDbContext) .AddProfileService<ProfileService>() .AddAspNetIdentity<User>();
But this would not work for me; so instead I did it like this:
services.AddIdentityServer() .AddDeveloperSigningCredential() // Stores clients and resources .AddConfigurationStore(options => options.ConfigureDbContext = ConfigureDbContext) // Stores tokens, consents, codes, etc .AddOperationalStore(options => options.ConfigureDbContext = ConfigureDbContext) .AddAspNetIdentity<User>(); services.AddScoped(typeof(IProfileService), typeof(ProfileService));
That's all I needed to do for Identity Server, the RequestedClaimTypes is now populated both with "role" and "permission". The only other thing I did, was when I created a RoleClaim, I set the claim type to "permission" and it works:
{ "nbf": 1611600995, "exp": 1611604595, "iss": "https://localhost:44362", "aud": [ "sxp", "identity-server" ], "client_id": "client", "sub": "949cc454-d7c9-45db-9eae-59e72d3025c1", "auth_time": 1611600983, "idp": "local", "role": "User Manager", "permission": [ "users:write", "user:read" ], "jti": "39C9F31958972704730DA65A8FCDAAEE", "iat": 1611600995, "scope": [ "identity:read", "identity:write", "sxp:read", "sxp:write" ], "amr": [ "pwd" ] }
Noice.
ClaimsIdentity, i.e. user. That's why theTypeof claim was introduced: to separate the claims with different purpose, for instance the claims of type Scope are for setting the access for the client app, while type Role can be used for setting up access for the user. The next question is how do you setup your data. Probably you use ASP.Net Identity and your roles go from there.