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

时间:2019-07-18 18:33:38

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

我们正在开发一个使用在.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。因此,没有人具有任何个组,因此无法通过授权。可以阅读有关here的无组问题。

这种基于字符串的策略注册,尽管可能看起来很乏味,但这似乎是.Net Core人们所建议的,但是如果未在用户声明中填充组,则该注册将保持平稳。我没有真正看到如何解决这个问题。 是否有一些特殊的方法可以为我的API设置AppRegistration,以便 获得用户声明中填充的所有组?

更新

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

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

2 个答案:

答案 0 :(得分:0)

您可以改为使用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使用策略(推荐)或自定义属性来验证用户组。

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

答案 1 :(得分:0)

由于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的更多信息。