通过 OpenID Connect 从 Azure AD 获取用户的电子邮件地址

Get the user's email address from Azure AD via OpenID Connect

我正在尝试使用他们的 Office 365 帐户对访问我网站的用户进行身份验证,因此我一直在遵循有关使用 OWIN OpenID Connect 中间件添加身份验证的指南,并成功设法对他们的个人资料进行身份验证和检索。

我现在正在尝试获取用户的电子邮件地址(这样我就可以用他们的联系方式填充他们的系统帐户),但我似乎无法收回电子邮件声明。我已尝试使用范围 openid profile email 发出请求,但声明集不包含任何邮件信息。

有没有办法通过 OpenID Connect 端点从 Azure AD 获取用户的电子邮件?

您是否可以通过 &resource=https://graph.windows.net in the sign-in request to the authorization endpoint, then query the Azure AD Graph API for the authenticated organizational user's Office 365 email address? For example, GET https://graph.windows.net/me/mail?api-version=1.5

有关其他参考,请参阅 AzureADSamples GitHub 上的 WebApp-WebAPI-MultiTenant-OpenIdConnect-DotNet 代码示例。

在找到解决方案之前,我为同样的问题苦苦挣扎了几天。回答您的问题:是的,只要您:

,您就应该能够在您的索赔中取回电子邮件地址
  1. 在您的请求中包含 profileemail 范围,并且
  2. 在 Azure 门户 Active Directory 部分配置您的应用程序以在 委派权限.[=75 下包含登录并读取用户配置文件 =]

请注意,电子邮件地址可能不会在 email 声明中返回:在我的情况下(一旦我开始工作)它会在 name 声明中返回。

但是,根本无法取回电子邮件地址可能是由以下问题之一引起的:

没有与 Azure AD 帐户关联的电子邮件地址

根据本 Scopes, permissions, and consent in the Azure Active Directory v2.0 endpoint 指南,即使您包含 email 范围,您也可能无法取回电子邮件地址:

The email claim is included in a token only if an email address is associated with the user account, which is not always the case. If it uses the email scope, your app should be prepared to handle a case in which the email claim does not exist in the token.

如果您收到其他与个人资料相关的声明(例如 given_namefamily_name),这可能就是问题所在。

被中间件丢弃的声明

这就是我的原因。我没有收到 任何 个人资料相关的声明(名字、姓氏、用户名、电子邮件等)。

在我的例子中,身份处理堆栈如下所示:

问题出在 IdentityServer3.AspNetIdentity AspNetIdentityUserService class: InstantiateNewUserFromExternalProviderAsync() method looks like this:

protected virtual Task<TUser> InstantiateNewUserFromExternalProviderAsync(
    string provider,
    string providerId,
    IEnumerable<Claim> claims)
{
    var user = new TUser() { UserName = Guid.NewGuid().ToString("N") };
    return Task.FromResult(user);
}

注意它传入一个声明集合然后忽略它。我的解决方案是创建一个由此派生的 class 并将该方法重写为如下内容:

protected override Task<TUser> InstantiateNewUserFromExternalProviderAsync(
    string provider,
    string providerId,
    IEnumerable<Claim> claims)
{
    var user = new TUser
    {
        UserName = Guid.NewGuid().ToString("N"),
        Claims = claims
    };
    return Task.FromResult(user);
}

我不确切知道您使用的是什么中间件组件,但很容易看到从您的外部提供商返回的原始声明;这至少会告诉您它们恢复正常,并且问题出在您的中间件中。只需将 Notifications 属性 添加到您的 OpenIdConnectAuthenticationOptions 对象,如下所示:

// Configure Azure AD as a provider
var azureAdOptions = new OpenIdConnectAuthenticationOptions
{
    AuthenticationType = Constants.Azure.AuthenticationType,
    Caption = Resources.AzureSignInCaption,
    Scope = Constants.Azure.Scopes,
    ClientId = Config.Azure.ClientId,
    Authority = Constants.Azure.AuthenticationRootUri,
    PostLogoutRedirectUri = Config.Identity.RedirectUri,
    RedirectUri = Config.Azure.PostSignInRedirectUri,
    AuthenticationMode = AuthenticationMode.Passive,
    TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = false
    },
    Notifications = new OpenIdConnectAuthenticationNotifications
    {
        AuthorizationCodeReceived = context =>
        {
            // Log all the claims returned by Azure AD
            var claims = context.AuthenticationTicket.Identity.Claims;
            foreach (var claim in claims)
            {
                Log.Debug("{0} = {1}", claim.Type, claim.Value);
            }
            return null;
        }
    },
    SignInAsAuthenticationType = signInAsType // this MUST come after TokenValidationParameters
};

app.UseOpenIdConnectAuthentication(azureAdOptions);

另见

几天来我一直在为同一个问题苦苦挣扎...我从拥有个人 Microsoft 帐户的用户那里获得了电子邮件地址,但没有获得拥有公司 Microsoft 帐户的用户的电子邮件地址。

对于个人帐户,电子邮件地址会像人们期望的那样在 email 字段中返回。

对于公司帐户,电子邮件地址在 preferred_username 字段中返回。

祝我好运,没有我还没有发现的另一个 Microsoft 变体...

2019 年的更新答案:email 声明是一项可选声明,可能未包含在请求中 (Source)

For managed users (those inside the tenant), it must be requested through this optional claim or, on v2.0 only, with the OpenID scope.

您必须更新 Azure 门户中的清单文件以包含可选声明,如下所示:

"optionalClaims": {
    "idToken": [
        {
            "name": "email",
            "source": null,
            "essential": false,
            "additionalProperties": []
        }
    ],
}

此回答的部分灵感来自 this blog post

在同样的情况下,我得到了一个非常简单的代码,return 所有用户都声称在登录后可以访问(包括电子邮件)

using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;

namespace Controllers
{
    public class BaseController : Controller
    {
        protected string GetCurrentUserIDFromClaims()
        {
            return User.FindFirstValue(ClaimTypes.NameIdentifier);
        }

        protected List<string> AllClaimsFromAzure()
        {
            ClaimsIdentity claimsIdentity = ((ClaimsIdentity)User.Identity);
            return claimsIdentity.Claims.Select(x => x.Value).ToList();
        }

        protected string GetCurrentEmailFromAzureClaims()
        {
            return AllClaimsFromAzure()[3];
        }
    }
}

从用户声明中获取 upn 值:

var userClaims = User.Identity as System.Security.Claims.ClaimsIdentity;
string email = userClaims?.FindFirst(System.Security.Claims.ClaimTypes.Upn)?.Value;