Как реализовать проверку подлинности на основе токенов jwt с помощью пользовательской схемы политик для авторизации в ядре. net? - PullRequest
0 голосов
/ 18 апреля 2020

Я пытаюсь создать приложение для игровой площадки. NET Core 3.1, пытаясь реализовать аутентификацию на основе токенов jwt, и хотел бы, чтобы использовал мою собственную схему политики (страшно?). Я смог сделать это довольно удобно с помощью настроенных атрибутов фильтра, которые были получены из AuthorizeAttribute in. NET Framework, но с трудностями в. NET Core. Потому что я использовал OnAuthorization ловушку и захватывал HttpActionContext, разрешал токен и проверял политику ролей и т. Д. c ... Но сейчас я использую IAuthorizationHandler, но у меня не было возможности заставить его работать в моем желаемом пути до сих пор. Я прочитал МНОГИЕ примеры, статьи, но все же я не мог найти тот же подход к тому, что я пытаюсь сделать.

(PS: Когда я ищу часы и не могу найти подобный подход, который также заставляет меня чувствовать Я очень нервничаю, потому что, возможно, иду по неправильному маршруту или пытаюсь заново изобрести колесо. Посмотрим, буду ли я ..)

Я также искал IdendityServer4 (тем не менее, многие люди находят, что им легче управлять, но мне кажется излишним то, что я пытаюсь сделать. Обвините меня, если я ошибаюсь.)

То, что я до сих пор делал, это то, что я могу чтобы успешно создать токен при входе пользователя в систему. Вот код: (если вы хотите увидеть, что именно я спрашиваю, пожалуйста, прокрутите до конца, но если вы собираетесь ответить, прочитайте)

За сценой я использую salted-ha sh пароль в дБ, а также исправление ключей с Алгоритм PBKDF2 (я ценю любые проблемы безопасности)

My GenerateT oken function:

[HttpPost("token")]
public IActionResult GenerateToken(UserCredentialDto userCredentialDto) {
    bool isValidUser = _appUserManager.IsValidCredentials(userCredentialDto);

    if (!isValidUser) {
        return BadRequest("invalid user/pass combination");
    }

    // assume I am getting all the roles that user has and add them in claims.
    var claims = _appUserManager.GetUserClaims(userCredentialDto); 
    var key = new SymmetricSecurityKey(_jwtSettings.Key);
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    var token = new JwtSecurityToken(
    issuer: _jwtSettings.Issuer, audience: _jwtSettings.Audience, claims: claims, expires: DateTime.Now.AddMinutes(StaticAFUConfigHelper.TokenExpirationInMinutes), signingCredentials: creds);

    return Ok(new {
        token = new JwtSecurityTokenHandler().WriteToken(token)
    });
}

Общий фон: (я не использую pnet таблицы идентификаторов)

  • У каждого пользователя есть роли (у меня есть таблица как Role и таблица внешних ссылок как UserRole)
  • Роли имеют политики (у меня есть таблица как Policy и таблица внешних ссылок как RolePolicy)

Общий рабочий процесс:

  • Пользователь запрашивает токен (просто - введите имя пользователя и пароль на странице входа в систему)
  • Если действительные учетные данные, генерируйте токен для пользователя, который включает роли пользователя в утверждениях.
  • Как только пользователь делает запрос к методу API, проверьте, если любой из role этого пользователя, в том числе требуется policy, чтобы иметь возможность сделать этот запрос!

Таким образом, «в основном», я хотел бы получить токен (отправил токен вместе с запросом или утверждениями), разрешить его, проверить, есть ли у пользователя то, что я ищу (укажите c policy et c ..) и действовать исходя из этого.

И в startup.cs

//Authentication
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options = >{
    options.TokenValidationParameters = new TokenValidationParameters {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = "my issuer",
        ValidAudience = "my audience",
        IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String("assume this is my secret key"))
    };
    options.SaveToken = true;
});

 // this part I'm also not sure as if I have 100s of policies, 
 // would all of them has to be defined here?
 // and how I specifically assign this to an api method! Anyways please keep reading if you dont mind
 services.AddAuthorization(options =>
                options.AddPolicy("CanReadData", policy => policy.Requirements.Add(new NeedsPolicyAttribute(PolicyEnum.CanReadData))));

Тогда я создаю ted TokenValidationHandler, производный от AuthorizationHandler с моим пользовательским атрибутом политики NeedsPolicyAttribute ..

NeedsPolicyAttribute:

 public class NeedsPolicyAttribute: IAuthorizationRequirement {
    public PolicyEnum RequiredPolicy {
        get;
    }
    public NeedsPolicyAttribute(PolicyEnum requiredPolicy) {
        RequiredPolicy = requiredPolicy;
    }
}

И HandleRequirementAsyn c:

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NeedsPolicyAttribute requirement) {
    var myToken = "1234567889"; // just hardcoded for example - assume I got the JWT from the context.

    SecurityToken validatedToken;
    var handler = new JwtSecurityTokenHandler();

    // assume there was no exception and I was able to validate the token which is a valid token...
    var user = handler.ValidateToken(myToken, _jwtSettings.TokenValidationParameters, out validatedToken);

    // ************************........ATTENTION HERE....... *****************
    // I would like to check if the user has a role and which includes the policy which was required by the api method.
    //if so then, 
    context.Succeed(requirement);

    //if not then
    context.Fail();

    // and finally
    return Task.CompletedTask;
}

И мой пример API-метода выглядит следующим образом:

  [HttpGet][Route("get/{id}")]
  [Authorize("CanReadData")] // THIS JUST LETS ME TRIGGER MY CUSTOM ATTRIBUTE TO BE CAPTURED BY HandleRequirementAsync BUT I CAN ONLY PROVIDE HARDCODE STRING
   public ActionResult < AppUserDto > GetAppUser(int id) {
     return _appUserManager.Get(id);
}

Что я на самом деле хочу сделать, так это украсить мой API-метод требуемой политикой и проверить, если данный токен имеет заявление с ролью, которая включает в себя требуемую политику .

что-то вроде ниже:

  [HttpGet][Route("get/{id}")]
  [MyPolicyAttrubute(MyPolicyEnum.CanDoBlaBla)] // I want to capture this in HandleRequirementAsync if possible and compare with my user claims..
   public ActionResult < AppUserDto > GetAppUser(int id) {
     return _appUserManager.Get(id);
}

захватывающие дух вопросы:

  • Я пытаетесь заново изобрести колесо?
  • Даже если я и есть, есть ли очевидные плохие практики с таким подходом?
  • Любая другая / лучшая рекомендация?

1 Ответ

0 голосов
/ 20 апреля 2020

Проведя часы, я смог сделать эту работу. Поэтому я просто хотел опубликовать это как ответ на мой вопрос, но мои " захватывающие дух вопросы " (см. В конце моего вопроса выше) все еще остаются. Так что имейте в виду, что это решение не гарантирует никаких проблем.

Одна из проблем, с которыми я столкнулся при написании кода в StartUp.cs в моем вопросе выше, была:

// if I have 100s of policies, would all of them have to be defined here?
 services.AddAuthorization(options =>
                options.AddPolicy("CanReadData", policy => policy.Requirements.Add(new NeedsPolicyAttribute(PolicyEnum.CanReadData))));

, потому что примеры кода добавляли их одну за другой в виде жестко закодированной строки, которая была беспокоит меня с самого начала, так как я хочу использовать Enum, а не жестко закодированные значения. И я не хотел добавлять много строк в Startup.cs, который также нужно было обновлять каждый раз, когда я добавляю новую политику в приложение.

Так что на самом деле это было легко. Все, что я сделал:

Я написал расширение для получения всех значений перечисления, как показано ниже:

 public static class EnumUtils {
  public static IEnumerable < T > GetAllEnumValues < T > () {
   return System.Enum.GetValues(typeof(T)).Cast < T > ();
  }
 }

Так что я смог использовать его, как показано ниже. Таким образом, я могу использовать любое новое созданное значение перечисления Policy поверх методов API в качестве атрибута, не касаясь StartUp.cs сейчас.

    services.AddAuthorization(options => {
     // add all the policies to option to be able to use in ExtendedAuthorizeAttribute on api methods.
     foreach(var policyEnum in EnumUtils.GetAllEnumValues < PolicyEnum > ())
           options.AddPolicy(policyEnum.ToString(), policy => policy.Requirements.Add(new ExtendedAuthorizeAttribute(policyEnum)));
    });

Затем я добавил политики, которые имеет пользователь:

public List < Claim > GetUserClaims(AuthRequestDto authRequestDto) {
    var userRoles = _unitOfWork.Roles.GetUserRoles(authRequestDto.UserId);
    var policies = userRoles.SelectMany(x = >x.RolePolicies.Where(p = >p.Policy.IsActive).Select(y = >y.Policy.Name)).Distinct().ToList();

    var claims = new List < Claim > ();
    policies.ForEach(policy = >claims.Add(new Claim("UserPolicy", policy)));
    claims.Add(new Claim("Id", authRequestDto.UserId.ToString()));
    return claims;
}

И прикрепил их к моему токену, чтобы, как только пользователь сделал запрос с этим токеном, я мог разрешить его и проверить на соответствие требованиям политики по методу API.

Затем я создал новый Attribute как ExtendedAuthorizeAttribute который происходит от AuthroizeAttribute И реализует IAuthorizationRequirement

Итак, 2 вещи здесь: я получил свой пользовательский атрибут из AuthroizeAttribute, потому что я хочу, чтобы он автоматически запускался для авторизации, чтобы проверить, имеет ли пользователь требуемую политику для этот метод API. И я реализовал IAuthorizationRequirement, потому что это позволяет мне использовать мой атрибут как «требование» в методе HandleRequirementAsync.

Итак, атрибут, который я создал, был:

/// <summary>
/// Extended Authorize Attribute is derived from Authorize Attribute
/// also implements IAuthorizationRequirement.
/// Deriving from AuthorizeAttribute accepts only string for policy names
/// By using this extension class, it let's me use Policy Enum then it converts it to string
/// before passing it to AuthorizeAttribute which was not possible in controller.  
/// </summary>
public class ExtendedAuthorizeAttribute: AuthorizeAttribute,
IAuthorizationRequirement {
    public ExtendedAuthorizeAttribute(PolicyEnum policyEnum = PolicyEnum.General) : base(policyEnum.ToString()) {}
}

И TokenValidationHandler стал как показано ниже:

public class TokenValidationHandler: AuthorizationHandler < ExtendedAuthorizeAttribute > {
    private readonly JwtSettings _jwtSettings;
    private readonly IHttpContextAccessor _contextAccessor;

    public TokenValidationHandler(JwtSettings jwtSettings, IHttpContextAccessor contextAccessor) {
        _jwtSettings = jwtSettings;
        _contextAccessor = contextAccessor;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ExtendedAuthorizeAttribute requirement) {

        // injected the IHttpContextAccessor to get the token from the request.
        var rawToken = !_contextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization") ? string.Empty: _contextAccessor ? .HttpContext ? .Request ? .Headers["Authorization"].ToString();

        if (string.IsNullOrEmpty(rawToken)) {
            context.Fail();
            return Task.CompletedTask;
        }

        var token = ScrubToken(rawToken);
        var handler = new JwtSecurityTokenHandler();

        try {
            // validates the given token and returns claims principal for user if validated.
            var user = handler.ValidateToken(token, _jwtSettings.TokenValidationParameters, out SecurityToken _);

            // Check if UserPolicies claims include the required the policy 
            if (IsRequiredPolicyExistOnUser(user.Claims ? .ToList(), requirement)) {
                context.Succeed(requirement);
            } else {
                context.Fail();
            }

        } catch(Exception e) {
            // TODO: Logging!
            context.Fail();
        }

        return Task.CompletedTask;
    }

    private bool IsRequiredPolicyExistOnUser(List < Claim > userClaims, ExtendedAuthorizeAttribute requirement) {
        return userClaims != null && userClaims.Any() && userClaims.Where(x = >x.Type == "UserPolicy").Any(c = >c.Value == requirement.Policy.ToString());
    }

    private string ScrubToken(string rawToken) {
        return rawToken.Replace("Bearer ", "");
    }
}

И, наконец, я смог использовать это в моих методах API, как показано ниже:

[HttpGet]
[Route("get/{id}")]
[ExtendedAuthorize(PolicyEnum.CanReadData)]
public ActionResult < AppUserDto > GetAppUser(int id) {
    return _appUserManager.Get(id);
}

, и все заработало так, как я хотел. Но, опять же, захватывающих вопросов все еще существуют!

...