从 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
现在我们终于完成了!
“几行代码”怎么样? :)