在Azure Active Directory B2C中按组授权

时间:2016-10-28 09:35:30

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

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

[Authorize(Users="Bill")]

然而,这不是很有效,我看到很少的用例。另一种解决方案是通过角色授权。但是由于某些原因,似乎并没有wowrk。例如,如果我向用户提供“全局管理员”角色,请尝试:

[Authorize(Roles="Global Admin")]

它不起作用。有没有办法通过群组或角色进行授权?

7 个答案:

答案 0 :(得分:41)

从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中配置了指定的组,其显示名称为“管理员”。

但是,如果您选择使用自定义声明类型,则需要通过在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

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

答案 1 :(得分:18)

这样可行,但是您在身份验证逻辑中编写了几行代码,以实现您的目标。

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

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

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

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

enter image description here

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

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

最后,您可以在此处对此功能进行投票:https://feedback.azure.com/forums/169401-azure-active-directory/suggestions/10123836-get-user-membership-groups-in-the-claims-with-ad-b,以便获取群组成员资格声明,而无需为此查询图谱API。

答案 2 :(得分:5)

我将其描述为书面形式,但截至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

答案 3 :(得分:2)

有一个官方示例:Azure AD B2C:基于角色的访问控制 available here 来自 Azure AD 团队。

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

答案 4 :(得分:1)

亚历克斯的答案对于找出可行的解决方案至关重要,这要感谢您指出了正确的方向。

但是它使用的jar在Core 2中已经过长时间折旧,而在Core 3(Migrate authentication and Identity to ASP.NET Core 2.0)中已完全删除

我们必须实现的基本任务是使用app.UseOpenIdConnectAuthentication()将事件处理程序附加到OnTokenValidated,该事件处理程序由ADB2C身份验证使用。我们必须做到这一点而不会干扰ADB2C的任何其他配置。

这是我的看法:

OpenIdConnectOptions

所有实现都封装在一个帮助器类中,以保持Startup类的清洁。如果原始事件处理程序不为null(不是btw),则会保存并调用该事件处理程序。

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

您将需要使用以下适当的软件包:

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

捕获::您必须授予应用程序读取AD的权限。自2019年10月起,此应用程序必须是``旧版''应用程序,而不是最新的B2C应用程序。这是一个很好的指南:Azure AD B2C: Use the Azure AD Graph API

答案 5 :(得分:1)

基于这里所有令人惊奇的答案,使用新的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();

答案 6 :(得分:0)

首先,感谢大家的先前答复。我花了一整天的时间来解决这个问题。我正在使用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);
            }
        });
    }