我正在尝试弄清楚如何在Azure Active Directory B2C中使用组进行授权。我可以通过用户授权,例如:
[Authorize(Users="Bill")]
然而,这不是很有效,我看到很少的用例。另一种解决方案是通过角色授权。但是由于某些原因,似乎并没有wowrk。例如,如果我向用户提供“全局管理员”角色,请尝试:
[Authorize(Roles="Global Admin")]
它不起作用。有没有办法通过群组或角色进行授权?
答案 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)中区分Roles
和Groups
。
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安全组和“角色声明”值后,您将能够使用具有角色功能的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);
}
});
}