0

I have a web application and a web API in pure .NET Core (no references to the old stuff). Both sides use Azure Active Directory in order to authorize the users.

In my Web application, I have this:

[Authorize]
public string GetApiData()
{
    // ...

    string userObjectID = (user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
    AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID, session));
    ClientCredential credential = new ClientCredential(ClientId, ClientSecret);
    AuthenticationResult result = await authContext.AcquireTokenAsync(ApiResourceId, credential);


    // Get all the user's permissions
    using (var client = new HttpClient())
    {
        var address = $"{Constants.API_BASE_ADDR}/api/{data}";

        using (var request = new HttpRequestMessage(HttpMethod.Get, address))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
            var response = await client.SendAsync(request);

            // ...
        }
    }
}

public class NaiveSessionCache : TokenCache
{
    private static readonly object FileLock = new object();
    string UserObjectId = string.Empty;
    string CacheId = string.Empty;
    ISession Session = null;

    public NaiveSessionCache(string userId, ISession session)
    {
        UserObjectId = userId;
        CacheId = UserObjectId + "_TokenCache";
        Session = session;
        this.AfterAccess = AfterAccessNotification;
        this.BeforeAccess = BeforeAccessNotification;
        Load();
    }

    public void Load()
    {
        lock (FileLock)
        {
            this.Deserialize(Session.Get(CacheId));
        }
    }

    public void Persist()
    {
        lock (FileLock)
        {
            // reflect changes in the persistent store
            Session.Set(CacheId, this.Serialize());
            // once the write operation took place, restore the HasStateChanged bit to false
            this.HasStateChanged = false;
        }
    }

    // Empties the persistent store.
    public override void Clear()
    {
        base.Clear();
        Session.Remove(CacheId);
    }

    public override void DeleteItem(TokenCacheItem item)
    {
        base.DeleteItem(item);
        Persist();
    }

    // Triggered right before ADAL needs to access the cache.
    // Reload the cache from the persistent store in case it changed since the last access.
    void BeforeAccessNotification(TokenCacheNotificationArgs args)
    {
        Load();
    }

    // Triggered right after ADAL accessed the cache.
    void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        // if the access operation resulted in a cache update
        if (this.HasStateChanged)
        {
            Persist();
        }
    }
}

And on the API side:

[Authorize]
[HttpGet("{data}"))]
public string Get(string data)
{
    // How to get authorized User information?
    // ...
}

Now, all of that works. The challenge I'm facing is being able to get what user is calling the Get method on the API side. The only things I've found that seem somewhat useful is the Bearer token, and what is assigned to userObjectID on the web side.

However, I need, at a minimum, the user's email address, although having access to everything like I do on the web app side would be nice.

Here's what is in the User.Claims enumerable on the API side:

[0]: {aud: https://constco.onmicrosoft.com/API}  
[1]: {iss: https://sts.windows.net/84fa7f[REDACTED]/}  
[2]: {iat: 1483853522}  
[3]: {nbf: 1483853522}  
[4]: {exp: 1483857422}  
[5]: {appid: 7d491[REDACTED]}  
[6]: {appidacr: 1}  
[7]: {e_exp: 10800}  
[8]: {http://schemas.microsoft.com/identity/claims/identityprovider: https://sts.windows.net/84fa7f[REDACTED]/}  
[9]: {http://schemas.microsoft.com/identity/claims/objectidentifier: 22b491[REDACTED]}  
[10]: {http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: 22b491[REDACTED]}  
[11]: {http://schemas.microsoft.com/identity/claims/tenantid: 84fa7f[REDACTED]}  
[12]: {ver: 1.0}  

And doing something like User.Identity.Name just gives null.

So, how can I get which user is calling into the API, using pure .NET Core?

Note: On the API side, I've tried inheriting from Controller and ApiController, and neither provided any better information.

7
  • You need to add the email and/or username to the claim (via scopes). Did you try blogs.msdn.microsoft.com/kaushal/2016/04/01/…? Once the http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name claim was added, User.Identity.Name should return its name and email as user name (if you add email too) Commented Jan 8, 2017 at 14:16
  • Would calling the Graph API be an option if the token lacks the necessary data? Your API could have a delegated permission to call Graph API, and then get the current caller's profile from there. Commented Jan 8, 2017 at 14:42
  • @Tseng, that link looks like it's for an overall setting. I get all the info I need on the web application side, but the web API side doesn't get that same information. Commented Jan 8, 2017 at 14:43
  • @juunas, that's possible. Although I don't really like the idea of having to call into that for every request (due to latency, which I don't know if that would actually be a problem). Commented Jan 8, 2017 at 14:44
  • @David Well, profile info won't be changing that much so you can cache it :) The problem you are having now is that the web app gets an Id token, whereas the API gets an access token. I'm not aware of any way to customize what claims are in the access tokens, other than in Azure AD B2C. Commented Jan 8, 2017 at 14:47

1 Answer 1

2

The only answer I can think of is to call Azure AD Graph API on behalf of the caller to get more info on them. You can find an example of this (albeit using MVC 5) here: https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof/blob/master/TodoListService/Controllers/TodoListController.cs#L120

You can cache the results so you don't need to call to the Graph API every time.

Snippet from the linked file that gets an access token with the on-behalf-of grant and then gets the user's profile from Graph API:

public static async Task<UserProfile> CallGraphAPIOnBehalfOfUser()
{
    UserProfile profile = null;
    string accessToken = null;
    AuthenticationResult result = null;

    //
    // Use ADAL to get a token On Behalf Of the current user.  To do this we will need:
    //      The Resource ID of the service we want to call.
    //      The current user's access token, from the current request's authorization header.
    //      The credentials of this application.
    //      The username (UPN or email) of the user calling the API
    //
    ClientCredential clientCred = new ClientCredential(clientId, appKey);
    var bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext as System.IdentityModel.Tokens.BootstrapContext;
    string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
    string userAccessToken = bootstrapContext.Token;
    UserAssertion userAssertion = new UserAssertion(bootstrapContext.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);

    string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
    string userId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
    AuthenticationContext authContext = new AuthenticationContext(authority, new DbTokenCache(userId));

    // In the case of a transient error, retry once after 1 second, then abandon.
    // Retrying is optional.  It may be better, for your application, to return an error immediately to the user and have the user initiate the retry.
    bool retry = false;
    int retryCount = 0;

    do
    {
        retry = false;
        try
        {
            result = await authContext.AcquireTokenAsync(graphResourceId, clientCred, userAssertion);
            accessToken = result.AccessToken;
        }
        catch (AdalException ex)
        {
            if (ex.ErrorCode == "temporarily_unavailable")
            {
                // Transient error, OK to retry.
                retry = true;
                retryCount++;
                Thread.Sleep(1000);
            }
        }
    } while ((retry == true) && (retryCount < 1));

    if (accessToken == null)
    {
        // An unexpected error occurred.
        return null;
    }

    //
    // Call the Graph API and retrieve the user's profile.
    //
    string requestUrl = String.Format(
        CultureInfo.InvariantCulture,
        graphUserUrl,
        HttpUtility.UrlEncode(tenant));
    HttpClient client = new HttpClient();
    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    HttpResponseMessage response = await client.SendAsync(request);

    //
    // Return the user's profile.
    //
    if (response.IsSuccessStatusCode)
    {
        string responseString = await response.Content.ReadAsStringAsync();
        profile = JsonConvert.DeserializeObject<UserProfile>(responseString);
        return profile;
    }

    // An unexpected error occurred calling the Graph API.  Return a null profile.
    return null;
}

Your web app gets an Id token which contains more info than the access token that your API receives. So this is the best I can think of.

Sign up to request clarification or add additional context in comments.

3 Comments

I've been playing with this, and it seems the example is out of date. It's saying the my resource doesn't exist in the API, and when I try /users/{userID} I get insufficient privileges. Any ideas on that?
That's odd, the me endpoint should work. msdn.microsoft.com/en-us/library/azure/ad/graph/api/…
This is what I get back. I've tried /Tenant/me?api... and, as your link shows, just /me?api... both give: { "odata.error": { "code": "Request_ResourceNotFound", "message": { "lang": "en", "value": "Resource not found for the segment 'me'." } } }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.