Why is my Authorize failing when I specify a role which the user has, with JWT token

49 Views Asked by At

I have implemented a simple JWT authentication system in my code. When using the [Authorize] attribute and then analysing the user claim inside the controller code, I can see that the user has the role "Admin" However, if I change the Authorize to [Authorize(Roles = "Admin")] I get a 403 returned. Note that I'm testing these methods through Postman.

Token generation:

[AllowAnonymous]
[HttpPost]
[Route("Login")]
//[HttpPost("Login/{username}/{password}")]
public IActionResult Login(string username, string password)
{
    var authRepo = new AuthenticationRepository();
    if (authRepo.Login(username, password))
    {
        var ua = new UserAttributes { Email = username, Role = "Admin" };
        var token = GenerateToken(ua);
        return Ok(token);
    }

    return NotFound("User not found");
}

private string GenerateToken(UserAttributes userAttributes)
{
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
    var claims = new[]
    {
            new Claim(ClaimTypes.Name, userAttributes.Email),
            new Claim(ClaimTypes.Role, userAttributes.Role)
        };
    var token = new JwtSecurityToken(_config["Jwt:Issuer"],
        _config["Jwt:Audience"],
        claims,
        expires: DateTime.Now.AddMinutes(Convert.ToDouble(_config["Jwt:Expires"])),
        signingCredentials: credentials);


    return new JwtSecurityTokenHandler().WriteToken(token);
}

Authorized code:

[HttpGet]
    [Route("TestRole")]
    [Authorize]
    public IActionResult AdminEndPoint()
    {
        var currentUser = GetCurrentUser();
        if (currentUser != null)
            return Ok($"Hi {currentUser.Email} you are an {currentUser.Role}");
        else
            return NotFound();
    }

    private UserAttributes? GetCurrentUser()
    {
        var identity = HttpContext.User.Identity as ClaimsIdentity;
        if (identity != null)
        {
            var userClaims = identity.Claims;
            return new UserAttributes
            {
                Email = userClaims.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value,
                Role = userClaims.FirstOrDefault(x => x.Type == ClaimTypes.Role)?.Value
            };
        }
        return null;
    }

Note that this works, but when I change to [Authorize(Roles = 'Admin')] it fails.

For completeness, this is my startup:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => {
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = false,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        RoleClaimType = "role",
        NameClaimType = "name",
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
    };
    options.Events = new JwtBearerEvents()
    {
        OnMessageReceived = msg =>
        {
            var token = msg?.Request.Headers.Authorization.ToString();
            string path = msg?.Request.Path ?? "";
            if (!string.IsNullOrEmpty(token))

            {
                Console.WriteLine("Access token");
                Console.WriteLine($"URL: {path}");
                Console.WriteLine($"Token: {token}\r\n");
            }
            else
            {
                Console.WriteLine("Access token");
                Console.WriteLine("URL: " + path);
                Console.WriteLine("Token: No access token provided\r\n");
            }
            return Task.CompletedTask;
        },
        OnTokenValidated = ctx =>
        {
            Console.WriteLine();
            Console.WriteLine("Claims from the access token");
            if (ctx?.Principal != null)
            {
                foreach (var claim in ctx.Principal.Claims)
                {
                    Console.WriteLine($"{claim.Type} - {claim.Value}");
                }
            }
            Console.WriteLine();
            return Task.CompletedTask;
        },
        OnAuthenticationFailed = aut =>
        {
            Console.WriteLine("Exceptoin during authentication");
            Console.WriteLine(aut.Exception);
            Console.WriteLine("");
            return Task.CompletedTask;
        }
    };
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();

app.UseAuthorization();

app.MapControllers();

app.Run();
1

There are 1 best solutions below

12
Tore Nestenius On

Microsoft and OpenID Connect have different opinion on what the name/role claims should be and because of that ,you need to tell AddJwtBearer what the name of the role and name claim is, using:

.AddJwtBearer(opt =>
{
    // ...
    opt.TokenValidationParameters.RoleClaimType = "role";
    opt.TokenValidationParameters.NameClaimType = "name";
    // ...
});

For more details, I wrote a blog post about it:

Debugging JwtBearer Claim Problems in ASP.NET Core