【问题标题】:Get Azure AD Groups Before Building Authorization Policies在构建授权策略之前获取 Azure AD 组
【发布时间】:2019-11-27 18:08:25
【问题描述】:

我们正在开发一个使用基于 .Net Core 2.2 Web API 构建的后端的应用程序。我们的大多数控制器只需要[Authorize] 属性而没有指定策略。但是,某些终结点将要求用户位于特定的 Azure AD 安全组中。对于这些情况,我在 Startup.cs 文件中实现了这样的策略:

var name = "PolicyNameIndicatingGroup";
var id = Guid.NewGuid; // Actually, this is set to the object ID of the group in AD.

services.AddAuthorization(
    options =>
    {
        options.AddPolicy(
            name,
            policyBuilder => policyBuilder.RequireClaim(
                "groups",
                id.ToString()));
    });

然后,对于需要此类授权的控制器,我有:

[Authorize("PolicyNameIndicatingGroup")]
public async Task<ResponseBase<string>> GroupProtectedControllerMethod() {}

问题是我们的用户都在大量的组中。这会导致 Graph API 根本不返回任何组声明,而是将简单的 hasGroups 布尔声明设置为 true。因此,nooneany组,因此无法通过授权。可以阅读关于here 的这个无组问题。

这种基于字符串的策略注册,尽管它可能乏善可陈,但似乎是 .Net Core 人们所推荐的,但如果用户声明中没有填充组,它就会失败。我并没有真正看到如何解决这个问题。 是否有一些特殊的方法可以为我的 API 设置 AppRegistration,以便它确实获取用户声明中填充的所有组?

更新:

在解决方案中,我确实有一个调用 Graph 来获取用户组的服务。但是,在为时已晚之前,我无法弄清楚如何调用它。换句话说,当用户点击控制器上的 AuthorizeAttribute 来检查策略时,用户的组还没有被填充,所以受保护的方法总是用 403 阻止它们。

我的尝试包括为我的所有 Web API 控制器制作一个自定义基本控制器。在基本控制器的构造函数中,我正在调用一个方法来检查 User.IdentityClaimsIdentity 类型)以查看它是否已创建并经过身份验证,如果是,我正在使用 ClaimsIdentity.AddClaim(Claim claim) 方法来填充从我的 Graph 调用中检索到的用户组。但是,当进入基本控制器的构造函数时,User.Identity 尚未设置,因此不会填充组,如前所述。不知何故,我需要在构建控制器之前填充用户组。

【问题讨论】:

    标签: azure asp.net-core azure-active-directory asp.net-core-webapi asp.net-core-2.2


    【解决方案1】:

    您可以改为使用 Microsoft Graph API 来查询用户组:

    POST https://graph.microsoft.com/v1.0/directoryObjects/{object-id}/getMemberGroups
    Content-type: application/json
    
    {
       "securityEnabledOnly": true
    }
    

    参考:https://docs.microsoft.com/en-us/graph/api/directoryobject-getmembergroups?view=graph-rest-1.0&tabs=http

    场景将是:

    1. 您的客户端应用程序将获取访问令牌 (A) 以访问您的后端 Web API。
    2. 您的 Web API 应用程序将获取访问令牌 (B),以便使用 OAuth 2.0 On-Behalf-Of flow 使用访问令牌 (A) 访问 Microsoft Graph API。访问令牌 (B) 将用于获取用户的组。
    3. Web API 使用策略(推荐)或自定义属性验证用户组。

    使用 Azure AD V2.0 终结点在此article 中列出了协议图和示例请求。 This article 用于 V1.0 端点。 Here 是 .Net Core 的代码示例。

    【讨论】:

    • 我的问题在#3。我有一个使用令牌 B 获取用户组的服务调用。我正在 Startup 中进行设置,以便我可以构建授权策略。我同时使用标准属性和自定义属性(在两种不同的方法上,看看是否有效)。对于前者,我将传递字符串策略名称。对于后者,我传递了一个枚举,该枚举映射到字符串并传递给 AuthorizeAttribute。问题是,在命中属性的时候还没有建立用户,所以没有返回任何组。我会用这些细节更新我的问题。
    • 尝试移动组查询并添加到自定义属性中的原则逻辑。
    【解决方案2】:

    感谢 ASP.NET Core 团队中的某个人提供的一些提示,我找到了此解决方案的答案。此解决方案涉及实现IClaimsTransformation(在Microsoft.AspNetCore.Authentication 命名空间中)。引用我的消息来源:

    [IClaimsTransformation] 是您连接到请求管道的服务,它将在每次身份验证后运行,您可以根据需要使用它来增加身份。那将是您进行 Graph API 调用的地方 [...]。”

    所以我编写了以下实现(请参阅代码下方的重要警告):

    public class AdGroupClaimsTransformer : IClaimsTransformation
    {
        private const string AdGroupsAddedClaimType = "adGroupsAlreadyAdded";
        private const string ObjectIdClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier";
    
        private readonly IGraphService _graphService; // My service for querying Graph
        private readonly ISecurityService _securityService; // My service for querying custom security information for the application
    
        public AdGroupClaimsTransformer(IGraphService graphService, ISecurityService securityService)
        {
            _graphService = graphService;
            _securityService = securityService;
        }
    
        public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
        {
            var claimsIdentity = principal.Identity as ClaimsIdentity;
            var userIdentifier = FindClaimByType(claimsIdentity, ObjectIdClaimType);
            var alreadyAdded = AdGroupsAlreadyAdded(claimsIdentity);
    
            if (claimsIdentity == null || userIdentifier == null || alreadyAdded)
            {
                return Task.FromResult(principal);
            }
    
            var userSecurityGroups = _graphService.GetSecurityGroupsByUserId(userIdentifier).Result;
            var allSecurityGroupModels = _securityService.GetSecurityGroups().Result.ToList();
    
            foreach (var group in userSecurityGroups)
            {
                var groupIdentifier = allSecurityGroupModels.Single(m => m.GroupName == group).GroupGuid.ToString();
    
                claimsIdentity.AddClaim(new Claim("groups", groupIdentifier));
            }
    
            claimsIdentity.AddClaim(new Claim(AdGroupsAddedClaimType, "true"));
    
            return Task.FromResult(principal);
        }
    
        private static string FindClaimByType(ClaimsIdentity claimsIdentity, string claimType)
        {
            return claimsIdentity?.Claims?.FirstOrDefault(c => c.Type.Equals(claimType, StringComparison.Ordinal))
                ?.Value;
        }
    
        private static bool AdGroupsAlreadyAdded(ClaimsIdentity claimsIdentity)
        {
            var alreadyAdded = FindClaimByType(claimsIdentity, AdGroupsAddedClaimType);
            var parsedSucceeded = bool.TryParse(alreadyAdded, out var valueWasTrue);
    
            return parsedSucceeded && valueWasTrue;
        }
    }
    

    在我的 Startup.cs 中,在 ConfigureServices 方法中,我像这样注册实现:

    services.AddTransient<IClaimsTransformation, AdGroupClaimsTransformer>();
    

    注意事项

    您可能已经注意到,我的实现是防御性编写的,以确保不会在已完成该过程的 ClaimsPrincipal 上再次运行转换。这里的潜在问题是对IClaimsTransformation 的调用可能会发生多次,这在某些情况下可能会很糟糕。你可以阅读更多关于这个here的信息。

    【讨论】:

    • 很棒的发现!感谢您的提示
    • 你知道 ISecurityService 存在于哪个包中吗?
    • @bbqchickenrobot - 这是我自己的访问自定义安全模型的服务。就我而言,我将有关我的应用程序权限以及我如何将它们与 Azure AD 实例中的某些组相关联的详细信息存储在数据库中。您可以使用任何您想要的安全模型,并使用声明转换向您的声明主体添加任何类型的声明。
    猜你喜欢
    • 2020-11-11
    • 2019-08-05
    • 1970-01-01
    • 2023-01-21
    • 2021-12-22
    • 2016-11-30
    • 2018-08-24
    • 1970-01-01
    • 2021-03-26
    相关资源
    最近更新 更多