9
votes

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 [email protected] and one with the username [email protected].

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("[email protected]");
    await UserManager.AddToRoleAsync(userToMakeAdmin, "Admin");

    ApplicationUser userToMakeSubscriber = await UserManager.FindByNameAsync("[email protected]");
    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 [email protected] 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.

  1. Created a new ProfileService class in my IdentityServer project with the following content.
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?

2
Search for this exact question, I answered it already here on SO, just can’t check now on mobile.Vidmantas Blazevicius
Thanks for your help, I have indeed seen your answer and tried to implement your solution. However the second if statement still returns False and the role is not part of the claims. I have edited my post with the code I added. If you can explain me what I'm doing wrong it will be very helpful. Thanks in advance!Mitch
Looks good so far, I will take a wild guess and say that it's case sensitive maybe, so try IsInRole("Admin"). Also inspect the raw token using something like jwt.io and check if the role claim is now added correctly to the token. Also you haven't implemented your ProfileService as per my answer.Vidmantas Blazevicius
@Mitch, can't see options.ResponseType = "id_token token"; in your client side config, so you request only IdentityResiource information and not ApiResource. nevettheless after your edit you should be able to get your roles with the id_token, but you have to request that explicitly: options.Scope.Add("roles");d_f
Unfortunately neither of them is the case. No matter if I change isInRole("ADMIN") to isInRole("Admin") ot to isInRole("admin"), they all return False. The decoded JWT token also does not contain a role key. However if I add options.Scope.Add("roles"); to the client side it returns the role attribute and the isInRole("Admin") return true! Thanks for your suggestions and help! It works like it should now :)Mitch

2 Answers

4
votes

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.

5
votes

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"
            };**
        });