0
votes

I created a react project with the default react template in asp.net core with individual user accounts. [Authorize] attribute works fine however when I try to implement [Authorize(Roles = "Administrator")] I get a 403 status code.

I've added ProfileService to add claims.

ProfileService.cs

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

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

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

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

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

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

"role": "Administrator" exists in my JWT token.

I have the Authorize attribute added to my controller.

    [Authorize(Roles = "Administrator")]
    [HttpGet]
    public async Task<ActionResult<IEnumerable<Order>>> GetOrders()
    {
        return await _context.Orders.ToListAsync();
    }

I have also configured my Startup.cs as below.

Startup.cs

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors();

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));

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

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

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

        services.AddAuthentication()
            .AddIdentityServerJwt();

        services.AddAuthorization();

        services.Configure<IdentityOptions>(options =>
        {
            options.ClaimsIdentity.RoleClaimType = JwtClaimTypes.Role;
        });

        services.AddTransient<IProfileService, ProfileService>();

        services.AddControllersWithViews();
        services.AddRazorPages();

        // In production, the React files will be served from this directory
        services.AddSpaStaticFiles(configuration =>
        {
            configuration.RootPath = "ClientApp/build";
        });
    }

Unsure where I'm going wrong with this.

2
I am guessing it is the claim mapping that acts up, you add roles as JwtClaimTypes.Role which is "role" in the profile service but the Authorize attribute is most likely using ClaimTypes.Role which is "schemas.microsoft.com/ws/2008/06/identity/claims/role" so perhaps you need to look into claim mappings? - Thomas Schmidt

2 Answers

0
votes

If you want to use the built-in Role authorization, you will have to change the way you assign the role claim from this:

 roleClaims.Add(new Claim(JwtClaimTypes.Role, role));

to this:

 roleClaims.Add(new Claim(ClaimTypes.Role, role));
0
votes

the better way will be to use policy-based authorization https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-5.0.

  1. Configure your policy in ConfigureServices:

         services.AddAuthorization(options =>
         {
             options.AddPolicy("RequireAdministratorRole",
                 policy => policy.RequireClaim(ClaimTypes.Role, "Administrator"));
         });
    
  2. In ProfileService

             var claims = roles.Select(role => new Claim(ClaimTypes.Role, role)).ToList();
    
  3. Add to the controller:

    [Authorize(Policy = "RequireAdministratorRole")]

It might not work because of the Microsoft claim name inconsistency can be fixed with:

            services.Configure<JwtBearerOptions>(options =>
        {
            var validator = options.SecurityTokenValidators.OfType<JwtSecurityTokenHandler>().SingleOrDefault();

            // Turn off Microsoft's JWT handler that maps claim types to .NET's long claim type names
            validator.InboundClaimTypeMap = new Dictionary<string, string>();
            validator.OutboundClaimTypeMap = new Dictionary<string, string>();
        });