Reputation: 765
I am new to IdentityServer and I have been struggling with this issue all day. So much so that I'm almost about to give up on this. I know this question has been asked over and over again and I have tried many different solutions but none seem to work. Hopefully you can help me push me in the right direction with this.
First I installed the IdentityServer4 templates by running dotnet new -i identityserver4.templates
and created a new project with the is4aspid template by running dotnet new is4aspid -o IdentityServer
.
After that i created a new IdentityServer database and ran the migrations. By that time I had a the default Identity database structure.
In Config.cs I changed MVC client
to the following:
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
ClientSecrets = { new Secret("47C2A9E1-6A76-3A19-F3C0-S37763QB36D9".Sha256()) },
RedirectUris = { "https://localhost:44307/signin-oidc" },
FrontChannelLogoutUri = "https://localhost:44307/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:44307/signout-callback-oidc" },
AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "api1", JwtClaimTypes.Role }
},
And changed the GetApis
method to this:
public static IEnumerable<ApiResource> GetApis()
{
return new ApiResource[]
{
new ApiResource("api1", "My API #1", new List<string>() { "role" })
};
}
There where of course no users in the database yet so i added a registration form and registered two dummy users, one with the username admin@example.com
and one with the username subscriber@example.com
.
To assign the roles to these user I created the following method in Startup.cs.
private async Task CreateUserRoles(IServiceProvider serviceProvider) {
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var UserManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
IdentityResult adminRoleResult;
IdentityResult subscriberRoleResult;
bool adminRoleExists = await RoleManager.RoleExistsAsync("Admin");
bool subscriberRoleExists = await RoleManager.RoleExistsAsync("Subscriber");
if (!adminRoleExists) {
adminRoleResult = await RoleManager.CreateAsync(new IdentityRole("Admin"));
}
if(!subscriberRoleExists) {
subscriberRoleResult = await RoleManager.CreateAsync(new IdentityRole("Subscriber"));
}
ApplicationUser userToMakeAdmin = await UserManager.FindByNameAsync("admin@example.com");
await UserManager.AddToRoleAsync(userToMakeAdmin, "Admin");
ApplicationUser userToMakeSubscriber = await UserManager.FindByNameAsync("subscriber@example.com");
await UserManager.AddToRoleAsync(userToMakeSubscriber, "Subscriber");
}
In the Configure
method of the same class I add the the parameter IServiceProvider services
and called the above method like so: CreateUserRoles(services).Wait();
. By this time my database did have two roles in it.
Next I created a new solution (within the same project) and in the Startup.cs file of that solution I added the following in the ConfigureServices
method.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options => {
options.SaveTokens = true;
options.ClientId = "mvc";
options.ClientSecret = "32D7A7W0-0ALN-2Q44-A1H4-A37990NN83BP";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:5000/";
options.ClaimActions.MapJsonKey("role", "role");
});
After that I added app.UseAuthentication();
in the Configure
method of the same class.
Then I created a new page with the following if statements.
if(User.Identity.IsAuthenticated) {
<div>Yes, user is authenticated</div>
}
if(User.IsInRole("ADMIN")) {
<div>Yes, user is admin</div>
}
I logged in with admin@example.com
but the second if statement returns False
. I inspected all the claims by looping over them like so.
@foreach (var claim in User.Claims) {
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
But there was no role claim to be found, only sid, sub, idp, preferred_username and name.
I tried to get the role in there so that the second if statement returns True but after trying and trying I have not yet been able to make it work. Can someone see what I have to do in order to make this work? I am an absolute beginner in IdentityServer4 and trying my best to understand it. Any help will be appreciated. Thanks in advance!
EDIT 1:
Thanks to this question and this question I got the feeling that I'm on the right track. I have made some modifications but I still can not get it to work. I just tried the following.
public class MyProfileService : IProfileService {
public MyProfileService() { }
public Task GetProfileDataAsync(ProfileDataRequestContext context) {
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
List<string> list = context.RequestedClaimTypes.ToList();
context.IssuedClaims.AddRange(roleClaims);
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context) {
return Task.CompletedTask;
}
}
Next I registered this class in the ConfigureServices method by adding the line services.AddTransient<IProfileService, MyProfileService>();
. After that I added a new a new line to the GetIdentityResources method, which looks like this now.
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource("roles", new[] { "role" })
};
}
I also added the roles to my Mvc client like so: AllowedScopes = { "openid", "profile", "api1", "roles" }
.
Next I switched over to the other project and added the following lines in the .AddOpenIdConnect oidc.
options.ClaimActions.MapJsonKey("role", "role", "role");
options.TokenValidationParameters.RoleClaimType = "role";
But still, I cannot get it to work like I want it to. Anyone knows what I am missing?
Upvotes: 14
Views: 22953
Reputation: 70184
I did it like this in .NET 5:
Add JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
before services.AddAuthentication
in Startup.cs
.
https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1349
I also added
services.AddScoped<IProfileService, ProfileService>();
and ProfileService.cs
that looks like this to map roles to claims:
public sealed class ProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<ApplicationUser> _userClaimsPrincipalFactory;
private readonly UserManager<ApplicationUser> _userMgr;
private readonly RoleManager<IdentityRole> _roleMgr;
public ProfileService(
UserManager<ApplicationUser> userMgr,
RoleManager<IdentityRole> roleMgr,
IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory)
{
_userMgr = userMgr;
_roleMgr = roleMgr;
_userClaimsPrincipalFactory = userClaimsPrincipalFactory;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
string sub = context.Subject.GetSubjectId();
ApplicationUser user = await _userMgr.FindByIdAsync(sub);
ClaimsPrincipal userClaims = await _userClaimsPrincipalFactory.CreateAsync(user);
List<Claim> claims = userClaims.Claims.ToList();
claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
if (_userMgr.SupportsUserRole)
{
IList<string> roles = await _userMgr.GetRolesAsync(user);
foreach (var roleName in roles)
{
claims.Add(new Claim(JwtClaimTypes.Role, roleName));
if (_roleMgr.SupportsRoleClaims)
{
IdentityRole role = await _roleMgr.FindByNameAsync(roleName);
if (role != null)
{
claims.AddRange(await _roleMgr.GetClaimsAsync(role));
}
}
}
}
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
string sub = context.Subject.GetSubjectId();
ApplicationUser user = await _userMgr.FindByIdAsync(sub);
context.IsActive = user != null;
}
}
Source:
https://ffimnsr.medium.com/adding-identity-roles-to-identity-server-4-in-net-core-3-1-d42b64ff6675
Upvotes: 3
Reputation: 4869
Slightly different question, absolutely matching answer.
With the Edit 1, IdP configuration looks enough to supply both identity and access tokens with roles when requested. The only thing left is to configure the client to request the access token (.Net client doesn't do that by default), or just request the roles
scope within the identity token.
To get the roles with id_token
, the client side config must include options.Scope.Add("roles");
To get the roles with bearer token, that token must be requested by specifying options.ResponseType = "id_token token";
in client side config.
Upvotes: 6
Reputation: 738
Two things you need to do to make sure you will get users roles in the claims:
1- In IdentityServer4 project: you need to have implementation for IProfileService http://docs.identityserver.io/en/latest/reference/profileservice.html
don't forget to add the class in startup.cs file like this
services.AddIdentityServer()
// I just removed some other configurations for clarity
**.AddProfileService<IdentityProfileService>();**
2- In Web Client project's startup.cs file: when configuring the openId, you have to mention this :
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "Identity URL ";
options.RequireHttpsMetadata = true;
options.ClientId = "saas_crm_webclient";
options.ClientSecret = "49C1A7E1-0C79-4A89-A3D6-A37998FB86B0";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = false;
options.Scope.Add("test.api");
options.Scope.Add("identity.api");
options.Scope.Add("offline_access");
**options.ClaimActions.Add(new JsonKeyClaimAction("role", null, "role"));**
**options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};**
});
Upvotes: 8