Search code examples
c#asp.net-mvcasp.net-coreauthorizationasp.net-identity

Role Claim won't be added when adding Role to User [JwtAuthentication]


I'm trying to add role-based authorization to my aspnetcore backend. For example I want a "Moderator" role that has access to different endpoints. For this i tried to use a bit of this tutorial : AspnetCore Identity Roles. The problem I have now is that when I add the a User to a role and use the [Authorize(Role="Moderator] attribute it wont work and I always get a 403.

Example Test Case:

[Fact]
public async Task It_should_return_404_for_non_existing_movies()
{
    using var scope = _factory.Services.CreateScope();
    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
    var arrangeContext = scope.ServiceProvider.GetRequiredService<MovieDBContext>();

    await userManager.CreateAsync(
    new IdentityUser { UserName = "Mod", Email = "[email protected]" }, "mod12345");

    var mod = await userManager.FindByNameAsync("Mod");
    await userManager.AddToRoleAsync(mod!, "Moderator");

    var token = await _client.LoginAsync("[email protected]", "mod12345");

    _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
    var response = await _client.DeleteAsync($"/movies/1337");
    response.StatusCode.Should().Be(HttpStatusCode.NotFound); // This fails because User.Claims does not contain role moderator
}

Endpoint im calling in the test (Still WIP):

[HttpDelete("{id}"), Authorize(Roles = "Moderator")]
public IActionResult DeleteMovie(int id)
{
    var movie = _context.Movies.Find(id);
    if (movie == null)
    {
        return NotFound();
    }

    _context.Movies.Remove(movie);
    _context.SaveChanges();
    return Ok();
}

Startup.cs where I add the aspnetcore identity and auth stuff:

services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(config =>
    {
        config.TokenValidationParameters = new TokenValidationParameters
        {
            ClockSkew = TimeSpan.Zero,
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = Configuration.GetValue<string>("JwtSettings:Issuer"),
            ValidAudience = Configuration.GetValue<string>("JwtSettings:Audience"),
            IssuerSigningKey =
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetValue<string>("JwtSettings:Key")!)),
        };
    });

services.AddAuthorization();


services
    .AddIdentityCore<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
        options.User.RequireUniqueEmail = true;
        options.Password.RequireDigit = false;
        options.Password.RequiredLength = 6;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = false;
        options.Password.RequireLowercase = false;
     })
     .AddRoles<IdentityRole>()
     .AddEntityFrameworkStores<UserContext>();

UserContext.cs:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace MovieDatabase.Identity;
public class UserContext : IdentityDbContext<IdentityUser, IdentityRole, string>
{
    public UserContext(DbContextOptions<UserContext> options)
        : base(options)
    {
    }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<IdentityRole>()
            .HasData(
                new[] {
                    new IdentityRole { Name = "Admin", NormalizedName = "ADMIN" },
                    new IdentityRole { Name = "Moderator", NormalizedName = "MODERATOR" },
                    new IdentityRole { Name = "User", NormalizedName = "USER" } }
            );

        base.OnModelCreating(modelBuilder);
    }
}

Here i am also adding the roles that i want to support for now using a migration. If im checking the claims of a user that is created like im doing in my test this is the result:

[
  {
    "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
    "value": "TokenForTheApiWithAuth"
  },
  {
    "type": "jti",
    "value": "2b9fb336-50b2-4569-845d-6c596eed39a2"
  },
  {
    "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
    "value": "080291d2-03b3-40dd-ac39-1793ba7e7b0d"
  },
  {
    "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
    "value": "test"
  },
  {
    "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
    "value": "[email protected]"
  },
  {
    "type": "exp",
    "value": "1697668413"
  },
  {
    "type": "iss",
    "value": "http://localhost:5278"
  },
  {
    "type": "aud",
    "value": "http://localhost:5253"
  }
]

AspNetUser Table: enter image description here

AspNetRoles Table: enter image description here

AspNetUserRoles Table:

enter image description here

As seen above my user is connected in to the role in the database and if i use the UserManager to ask if the User got the "Moderator" role it will return true, but the Role Claim is still missing (s. JSON above). Im kind of stuck on this problem for a few days now maybe some sees what im missing.

I tried lots of different things for example: Most of the Solutions found here Github Issue after i found this stack overflow question: Question.

Also tried the solution in this question like adding .AddRoleManager(RoleManager<IdentityRole) in the statup.cs or adding services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, UserClaimsPrincipalFactory<IdentityUser, IdentityRole>>();

But nothing seems to add the Role Claim to the User when adding the user to the role.

Github Repo with all the code: https://github.com/alexanderluettig/movie-database/tree/main


Solution

  • So after the comment from @Rena that when using JwtAuthentication that the Role Claims won't be added automatically, when creating the jwt Token. I added them manually when creating the Jwt Token inside my Tokenservice.cs

    The lines i added were:

    var roles = _userManager.GetRolesAsync(user).Result;
    claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));