Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Identify roles with SPA and .NET Core 3

I have an application using .NET Core 3.1 and also a frontend using the default React application, generated from this link.

In the .NET Core app, I have Identity Server setup with users and roles.

When I'm in the React app, I would like to know the roles from the user. I see that there's currently a library being used called oidc-client.

From the responses I can debug when authorizing the user, I see that there are some scopes being returned.

scope: "openid profile [Name of the app]"

Here's the full response.

enter image description here

How can I know the roles from that user? Do I need to add it somewhere in my .NET Core app? Or can I figure it from the access_token in the response?

like image 602
Felipe Peña Avatar asked Jan 26 '23 03:01

Felipe Peña


2 Answers

That template is using ASP.NET Core Identity to manage users/roles . So that the first thing is to enable roles :

services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();

Crating custom Profile service to include custom claims into tokens & userinfo endpoint :

public class ProfileService : IProfileService
{
    protected readonly UserManager<ApplicationUser> _userManager;


    public ProfileService(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        ApplicationUser user = await _userManager.GetUserAsync(context.Subject);

        IList<string> roles = await _userManager.GetRolesAsync(user);

        IList<Claim> roleClaims = new List<Claim>();
        foreach (string role in roles)
        {
            roleClaims.Add(new Claim(JwtClaimTypes.Role, role));
        }

        //add user claims

        roleClaims.Add(new Claim(JwtClaimTypes.Name, user.UserName));
        context.IssuedClaims.AddRange(roleClaims);
    }

    public Task IsActiveAsync(IsActiveContext context)
    {
        return Task.CompletedTask;
    }
}

And register in Startup.cs :

services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
        .AddProfileService<ProfileService>(); 

Now the claims will include in userinfo endpoint , your react application will automatically request the userinfo endpoint to get user's profile in getUser function of AuthorizeService.js file , trace the _user.profile to get the new claims . Also , the role claims are included in access token .

like image 101
Nan Yu Avatar answered Jan 27 '23 17:01

Nan Yu


You don't have to implement a ProfileService. The ReactJS+ID4 template already sets up a client (Client[0]) for the front end, you just have to add the appropriate configurations to have it place the role into the token.

        services
            .AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddRoles<IdentityRole>() //<- Very important, don't forget
            .AddEntityFrameworkStores<AuthDbContext>();

        services.AddIdentityServer()
            .AddApiAuthorization<ApplicationUser, AuthDbContext>(x =>
            {
                x.IdentityResources.Add(new IdentityResource("roles", "Roles", new[] { JwtClaimTypes.Role, ClaimTypes.Role }));
                foreach(var c in x.Clients)
                {
                    c.AllowedScopes.Add("roles");
                }
                foreach (var a in x.ApiResources)
                {
                    a.UserClaims.Add(JwtClaimTypes.Role);
                }
            });

On the client side, be careful using the role. It can either be a string or an array of strings based on how many roles the user is assigned to. I use the ensureArray function to help with this.

  isAdmin(user: User|null): boolean {
    return this.isInAnyRole(user, ["Admin"]);
  }

  isInAnyRole(user: User|null, requiredAnyRoles: string[]): boolean {
    var authorized = false;
    if (user) {
      var userRoles = this.ensureArray(user.profile.role);
      requiredAnyRoles.forEach(role => {
        if (userRoles.indexOf(role) > -1) {
          authorized = true;
        }
      });
    }
    return authorized;
  }

  private ensureArray(value: any): string[] {
    if (!Array.isArray(value)) {
      return [<string>value];
    }
    return value;
  }

Then you can add a policy on your server side.

services.AddAuthorization(options =>
{
     options.AddPolicy("RequireAdminRole", policy =>
     {
          policy.RequireClaim(ClaimTypes.Role, "Admin");
     });
});

Protect your api

[Authorize(Policy = "RequireAdminRole")]
[HttpPost()]
public async Task<IActionResult> Post([FromBody] CreateModel model)
like image 21
Sike Mullivan Avatar answered Jan 27 '23 18:01

Sike Mullivan