在构建授权策略之前获取Azure AD组 [英] Get Azure AD Groups Before Building Authorization Policies

查看:90
本文介绍了在构建授权策略之前获取Azure AD组的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

We're developing an application that uses a back-end built on .Net Core 2.2 Web API. Most of our controllers merely require the [Authorize] attribute with no policy specified. However, some endpoints are going to require the user to be in a particular Azure AD Security Group. For those cases, I implemented policies like this in the Startup.cs file:

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

然后,在需要这种授权的控制器上,我有:

Then, on controllers requiring this type of authorization, I have:

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

问题在于我们的用户都在大量的组中.这将导致Graph API完全不返回任何组声明,而是将一个简单的hasGroups布尔声明设置为true.因此,没有人具有任何个组,因此无法通过授权.可以在此处阅读此无组问题.

The problem is that our users are all in a large number of groups. This causes the Graph API to return no group claims at all, and instead a simple hasGroups boolean claim set to true. Therefore, no one has any groups, and thus cannot pass authorization. This no-groups issue can be read about here.

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

This string-based policy registration, lackluster as it may be, seems to be what the .Net Core people are recommending, yet it falls flat if the groups aren't populated on the User Claims. I'm not really seeing how to circumnavigate the issue. Is there some special way to set up the AppRegistration for my API so that it does get all of the groups populated on the User Claims?

更新:

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

In the solution, I do have a service that calls Graph to get the user's groups. However, I can't figure out how to call it before it's too late. In other words, when the user hits the AuthorizeAttribute on the controller to check for the policy, the user's groups have not yet been populated, so the protected method always blocks them with a 403.

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

My attempt consisted of making a custom base controller for all of my Web API Controllers. Within the base controller's constructor, I'm calling a method that checks the User.Identity (of type ClaimsIdentity) to see if it's been created and authenticated, and, if so, I'm using the ClaimsIdentity.AddClaim(Claim claim) method to populate the user's groups, as retrieved from my Graph call. However, when entering the base controller's constructor, the User.Identity hasn't been set up yet, so the groups don't get populated, as previously described. Somehow, I need the user's groups to be populated before I ever get to constructing the controller.

推荐答案

由于ASP.NET Core团队的某人提供了一些技巧,因此我找到了该解决方案的答案.此解决方案涉及实现IClaimsTransformation(在Microsoft.AspNetCore.Authentication名称空间中).引用我的来源:

I found an answer to this solution thanks to some tips from someone on the ASP.NET Core team. This solution involves implementing an IClaimsTransformation (in the Microsoft.AspNetCore.Authentication namespace). To quote my source:

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

[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 [...]."

因此,我编写了以下实现(请参见代码下方的重要说明):

So I wrote the following implementation (see an important caveat below the code):

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方法中,我这样注册实现:

Within my Startup.cs, in the ConfigureServices method, I register the implementation like this:

services.AddTransient<IClaimsTransformation, AdGroupClaimsTransformer>();

警告

您可能已经注意到,我的实现是为防御目的而编写的,以确保不会在已经执行了该过程的ClaimsPrincipal上第二次运行该转换.这里的潜在问题是对IClaimsTransformation的调用可能会发生多次,并且在某些情况下可能会很糟糕.您可以阅读有关此

You may have noticed that my implementation is written defensively to make sure the transformation will not be run a second time on a ClaimsPrincipal that has already undergone the procedure. The potential issue here is that calls to the IClaimsTransformation might occur multiple times, and that might be bad in some scenarios. You can read more about this here.

这篇关于在构建授权策略之前获取Azure AD组的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆