简体   繁体   中英

JWT Authentication, roles defined in Authorize attribute are ignored

While trying to implement role-based-authentication using JWT as default authentication scheme, I've encountered a situation where roles defined in the Authorize attribute are being ignored, allowing any request ( with a valid token ) to pass, even if not in those roles, (what interesting is that other policies with custom requirements defined in the very same Authorize attribute are working fine)

Reading jerrie's artical he mentions that

Here is a great find: The JWT middleware in ASP.NET Core knows how to interpret a “roles” claim inside your JWT payload, and will add the appropriate claims to the ClaimsIdentity . This makes using the [Authorize] attribute with Roles very easy.

And:

Where this gets really interesting is when you consider that passing Roles to the [Authorize] will actually look whether there is a claim of type http://schemas.microsoft.com/ws/2008/06/identity/claims/role with the value of the role(s) you are authorizing. This means that I can simply add [Authorize(Roles = "Admin")] to any API method, and that will ensure that only JWTs where the payload contains the claim “roles” containing the value of Admin in the array of roles will be authorized for that API method.

does that still hold true? (This article is several years old)
Am I doing anything wrong?

StartUp (ConfigureServices)

public void ConfigureServices(IServiceCollection services)
{
    string defaultConnection = Configuration.GetConnectionString("Default");

    services.AddDbContext<IdentityContext>(options => options.UseSqlServer(defaultConnection).UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll));

    services.AddIdentity<AppUser, IdentityRole>()
        .AddEntityFrameworkStores<IdentityContext>()
        .AddDefaultTokenProviders();

    services.AddAuthorization(o => o.AddPolicy(Policy.IsInTenant, x => x.AddRequirements(new IsInTenantRequirement())));

    services.AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(x =>
    {
        x.SaveToken = true;
        x.TokenValidationParameters = new TokenValidationParameters
        {
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidAudience = "somehost...",
            ValidIssuer = "somehost...",
        };
    });
}

StartUp (Configure)

public void Configure(IApplicationBuilder app, IWebHostEnvironment envy)
{
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(x => x.MapControllers());
}

Controller:

[ApiController]
[Authorize(Roles = "some_random_string_which_is_not_registered_anywhere")] // <== any request with a valid token can access this controller
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public string Get()
    {
        return "how are you?"
    }
}

Token Service

public class JwtService : ITokenService
{
    private readonly JwtConfig _config;
    public JwtService(IOptions<JwtConfig> config) =>  _config = config.Value;

    public string GenerateRefreshToken(int size = 32)
    {
        var randomNumber = new byte[size];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(randomNumber);
            return Convert.ToBase64String(randomNumber);
        }
    }

    public string GenerateAccessToken(IEnumerable<Claim> claims)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config.Secret));
        var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

        var tokeOptions = new JwtSecurityToken(
            issuer: _config.Issuer,
            audience: _config.Audience,
            claims: claims,
            expires: DateTime.Now.AddMinutes(int.Parse(_config.ExpirationInMinutes)),
            signingCredentials: signinCredentials
        );

        return tokenHandler.WriteToken(tokeOptions);
    }


    public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
    {
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = false, 
            ValidateIssuer = false,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config.Secret)),
            ValidateLifetime = false 
        };

        var tokenHandler = new JwtSecurityTokenHandler();
        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);

        var jwtSecurityToken = securityToken as JwtSecurityToken;

        if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
            throw new SecurityTokenException("Invalid token");

        return principal;
    }

}

Login (using token service)
(currently I'm not adding any roles to the token, nonetheless users have full access to resources guarded with specific roles.)

[AllowAnonymous]
[HttpPost()]
public async Task<IActionResult> Post(LoginDTO model)
{
    if (!ModelState.IsValid) return BadRequest("errors.invalidParams");

    var user = await _userManager.FindByEmailAsync(model.Email);
    if (user == null)
    {
        return Unauthorized("errors.loginFailure");
    }

    var result = await _signInManager.PasswordSignInAsync(user?.UserName, model.Password, model.RememberMe, false);


    if (result.Succeeded)
    {
        var claims = new List<Claim>
        {
            new Claim(AppClaim.TenantId, user.TenantId.ToString()),
            new Claim(JwtRegisteredClaimNames.Email, user.Email),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        };

        claims.AddRange(await _authOperations.GetUserRolesAsClaims(user));
        claims.AddRange(await _authOperations.GetAllUserClaims(user));

        var accessToken = _tokenService.GenerateAccessToken(claims);
        var refreshToken = _tokenService.GenerateRefreshToken();
        user.RefreshToken = refreshToken;
        user.RefreshTokenExpiryTime = DateTime.Now.AddDays(7);

        await _ctx.SaveChangesAsync();

        return Ok(new TokenExchangeDTO
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken
        });
    }

appsettings.json

"JwtConfig": {
  "Secret": "secret...",
  "ExpirationInMinutes": 1440,
  "Issuer": "somehost...",
  "Audience": "somehost..."
}

Please let me know if extra details or better information are needed to answer my question.

Here is the whole working demo about how to use JWT role based authentication:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters()
            {
                ValidIssuer = Configuration["Jwt:JwtIssuer"],
                ValidAudience = Configuration["Jwt:JwtIssuer"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:JwtKey"])),
                ValidateIssuer = true, 
                ValidateAudience = true,
                ValidateIssuerSigningKey = true,
            };
        });       
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseHttpsRedirection();
    app.UseRouting();

    app.UseAuthentication(); 
        
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
        endpoints.MapControllers();
    });
}

Store Issuer , Audience and SigningKey in appSettings.json:

"jwt": {
    "JwtKey": "YourJwtKey",
    "JwtIssuer": "YourJwtIssuer"
}

Generate the token:

[Route("api/[Controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private IConfiguration _config;
    public ValuesController(IConfiguration config)
    {
        _config = config;
    }
    [Route("GenerateToken")]
    public async Task<IActionResult> GenerateToken()
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Role, "Admin")
        };
        var token = new JwtSecurityToken(_config["Jwt:JwtIssuer"],
                                         _config["Jwt:JwtIssuer"],
                                         claims: claims,
                                         expires: DateTime.Now.AddDays(5),
                                         signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:JwtKey"])),
                                             SecurityAlgorithms.HmacSha256));
        var data = new JwtSecurityTokenHandler().WriteToken(token);
        return Ok(new { data });                   
    }
}

Test method:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    [Authorize(Roles = "admin")]
    [HttpGet]
    public async Task<IActionResult> Get()
    {

        return Ok();
    }
    [Authorize(Roles = "Admin")]
    [HttpGet("GetAdmin")]
    public async Task<IActionResult> GetAdmin()
    {

        return Ok();
    }
}

Result:

在此处输入图像描述

Reference:

https://stackoverflow.com/a/61403262/11398810

It turned out that I've mistakenly configured one of my custom Authorization Handlers to accept the base IAuthorizationRequirement as a Requirement parameter type, instead of a specific derived Requirement, as a result context.Succeed(requirement) was called for any requirement essentially marking it as succeeded.

The original code:

public class IsInTenantRequirement : IAuthorizationRequirement { }

public class IsInTenantAuthorizationHandler : AuthorizationHandler<IAuthorizationRequirement>
{
    private readonly RouteData _routeData;

    public IsInTenantAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
    {
        _routeData = httpContextAccessor.HttpContext.GetRouteData();
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement)
    {
        var tenantIdFromRequest = _routeData.Values["tenantId"]?.ToString();
        var tenantId = context.User.FindFirstValue(AppClaim.TenantId);

        if (tenantIdFromRequest == tenantId)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

The updated code:

public class IsInTenantRequirement : IAuthorizationRequirement { }

public class IsInTenantAuthorizationHandler : AuthorizationHandler<IsInTenantRequirement>
{
    private readonly RouteData _routeData;

    public IsInTenantAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
    {
        _routeData = httpContextAccessor.HttpContext.GetRouteData();
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsInTenantRequirement requirement)
    {
        var tenantIdFromRequest = _routeData.Values["tenantId"]?.ToString();
        var tenantId = context.User.FindFirstValue(AppClaim.TenantId);

        if (tenantIdFromRequest == tenantId)
        {
            context.Succeed(requirement);
        }
        context.Succeed(requirement);


        return Task.CompletedTask;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM