Аутентификация на основе ролей клиента? - PullRequest
0 голосов
/ 06 июня 2018

В настоящее время я аутентифицирую пользователей в своем приложении, используя аутентификацию на основе ролей с OAuth и WebApi.Я настроил это так:

public override async Task GrantResourceOwnerCredentials (OAuthGrantResourceOwnerCredentialsContext context)
{
    var user = await AuthRepository.FindUser(context.UserName, context.Password);

    if (user === null)
    {
        context.SetError("invalid_grant", "The username or password is incorrect");
        return;
    }

    var id = new ClaimsIdentity(context.Options.AuthenticationType);
    id.AddClaim(New Claim(ClaimTypes.Name, context.UserName));

    foreach (UserRole userRole in user.UserRoles)
    {
        id.AddClaim(new Claim(ClaimTypes.Role, userRole.Role.Name));
    }

    context.Validated(id);
}

Защита моих маршрутов API с помощью тега <Authorize>.

Однако с тех пор я столкнулся с проблемой, из-за которой мои пользователи могутзанимать разные роли для разных клиентов.Например:

Пользователь A может быть связан с несколькими клиентами: клиент A и клиент B.
Пользователь A может иметь разные «роли» при доступе к информации от любого клиента.Таким образом, пользователь A может быть администратором для клиента A и основным пользователем для клиента B.

Это означает, что в следующем примере:

[Authorize(Roles = "Admin")]
[Route("api/clients/{clientId}/billingInformation")]
public IHttpActionResult GetBillingInformation(int clientId) 
{
    ...
}

Пользователь A может получить доступ к платежной информации для клиента A,но не для Клиента Б.

Очевидно, что то, что у меня сейчас есть, не будет работать для этого типа аутентификации.Как лучше всего настроить аутентификацию на основе ролей для конкретного клиента?Могу ли я просто изменить то, что у меня есть сейчас, или мне придется полностью настроить его по-другому?

Ответы [ 4 ]

0 голосов
/ 16 июня 2018

Требуется наличие пользователей с разными полномочиями.Не чувствуйте себя обязанным строго сопоставлять права / разрешения пользователя с его ролями.Роли являются частью личности пользователя и не должны зависеть от клиента.Предлагаю разложить требование:

  • Только администратор может получить доступ к биллинговому домену / подсистеме

    //Role-based authorization
    [Authorize(Roles = "Admin")]
    public class BillingController {
    

    https://docs.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-2.1

  • Пользователь A может получить доступ к платежной информации для Клиента A, но не для Клиента B. Атрибут не может работать здесь, потому что нам нужно хотя бы загрузить соответствующий клиент.
    https://docs.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-2.1&tabs=aspnetcore2x
    Не существует встроенного способа длярешить это.Вы должны определить свою собственную систему настройки прав доступа продавца.Это может быть простая таблица «многие ко многим» (1 пользователь может получить доступ к N торговцам, 1 торговец может получить доступ к N пользователям)

    public IHttpActionResult GetBillingInformation(int clientId) 
    {
        var merchant = clientRepository.Get(clientId);
        if(!UserIsConfiguredToAccessMerchant(User, merchant))
            return Unauthorized();
    
    }
    

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

  • Размер токена может резко увеличиться

  • Пользовательмогут иметь различные полномочия в отношении контекста домена или микросервиса

Ниже приведены некоторые полезные ссылки:

https://docs.microsoft.com/en-us/dotnet/framework/security/claims-based-identity-model
https://leastprivilege.com/2016/12/16/identity-vs-permissions/
https://leastprivilege.com/2014/06/24/resourceaction-based-authorization-for-owin-and-mvc-and-web-api/

0 голосов
/ 15 июня 2018

Лично я думаю, что вам нужно полностью отказаться от использования атрибута [Authorize].Понятно, что ваши требования к авторизации более сложны, чем тот метод, для которого он был готов.

Также в вопросе о том, как мне кажется, аутентификация и авторизация используются взаимозаменяемо.Здесь мы имеем дело с Авторизацией.

Поскольку вы используете авторизацию на основе идентификационных данных и утверждений.Я бы посмотрел на это "на лету", так сказать.Наряду с утверждениями вы могли бы использовать динамическую генерацию политики, а также авторизацию на основе сервисов, используя IAuthorizationRequirement экземпляры для создания сложных правил и требований.

Углубление в реализации этого является большой темой, но естьнекоторые очень хорошие ресурсы доступны.Первоначальный подход (который я использовал сам) был первоначально детально описан Домом и Броком из IdentityServer слава.

Они сделали всестороннюю видео-презентацию по этому вопросу в NDC в прошлом году, которую вы можете посмотреть здесь .

Основываясь на концепциях, обсуждаемых в этом видео, Джерри Пелсер писал о близкой реализации, которую вы можете прочитать здесь .

Общие компонентыявляются:

Атрибуты [Authorize] будут заменены генератором политик, например:

public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
    private readonly IConfiguration _configuration;

    public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options, IConfiguration configuration) : base(options)
    {
        _configuration = configuration;
    }

    public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        // Check static policies first
        var policy = await base.GetPolicyAsync(policyName);

        if (policy == null)
        {
            policy = new AuthorizationPolicyBuilder()
                .AddRequirements(new HasScopeRequirement(policyName, $"https://{_configuration["Auth0:Domain"]}/"))
                .Build();
        }

        return policy;
    }
}

А затем вы создадите все экземпляры IAuthorizationRequirement, необходимые для обеспечения правильной авторизации пользователей,Примером этого может быть что-то вроде:

public class HasScopeRequirement : IAuthorizationRequirement
{
    public string Issuer { get; }
    public string Scope { get; }

    public HasScopeRequirement(string scope, string issuer)
    {
        Scope = scope ?? throw new ArgumentNullException(nameof(scope));
        Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
    }
}

Дом и Брок затем также детализируют клиентскую реализацию, которая связывает все это вместе, что может выглядеть примерно так:

  public class AuthorisationProviderClient : IAuthorisationProviderClient
  {
    private readonly UserManager<ApplicationUser> userManager;
    private readonly RoleManager<IdentityRole> roleManager;

    public AuthorisationProviderClient(
      UserManager<ApplicationUser> userManager, 
      RoleManager<IdentityRole> roleManager)
    {
      this.userManager = userManager;
      this.roleManager = roleManager;
    }

    public async Task<bool> IsInRole(ClaimsPrincipal user, string role)
    {
      var appUser = await GetApplicationUser(user);
      return await userManager.IsInRoleAsync(appUser, role);
    }

    public async Task<List<Claim>> GetAuthorisationsForUser(ClaimsPrincipal user)
    {
      List<Claim> claims = new List<Claim>();
      var appUser = await GetApplicationUser(user);

      var roles = await userManager.GetRolesAsync(appUser);

      foreach (var role in roles)
      {
        var idrole = await roleManager.FindByNameAsync(role);

        var roleClaims = await roleManager.GetClaimsAsync(idrole);

        claims.AddRange(roleClaims);
      }

      return claims;
    }

    public async Task<bool> HasClaim(ClaimsPrincipal user, string claimValue)
    {
      Claim required = null;
      var appUser = await GetApplicationUser(user);

      var userRoles = await userManager.GetRolesAsync(appUser);

      foreach (var userRole in userRoles)
      {
        var identityRole = await roleManager.FindByNameAsync(userRole);

        // this only checks the AspNetRoleClaims table
        var roleClaims = await roleManager.GetClaimsAsync(identityRole);
        required = roleClaims.FirstOrDefault(x => x.Value == claimValue);

        if (required != null)
        {
          break;
        }
      }

      if (required == null)
      {
        // this only checks the AspNetUserClaims table
        var userClaims = await userManager.GetClaimsAsync(appUser);
        required = userClaims.FirstOrDefault(x => x.Value == claimValue);
      }

      return required != null;
    }

    private async Task<ApplicationUser> GetApplicationUser(ClaimsPrincipal user)
    {
      return await userManager.GetUserAsync(user);
    }
  }

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

0 голосов
/ 15 июня 2018

Одним из решений было бы добавить отношение клиент / пользователь как часть ClaimsIdentity, а затем проверить это с производным AuthorizeAttribute.

Вы бы расширили объект User с помощью словарясодержит все их роли и клиентов, для которых они авторизованы в этой роли - предположительно, содержащиеся в вашем db:

public Dictionary<string, List<int>> ClientRoles { get; set; }

В вашем методе GrantResourceOwnerCredentials вы добавили бы их как отдельные утверждения с идентификаторами клиентов какзначение:

foreach (var userClientRole in user.ClientRoles)
{
    oAuthIdentity.AddClaim(new Claim(userClientRole.Key,
        string.Join("|", userClientRole.Value)));
}

А затем создайте пользовательский атрибут для обработки чтения значения утверждений.Немного сложная часть здесь получает значение clientId.Вы привели один пример, где он находится в маршруте, но он может не соответствовать вашему приложению.Вы можете передать его явно в заголовке или получить любую функцию парсинга URL / маршрута, работающую при всех необходимых обстоятельствах.

public class AuthorizeForCustomer : System.Web.Http.AuthorizeAttribute
{
    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        var isAuthorized = base.IsAuthorized(actionContext);

        string clientId = ""; //Get client ID from actionContext.Request;

        var user = actionContext.ControllerContext.RequestContext.Principal as ClaimsPrincipal;
        var claim = user.FindFirst(this.Roles);
        var clientIds = claim.Value.Split('|');

        return isAuthorized && clientIds.Contains(clientId);
    }
}

И вы просто поменяете

[Authorize(Roles = "Admin")] на [AuthorizeForCustomer(Roles = "Admin")]

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

0 голосов
/ 12 июня 2018

Вы можете удалить тег авторизации и выполнить проверку роли внутри функции.

Лямбда-решение:

Существуют ли роли, которые добавляются на основе CustomerID и UserID?
Если это так, вы можете сделать что-то вроде приведенного ниже примера, где вы получитеклиент на основе значений, которые у вас есть, а затем вернуть ответ.

string userID = RequestContext.Principal.Identity.GetUserId();
var customer = Customer.WHERE(x => x.UserID == userID && x.clientId == clientId && x.Roles == '1')

Можете ли вы предоставить нам дополнительную информацию о том, что вы используете для хранения связи / роли между Клиентом и Пользователем.

РЕДАКТИРОВАТЬ:

Вот пример того, как вы можете использовать ActionFilterAttribute.Он получает CustomerId из запроса, а затем берет UserId идентификатора из запроса.Таким образом, вы можете заменить [Authorize] на [UserAuthorizeAttribute]

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
    public class UserAuthorizeAttribute : System.Web.Http.Filters.ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            try
            {
                var authHeader = actionContext.Request.Headers.GetValues("Authorization").First();
                if (string.IsNullOrEmpty(authHeader))
                {
                    actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Missing Authorization-Token")
                    };
                    return;
                }

                ClaimsPrincipal claimPrincipal = actionContext.Request.GetRequestContext().Principal as ClaimsPrincipal;
                if (!IsAuthoticationvalid(claimPrincipal))
                {
                    actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Invalid Authorization-Token")
                    };
                    return;
                }

                if (!IsUserValid(claimPrincipal))
                {
                    actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Invalid User name or Password")
                    };
                    return;
                }

                //Finally role has perpession to access the particular function
                if (!IsAuthorizationValid(actionContext, claimPrincipal))
                {
                    actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Permission Denied")
                    };
                    return;
                }

            }
            catch (Exception ex)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
                {
                    Content = new StringContent("Missing Authorization-Token")
                };
                return;
            }

            try
            {
                //AuthorizedUserRepository.GetUsers().First(x => x.Name == RSAClass.Decrypt(token));
                base.OnActionExecuting(actionContext);
            }
            catch (Exception)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    Content = new StringContent("Unauthorized User")
                };
                return;
            }
        }

        private bool IsAuthoticationvalid(ClaimsPrincipal claimPrincipal)
        {
            if (claimPrincipal.Identity.AuthenticationType.ToLower() == "bearer"
                && claimPrincipal.Identity.IsAuthenticated)
            {
                return true;
            }
            return false;
        }

        private bool IsUserValid(ClaimsPrincipal claimPrincipal)
        {
            string userID = claimPrincipal.Identity.GetUserId();
            var securityStamp = claimPrincipal.Claims.Where(c => c.Type.Equals("AspNet.Identity.SecurityStamp", StringComparison.OrdinalIgnoreCase)).Single().Value;

            var user = _context.AspNetUsers.Where(x => x.userID.Equals(userID, StringComparison.OrdinalIgnoreCase)
                && x.SecurityStamp.Equals(securityStamp, StringComparison.OrdinalIgnoreCase));
            if (user != null)
            {
                return true;
            }
            return false;
        }

        private bool IsAuthorizationValid(HttpActionContext actionContext, ClaimsPrincipal claimPrincipal)
        {
            string userId = claimPrincipal.Identity.GetUserId();
            string customerId = (string)actionContext.ActionArguments["CustomerId"];
            return AllowedToView(userId, customerId);
        }

        private bool AllowedToView(string userId, string customerId)
        {
            var customer = _context.WHERE(x => x.UserId == userId && x.CustomerId == customerId && x.RoleId == '1')
            return false;
        }
    }
...