客户端特定的基于角色的身份验证?

4

目前,我正在使用基于角色的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);
}

使用<Authorize>标签保护我的API路由。

然而,我遇到了一个问题,我的用户可以为不同的客户端持有不同的角色。例如:

用户A可以与多个客户关联:客户A和客户B。
当从任一客户端访问信息时,用户A可以拥有不同的"角色"。因此,对于客户A,用户A可能是管理员,而对于客户B,用户A可能只是普通用户。

这意味着以下示例:

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

用户A可以访问客户A的计费信息,但不能访问客户B的计费信息。

显然,我现在所拥有的身份验证方式无法满足此类身份验证。如何设置基于角色的特定于客户端的身份验证是最佳方式?我是否可以简单地更改现有内容,还是必须完全以不同的方式进行设置?


重写Onauthorization方法,并将角色映射到用户ID。我认为这是最好的方式。 - Debashish Saha
@Jun,你要找的是“基于资源的授权”。已经有很好的答案了:https://dev59.com/AWMk5IYBdhLWcg3w6yHh - momo
你的 AuthRepository 是自定义实现吗? - ste-fu
@ste-fu 是的。你为什么问? - Jun Kang
我在想将客户ID传递到获取用户方法中是否有效...不过我认为我的答案总体上更好。 - ste-fu
4个回答

1
你可以删除授权标签,而是在函数内部进行角色验证。
Lambda解决方案:
如果基于客户ID和用户ID添加了角色,那么你可以像下面的示例一样根据你拥有的值获取客户信息,然后返回响应。
string userID = RequestContext.Principal.Identity.GetUserId();
var customer = Customer.WHERE(x => x.UserID == userID && x.clientId == clientId && x.Roles == '1')

您能否提供有关客户和用户之间连接/角色存储使用的信息?

编辑:

这里是如何使用ActionFilterAttribute的示例。它从请求中获取CustomerId,然后从请求中获取标识的UserId。因此,您可以使用[UserAuthorizeAttribute]替换[Authorize]。

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

1
我拒绝相信在每个API路由内进行授权检查是处理此类身份验证的最佳方式。如果您想获取更多信息,请留下评论;如果您暂时无法留下评论,请等待可以留言的时候再来。目前,客户和用户之间没有联系,这就是我需要的。如果您阅读了问题,您会发现这基本上是我的主要问题。最后,我希望用户的UserRoles看起来像这样:UserId, RoleId, CustomerId - Jun Kang
非常抱歉,我误解了问题,并想知道名称,以便编写 PoC 而不仅仅是给您提供方向。如果您想对所有 API 调用进行全局检查,可以使用 ActionFilterAttribute 类来创建自己的授权过程。我更新了答案,并提供了一个示例,说明如何创建一个。 - Daniel Frykman
你可能不愿相信,但是你在问题中描述的场景并不适合 [Authorize] 属性设计的简单用例。 - Jammer

1
个人认为你需要完全摆脱使用[Authorize]属性。很明显,您的授权要求比该方法“开箱即用”所预期的更加复杂。
此外,在关于我认为身份验证和授权被互换使用的问题上。我们在这里处理的是授权。
由于您正在使用基于身份和声明的授权。我会考虑在“即时”进行此操作。除了声明之外,您还可以利用动态策略生成以及使用IAuthorizationRequirement实例的基于服务的授权来构建复杂的规则和要求。
深入研究其实现是一个大话题,但有一些非常好的资源可用。最初的方法(我自己使用过)最初由IdentityServer的Dom和Brock详细介绍。
他们在去年的NDC上做了一次全面的视频演示,您可以在此处观看。

本文介绍的概念与 Jerrie Pelser 在其博客中讨论的实现密切相关,您可以在这里阅读。

一般组件包括:

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

Dom和Brock还详细介绍了一个客户端实现,将所有这些内容绑定在一起,可能看起来像这样:
  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);
    }
  }

虽然此实现未完全满足您的确切要求(无论如何都很难做到),但在您提出的情况下,这几乎肯定是我会采用的方法。


1
一种解决方案是将客户端/用户关系作为 ClaimsIdentity 的一部分添加,并使用派生的 AuthorizeAttribute 进行检查。
您可以通过扩展 User 对象,使用包含其所有角色和在该角色中授权的客户端的字典 - 可能包含在您的数据库中:
public Dictionary<string, List<int>> ClientRoles { get; set; }

在您的GrantResourceOwnerCredentials方法中,您将这些作为单独的声明添加,其中客户端ID作为值:
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

这个需求涉及到具有不同授权的用户。不必严格匹配用户权限/授权与其角色。角色是用户身份的一部分,不应该依赖于客户端。
我建议将需求分解为以下几点:

注意:声明应仅包含用户身份数据(姓名、电子邮件、角色等)。在令牌中添加授权、访问权限声明不是一个好选择,我个人的观点是:
  • 令牌大小可能会大幅增加

  • 用户可能具有不同的授权,涉及域上下文或微服务

以下是一些有用的链接:

https://learn.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/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接