Аутентификация JWT, роли, определенные в атрибуте Authorize, игнорируются

При попытке реализовать аутентификацию на основе ролей с использованием JWT в качестве схемы аутентификации по умолчанию я столкнулся с ситуацией, когда роли, определенные в атрибуте Authorize, игнорируются, что позволяет пройти любой запрос (с действительным токеном), даже если он не в тех роли (что интересно, другие политики с пользовательскими требованиями, определенными в том же атрибуте Authorize, работают нормально)

Читая статью Джерри, он упоминает, что

Вот отличная находка: ПО промежуточного слоя JWT в ASP.NET Core знает, как интерпретировать утверждение «ролей» внутри полезной нагрузки JWT, и добавит соответствующие утверждения в ClaimsIdentity. Это делает использование атрибута [Authorize] с ролями очень простым.

И:

Это становится действительно интересным, когда вы считаете, что передача ролей в [Authorize] будет фактически смотреть, есть ли утверждение типа http://schemas.microsoft.com/ws/2008/06/identity/claims/role с значение ролей, которые вы авторизуете. Это означает, что я могу просто добавить [Authorize(Roles = "Admin")] к любому методу API, и это гарантирует, что только те JWT, полезная нагрузка которых содержит «роли» утверждения, содержащие значение Admin в массиве ролей, будут авторизованы для этого метода API.

это все еще верно? (Этой статье уже несколько лет)
Я что-то не так делаю?

Запуск (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...",
        };
    });
}

Запуск (Настройка)

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

Контроллер:

[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?"
    }
}

Служба токенов

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;
    }

}

Войти (используя сервис токенов)
(в настоящее время я не добавляю никаких ролей в токен, тем не менее, пользователи имеют полный доступ к ресурсам, защищенным определенными ролями.)

[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..."
}

Пожалуйста, дайте мне знать, если для ответа на мой вопрос необходимы дополнительные подробности или более подробная информация.

Как вы сгенерировали токен?

Rena 10.12.2020 04:51
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
1 153
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вот вся рабочая демонстрация о том, как использовать аутентификацию на основе ролей JWT:

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();
    });
}

Сохраните Issuer, Audience и SigningKey в appSettings.json:

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

Сгенерируйте токен:

[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 });                   
    }
}

Метод испытания:

[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();
    }
}

Результат:

Ссылка:

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

спасибо за ответ, вроде все сделал правильно, добавил нужные детали

Rafi Henig 10.12.2020 12:32

глядя на ваш пример проекта 1drv.ms/u/s!AkZwXaPsV4CxgkbR_a95JsHjbb6s?e=zUWTHS Похоже, вы используете политики, требующие ролей, а не встроенную функцию ролей

Rafi Henig 10.12.2020 12:42

Привет @RafiHenig, нет, вам не нужно проверять мой предыдущий проект. То, что я сделал сейчас, использует функцию сборки в роли. На самом деле, я рад, что вы решили свою проблему.

Rena 11.12.2020 02:24
Ответ принят как подходящий

Оказалось, что я по ошибке настроил один из своих пользовательских обработчиков авторизации, чтобы он принимал базовый IAuthorizationRequirement в качестве типа параметра требования вместо конкретного производного требования, в результате context.Succeed(requirement) вызывался для любого требования, по существу помечая его как выполненное.

Исходный код:

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;
    }
}

Обновленный код:

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;
    }
}

Другие вопросы по теме