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

42.6k views Asked by At

I'm trying to authenticate users to my site with their Office 365 accounts, so I have been following the guidance on using the OWIN OpenID Connect middleware to add authentication and successfully managed to authenticate and retrieve their profile.

I am now trying to get the email address of the user (so I can populate their system account with their contact details), but I can't seem to get an email claim back. I have tried making a request using the scope openid profile email, but the claim-set does not contain any mail information.

Is there a way to get the email of a user from Azure AD via the OpenID Connect endpoint?

7

There are 7 answers

2
Mark Whitaker On BEST ANSWER

I struggled with the same problem for a few days before arriving at a solution. In answer to your question: yes, you should be able to get the e-mail address back in your claims as long as you:

  1. Include the profile or email scope in your request, and
  2. Configure your application in the Azure Portal Active Directory section to include Sign in and read user profile under Delegated Permissions.

Note that the e-mail address may not be returned in an email claim: in my case (once I got it working) it's coming back in a name claim.

However, not getting the e-mail address back at all could be caused by one of the following issues:

No e-mail address associated with the Azure AD account

As per this guide to Scopes, permissions, and consent in the Azure Active Directory v2.0 endpoint, even if you include the email scope you may not get an e-mail address back:

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.

If you're getting other profile-related claims back (like given_name and family_name), this might be the problem.

Claims discarded by middleware

This was the cause for me. I wasn't getting any profile-related claims back (first name, last name, username, e-mail, etc.).

In my case, the identity-handling stack looks like this:

The problem was in the IdentityServer3.AspNetIdentity AspNetIdentityUserService class: the 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);
}

Note it passes in a claims collection then ignores it. My solution was to create a class derived from this and override the method to something like this:

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

I don't know exactly what middleware components you're using, but it's easy to see the raw claims returned from your external provider; that'll at least tell you they're coming back OK and that the problem is somewhere in your middleware. Just add a Notifications property to your OpenIdConnectAuthenticationOptions object, like this:

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

See also

1
Zion Brewer On

Is it an option for you to pass &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

For additional reference, see the WebApp-WebAPI-MultiTenant-OpenIdConnect-DotNet code sample on the AzureADSamples GitHub.

1
minou On

I was struggling with the same issue for days... I was getting the email address from users with personal Microsoft accounts but not for those with company Microsoft accounts.

For personal accounts, the email address is returned in an email field like one would expect.

For company accounts, the email address is returned in a preferred_username field.

Keeping my fingers crossed that there isn't another Microsoft variation that I haven't discovered yet...

0
pau Fer On

in the same situation I ended up with a very simple code that return all the user claims accesible after login (including email)

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];
        }
    }
}
0
Koby Douek On

Get the upn value from the user's claims:

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

so I can populate their system account with their contact details

Looks like you can't reliably obtain an email address from AAD claims.

Here are some of the claims that you may think you can use for this purpose and their documentation (copied from here), emphasis mine.

Claim Description
preferred_username The primary username that represents the user. It could be an email address, phone number, or a generic username without a specified format. Its value is mutable and might change over time. Since it's mutable, this value can't be used to make authorization decisions. It can be used for username hints and in human-readable UI as a username. The profile scope is required to receive this claim. Present only in v2.0 tokens.
name The name claim provides a human-readable value that identifies the subject of the token. The value isn't guaranteed to be unique, it can be changed, and should be used only for display purposes. The profile scope is required to receive this claim.
email Present by default for guest accounts that have an email address. Your app can request the email claim for managed users (from the same tenant as the resource) using the email optional claim. This value isn't guaranteed to be correct and is mutable over time. Never use it for authorization or to save data for a user. If you require an addressable email address in your app, request this data from the user directly by using this claim as a suggestion or prefill in your UX. On the v2.0 endpoint, your app can also request the email OpenID Connect scope - you don't need to request both the optional claim and the scope to get the claim.

So I guess your best bet is to follow the advice of the doc on the email claim and show a registration form to the user, on which the field "Email" is pre-filled with the value of preferred_username or email. The user should confirm that this value actually makes sense.

3
mai On

Updated answer for 2019: email claim is an optional claim that might be not included in the request (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.

You have to update the manifest file in the Azure Portal to include optional claim, like so:

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

This answer was partially inspired by this blog post.