【问题标题】:Authorize By Group in Azure Active Directory B2CAzure Active Directory B2C 中的按组授权
【发布时间】:2017-03-11 04:16:26
【问题描述】:

我试图弄清楚如何在 Azure Active Directory B2C 中授权使用组。我可以通过用户授权,例如:

[Authorize(Users="Bill")]

但是,这不是很有效,而且我看到的用例很少。另一种解决方案是通过角色授权。但是由于某种原因,这似乎不起作用。例如,如果我给用户角色“全局管理员”,它就不起作用,然后尝试:

[Authorize(Roles="Global Admin")]

有没有办法通过组或角色进行授权?

【问题讨论】:

    标签: asp.net-mvc azure azure-ad-b2c


    【解决方案1】:

    从 Azure AD 为用户获取组成员身份需要的不仅仅是“几行代码”,所以我想我应该分享一下最终对我有用的东西,以节省其他人几天的麻烦和头撞。

    让我们首先将以下依赖项添加到 project.json:

    "dependencies": {
        ...
        "Microsoft.IdentityModel.Clients.ActiveDirectory": "3.13.8",
        "Microsoft.Azure.ActiveDirectory.GraphClient": "2.0.2"
    }
    

    第一个是必要的,因为我们需要验证我们的应用程序才能访问 AAD Graph API。 第二个是我们将用于查询用户成员资格的 Graph API 客户端库。 不用说,这些版本仅在撰写本文时有效,并且将来可能会发生变化。

    接下来,在 Startup 类的 Configure() 方法中,也许就在我们配置 OpenID Connect 身份验证之前,我们如下创建 Graph API 客户端:

    var authContext = new AuthenticationContext("https://login.microsoftonline.com/<your_directory_name>.onmicrosoft.com");
    var clientCredential = new ClientCredential("<your_b2c_app_id>", "<your_b2c_secret_app_key>");
    const string AAD_GRAPH_URI = "https://graph.windows.net";
    var graphUri = new Uri(AAD_GRAPH_URI);
    var serviceRoot = new Uri(graphUri, "<your_directory_name>.onmicrosoft.com");
    this.aadClient = new ActiveDirectoryClient(serviceRoot, async () => await AcquireGraphAPIAccessToken(AAD_GRAPH_URI, authContext, clientCredential));
    

    警告:请勿对您的应用密钥进行硬编码,而是将其保存在安全的地方。嗯,你已经知道了,对吧? :)

    我们交给AD客户端构造函数的异步AcquireGraphAPIAccessToken()方法会在客户端需要获取认证令牌时根据需要调用。该方法如下所示:

    private async Task<string> AcquireGraphAPIAccessToken(string graphAPIUrl, AuthenticationContext authContext, ClientCredential clientCredential)
    {
        AuthenticationResult result = null;
        var retryCount = 0;
        var retry = false;
    
        do
        {
            retry = false;
            try
            {
                // ADAL includes an in-memory cache, so this will only send a request if the cached token has expired
                result = await authContext.AcquireTokenAsync(graphAPIUrl, clientCredential);
            }
            catch (AdalException ex)
            {
                if (ex.ErrorCode == "temporarily_unavailable")
                {
                    retry = true;
                    retryCount++;
                    await Task.Delay(3000);
                }
            }
        } while (retry && (retryCount < 3));
    
        if (result != null)
        {
            return result.AccessToken;
        }
    
        return null;
    }
    

    请注意,它具有用于处理瞬态条件的内置重试机制,您可能希望根据应用程序的需要对其进行调整。

    现在我们已经处理了应用程序身份验证和 AD 客户端设置,我们可以继续利用 OpenIdConnect 事件来最终使用它。 回到我们通常调用 app.UseOpenIdConnectAuthentication() 并创建 OpenIdConnectOptions 实例的 Configure() 方法,我们为 OnTokenValidated 事件添加一个事件处理程序:

    new OpenIdConnectOptions()
    {
        ...         
        Events = new OpenIdConnectEvents()
        {
            ...
            OnTokenValidated = SecurityTokenValidated
        },
    };
    

    当登录用户的访问令牌已获得、验证并建立用户身份时触发该事件。 (不要与调用 AAD Graph API 所需的应用程序自己的访问令牌混淆!) 它看起来是查询 Graph API 以获取用户组成员资格并将这些组添加到身份的好地方,以附加声明的形式:

    private Task SecurityTokenValidated(TokenValidatedContext context)
    {
        return Task.Run(async () =>
        {
            var oidClaim = context.SecurityToken.Claims.FirstOrDefault(c => c.Type == "oid");
            if (!string.IsNullOrWhiteSpace(oidClaim?.Value))
            {
                var pagedCollection = await this.aadClient.Users.GetByObjectId(oidClaim.Value).MemberOf.ExecuteAsync();
    
                do
                {
                    var directoryObjects = pagedCollection.CurrentPage.ToList();
                    foreach (var directoryObject in directoryObjects)
                    {
                        var group = directoryObject as Group;
                        if (group != null)
                        {
                            ((ClaimsIdentity)context.Ticket.Principal.Identity).AddClaim(new Claim(ClaimTypes.Role, group.DisplayName, ClaimValueTypes.String));
                        }
                    }
                    pagedCollection = pagedCollection.MorePagesAvailable ? await pagedCollection.GetNextPageAsync() : null;
                }
                while (pagedCollection != null);
            }
        });
    }
    

    这里使用的是角色声明类型,但是您可以使用自定义的。

    完成上述操作后,如果您使用 ClaimType.Role,您需要做的就是像这样装饰您的控制器类或方法:

    [Authorize(Role = "Administrators")]
    

    当然,前提是您在 B2C 中配置了一个显示名称为“Administrators”的指定组。

    但是,如果您选择使用自定义声明类型,则需要通过在 ConfigureServices() 方法中添加类似内容来定义基于声明类型的授权策略,例如:

    services.AddAuthorization(options => options.AddPolicy("ADMIN_ONLY", policy => policy.RequireClaim("<your_custom_claim_type>", "Administrators")));
    

    然后装饰一个特权控制器类或方法如下:

    [Authorize(Policy = "ADMIN_ONLY")]
    

    好的,我们完成了吗? - 嗯,不完全是。

    如果您运行应用程序并尝试登录,您会收到来自 Graph API 的异常,声称“权限不足,无法完成操作”。 这可能并不明显,但是当您的应用程序使用其 app_id 和 app_key 成功通过 AD 进行身份验证时,它没有从您的 AD 读取用户详细信息所需的权限。 为了授予应用程序这样的访问权限,我选择使用Azure Active Directory Module for PowerShell

    以下脚本对我有用:

    $tenantGuid = "<your_tenant_GUID>"
    $appID = "<your_app_id>"
    
    $userVal = "<admin_user>@<your_AD>.onmicrosoft.com"
    $pass = "<admin password in clear text>"
    $Creds = New-Object System.Management.Automation.PsCredential($userVal, (ConvertTo-SecureString $pass -AsPlainText -Force))
    
    Connect-MSOLSERVICE -Credential $Creds
    $msSP = Get-MsolServicePrincipal -AppPrincipalId $appID -TenantID $tenantGuid
    
    $objectId = $msSP.ObjectId
    
    Add-MsolRoleMember -RoleName "Company Administrator" -RoleMemberType ServicePrincipal -RoleMemberObjectId $objectId
    

    现在我们终于完成了! “几行代码”怎么样? :)

    【讨论】:

    • 这是一篇出色的文章。谢谢!
    • 如此美丽,如此清晰,非常棒!
    • @ChristerBrannstrom 谢谢! - 我很高兴它帮助了一些人。
    • @AlexLobakov 嘿,我正在尝试您的解决方案并收到“NotSupportedException:不支持指定的方法。HandleSignInAsync”的错误。这是你可以更好地向我解释的事情,以便我可以解决它
    • 有许多移动部分可能会出错,但请查看这篇文章中的“一些常见陷阱”部分:rimdev.io/openid-connect-and-asp-net-core-1-0 看看其中一个是否适用于您的情况。另外,请确保在添加 OIDC 之前添加 cookie 身份验证:app.UseCookieAuthentication(....)
    【解决方案2】:

    这会起作用,但是您必须在身份验证逻辑中编写几行代码才能实现您要查找的内容。

    首先,您必须区分 Azure AD (B2C) 中的 RolesGroups

    User Role 非常具体,仅在 Azure AD (B2C) 本身内有效。角色定义了用户在 Azure AD 中拥有的权限。

    Group(或Security Group)定义用户组成员,可以暴露给外部应用程序。外部应用程序可以在安全组之上模拟基于角色的访问控制。是的,我知道这听起来可能有点令人困惑,但就是这样。

    因此,您的第一步是在 Azure AD B2C 中为您的 Groups 建模 - 您必须创建组并手动将用户分配到这些组。您可以在 Azure 门户 (https://portal.azure.com/) 中执行此操作:

    然后,回到您的应用程序,一旦用户成功通过身份验证,您将不得不编写一些代码并向Azure AD B2C Graph API 询问用户成员资格。您可以使用this sample 获得有关如何获得用户组成员资格的灵感。最好在其中一个 OpenID 通知(即SecurityTokenValidated)中执行此代码,并将用户角色添加到 ClaimsPrincipal。

    将 ClaimsPrincipal 更改为具有 Azure AD 安全组和“角色声明”值后,您将能够将授权属性与角色功能一起使用。这真的是 5-6 行代码。

    最后,您可以为 here 功能投票,以获得群组成员资格,而无需为此查询 Graph API。

    【讨论】:

    • 你能不能展示一下那 5-6 行?几天来,我一直在努力拼凑这个问题的答案,而且我已经编写了超过 100 行代码(而且它还没有工作!)。如果只需 5 或 6 行就可以轻松连接通知、查询图表以获取用户组数据并将组添加到 ClaimsPrincipal 角色,那么我显然是在找错树了。我真的很感激一些重定向!
    • 如何访问“Azure B2C 设置”?我发现没有地方可以将组添加到 Azure B2C 租户,但奇怪的是,我可以将用户添加到组(即使不存在组)。
    • @Donald Airey 它已移至 Azure 门户中的单独条目“组”。
    【解决方案3】:

    我以书面形式实现了这一点,但截至 2017 年 5 月,这条线

    ((ClaimsIdentity)context.Ticket.Principal.Identity).AddClaim(new Claim(ClaimTypes.Role, group.DisplayName, ClaimValueTypes.String));
    

    需要改成

    ((ClaimsIdentity)context.Ticket.Principal.Identity).AddClaim(new Claim(ClaimTypes.Role, group.DisplayName));
    

    使其与最新的库一起工作

    作者辛苦了

    另外,如果您遇到 Connect-MsolService 问题,将错误的用户名和密码更新到最新的 lib

    【讨论】:

    • 现在 Ticket 属性已消失,因此必须将其更改为 ((ClaimsIdentity) context.Principal.Identity
    【解决方案4】:

    Alex 的回答对于找出可行的解决方案至关重要,感谢您指出正确的方向。

    但是它使用了app.UseOpenIdConnectAuthentication(),它在 Core 2 中早已贬值,并在 Core 3 中完全删除 (Migrate authentication and Identity to ASP.NET Core 2.0)

    我们必须实现的基本任务是使用OpenIdConnectOptions 将事件处理程序附加到OnTokenValidated,ADB2C 身份验证在后台使用它。我们必须在不干扰 ADB2C 的任何其他配置的情况下执行此操作。

    这是我的看法:

    // My (and probably everyone's) existing code in Startup:
    services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
            .AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));
    
    // This adds the custom event handler, without interfering any existing functionality:
    services.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme,
    options =>
    {
        options.Events.OnTokenValidated =
            new AzureADB2CHelper(options.Events.OnTokenValidated).OnTokenValidated;
    });
    

    所有实现都封装在一个帮助类中,以保持 Startup 类的清洁。原始事件处理程序被保存并在它不为空时调用(它不是顺便说一句)

    public class AzureADB2CHelper
    {
        private readonly ActiveDirectoryClient _activeDirectoryClient;
        private readonly Func<TokenValidatedContext, Task> _onTokenValidated;
        private const string AadGraphUri = "https://graph.windows.net";
    
    
        public AzureADB2CHelper(Func<TokenValidatedContext, Task> onTokenValidated)
        {
            _onTokenValidated = onTokenValidated;
            _activeDirectoryClient = CreateActiveDirectoryClient();
        }
    
        private ActiveDirectoryClient CreateActiveDirectoryClient()
        {
            // TODO: Refactor secrets to settings
            var authContext = new AuthenticationContext("https://login.microsoftonline.com/<yourdomain, like xxx.onmicrosoft.com>");
            var clientCredential = new ClientCredential("<yourclientcredential>", @"<yourappsecret>");
    
    
            var graphUri = new Uri(AadGraphUri);
            var serviceRoot = new Uri(graphUri, "<yourdomain, like xxx.onmicrosoft.com>");
            return new ActiveDirectoryClient(serviceRoot,
                async () => await AcquireGraphAPIAccessToken(AadGraphUri, authContext, clientCredential));
        }
    
        private async Task<string> AcquireGraphAPIAccessToken(string graphAPIUrl,
            AuthenticationContext authContext,
            ClientCredential clientCredential)
        {
            AuthenticationResult result = null;
            var retryCount = 0;
            var retry = false;
    
            do
            {
                retry = false;
                try
                {
                    // ADAL includes an in-memory cache, so this will only send a request if the cached token has expired
                    result = await authContext.AcquireTokenAsync(graphAPIUrl, clientCredential);
                }
                catch (AdalException ex)
                {
                    if (ex.ErrorCode != "temporarily_unavailable")
                    {
                        continue;
                    }
    
                    retry = true;
                    retryCount++;
                    await Task.Delay(3000);
                }
            } while (retry && retryCount < 3);
    
            return result?.AccessToken;
        }
    
        public Task OnTokenValidated(TokenValidatedContext context)
        {
            _onTokenValidated?.Invoke(context);
            return Task.Run(async () =>
            {
                try
                {
                    var oidClaim = context.SecurityToken.Claims.FirstOrDefault(c => c.Type == "oid");
                    if (!string.IsNullOrWhiteSpace(oidClaim?.Value))
                    {
                        var pagedCollection = await _activeDirectoryClient.Users.GetByObjectId(oidClaim.Value).MemberOf
                            .ExecuteAsync();
    
                        do
                        {
                            var directoryObjects = pagedCollection.CurrentPage.ToList();
                            foreach (var directoryObject in directoryObjects)
                            {
                                if (directoryObject is Group group)
                                {
                                    ((ClaimsIdentity) context.Principal.Identity).AddClaim(new Claim(ClaimTypes.Role,
                                        group.DisplayName, ClaimValueTypes.String));
                                }
                            }
    
                            pagedCollection = pagedCollection.MorePagesAvailable
                                ? await pagedCollection.GetNextPageAsync()
                                : null;
                        } while (pagedCollection != null);
                    }
                }
                catch (Exception e)
                {
                    Debug.WriteLine(e);
                }
            });
        }
    }
    

    您将需要合适的软件包,我正在使用以下软件包:

    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />
    <PackageReference Include="Microsoft.Azure.ActiveDirectory.GraphClient" Version="2.1.1" />
    <PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.3" />
    

    Catch:您必须授予您的应用程序读取 AD 的权限。截至 2019 年 10 月,此应用程序必须是“旧版”应用程序,而不是最新的 B2C 应用程序。这是一个很好的指南:Azure AD B2C: Use the Azure AD Graph API

    【讨论】:

      【解决方案5】:

      根据此处所有令人惊叹的答案,使用新的 Microsoft Graph API 获取用户组

      
      IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
                .Create("application-id")
                .WithTenantId("tenant-id")
                .WithClientSecret("xxxxxxxxx")
                .Build();
      
      ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
      
      GraphServiceClient graphClient = new GraphServiceClient(authProvider);
      
      
      var groups = await graphClient.Users[oid].MemberOf.Request().GetAsync();
      

      【讨论】:

      • ClientCredentialProvider 似乎不存在于 .net core 5 中
      • 必须安装包 Install-Package Microsoft.Graph Install-Package Microsoft.Graph.Auth -IncludePrerelease
      【解决方案6】:

      有官方示例:Azure AD B2C: Role-Based Access Control available here 来自 Azure AD 团队。

      但是,是的,唯一的解决方案似乎是通过在 MS Graph 的帮助下读取用户组的自定义实现。

      【讨论】:

        【解决方案7】:

        首先感谢大家之前的回复。我花了一整天的时间来完成这项工作。我正在使用 ASPNET Core 3.1,在使用之前响应中的解决方案时出现以下错误:

        secure binary serialization is not supported on this platform
        

        我已替换为 REST API 查询并且能够获取组:

            public Task OnTokenValidated(TokenValidatedContext context)
            {
                _onTokenValidated?.Invoke(context);
                return Task.Run(async () =>
                {
                    try
                    {
                        var oidClaim = context.SecurityToken.Claims.FirstOrDefault(c => c.Type == "oid");
                        if (!string.IsNullOrWhiteSpace(oidClaim?.Value))
                        {
                            HttpClient http = new HttpClient();
        
                            var domainName = _azureADSettings.Domain;
                            var authContext = new AuthenticationContext($"https://login.microsoftonline.com/{domainName}");
                            var clientCredential = new ClientCredential(_azureADSettings.ApplicationClientId, _azureADSettings.ApplicationSecret);
                            var accessToken = AcquireGraphAPIAccessToken(AadGraphUri, authContext, clientCredential).Result;
        
                            var url = $"https://graph.windows.net/{domainName}/users/" + oidClaim?.Value + "/$links/memberOf?api-version=1.6";
        
                            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
                            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                            HttpResponseMessage response = await http.SendAsync(request);
        
                            dynamic json = JsonConvert.DeserializeObject<dynamic>(await response.Content.ReadAsStringAsync());
        
                            foreach(var group in json.value)
                            {
                                dynamic x = group.url.ToString();
        
                                request = new HttpRequestMessage(HttpMethod.Get, x + "?api-version=1.6");
                                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                                response = await http.SendAsync(request);
        
                                dynamic json2 = JsonConvert.DeserializeObject<dynamic>(await response.Content.ReadAsStringAsync());
        
                                ((ClaimsIdentity)((ClaimsIdentity)context.Principal.Identity)).AddClaim(new Claim(ClaimTypes.Role.ToString(), json2.displayName.ToString()));
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        Debug.WriteLine(e);
                    }
                });
            }
        

        【讨论】:

          猜你喜欢
          • 2023-03-25
          • 1970-01-01
          • 1970-01-01
          • 2018-03-08
          • 2021-05-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多