在构建授权策略之前获取 Azure AD 组

Get Azure AD Groups Before Building Authorization Policies

我们正在开发一个使用基于 .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() {}

问题是我们的用户都在大量的群里。这导致图表 API 到 return 根本没有组声明,而是一个简单的 hasGroups 布尔声明设置为 true。因此,没有人任何个群组,因此无法通过授权。关于 .

可以阅读此无团体问题

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

更新:

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

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

您可以使用 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) 以访问您的 back-end Web API。
  2. 您的 Web API 应用程序将获取访问令牌 (B) 以访问 Microsoft Graph API,访问令牌 (A) 使用 OAuth 2.0 On-Behalf-Of flow。访问令牌 (B) 将用于获取用户的组。
  3. Web API 使用策略(推荐)或自定义属性验证用户组。

此处列出的协议图和示例请求article using the Azure AD V2.0 Endpoint. This article is for the V1.0 endpoint. Here是.Net Core 的代码示例。

感谢 ASP.NET 核心团队成员的一些提示,我找到了该解决方案的答案。此解决方案涉及实施 IClaimsTransformation(在 Microsoft.AspNetCore.Authentication 命名空间中)。引用我的消息来源:

[IClaimsTransformation] is a service you wire into the request pipeline which will run after every authentication and you can use it to augment the identity as you like. That would be where you’d do your Graph API call [...]."

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

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