Microsoft Graph API .NET - Able to pull all users (including myself), but not just me

2.9k Views Asked by At

I have reopened this question. Please see my edit at the bottom.

Using the VS 2017 template ASP.NET MVC Web App using Work/School Account Authentication project

I'm getting this error:

The token for accessing the Graph API has expired. Click here to sign-in and get a new access token.

when trying to pull basic information about just myself, and I have no issues when I pull my info along with basically everyone else in my AD.

UserProfileController.cs: (function definitions)

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OpenIdConnect;
using MY_PROJECT.Utilities;
using Microsoft.Graph;

namespace MY_PROJECT.Controllers
{
    [Authorize]
    public class UserProfileController : Controller
    {
        /// <summary>
        /// Get the signed-in user
        /// </summary>
        /// <returns>A single User</returns>
        public async Task<User> GetMe()
        {
            GraphServiceClient graphClient = new GraphServiceClient(new AzureAuthenticationProvider());

            return await graphClient.Me.Request().GetAsync();
        }

        /// <summary>
        /// Get all users in the organization
        /// </summary>
        /// <returns>A list of Users</returns>
        public async Task<List<User>> GetAllUsers()
        {
            List<User> userResult = new List<User>();

            GraphServiceClient graphClient = new GraphServiceClient(new AzureAuthenticationProvider());
            IGraphServiceUsersCollectionPage users = await graphClient.Users.Request().Top(500).GetAsync(); // Hard coded to pull 500 users
            userResult.AddRange(users);

            // Users are returned as pages; keep pulling pages until we run out of them
            while (users.NextPageRequest != null)
            {
                users = await users.NextPageRequest.GetAsync();
                userResult.AddRange(users);
            }

            return userResult;
        }

        public async Task<ActionResult> Index()
        {
            try
            {
                // Get the signed-in user's profile
                User me = await GetMe();

                return View(me);
            }
            catch (AdalException)
            {
                // Return to error page.
                return View("Error");
            }
            // if the above failed, the user needs to explicitly re-authenticate for the app to obtain the required token
            catch (Exception)
            {
                return View("Relogin");
            }
        }

        [Authorize(Roles = "Admin")]
        public async Task<ActionResult> Admin()
        {
            try
            {
                var user = await GetAllUsers();

                return View(user);
            }
            catch (AdalException)
            {
                // Return to error page.
                return View("Error");
            }
            // if the above failed, the user needs to explicitly re-authenticate for the app to obtain the required token
            catch (Exception)
            {
                return View("Relogin");
            }
        }

        public void RefreshSession()
        {
            HttpContext.GetOwinContext().Authentication.Challenge(
                new AuthenticationProperties { RedirectUri = "/UserProfile" },
                OpenIdConnectAuthenticationDefaults.AuthenticationType);
        }
    }
}

AzureAuthenticationProvider.cs (handles the MS Graph API connection)

using System.Configuration;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using MY_PROJECT.Models;
using Microsoft.Graph;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace MY_PROJECT.Utilities
{
    class AzureAuthenticationProvider : IAuthenticationProvider
    {
        private string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
        private string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
        private string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];

        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
            string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;

            // get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
            ClientCredential creds = new ClientCredential(clientId, appKey);
            // initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
            AuthenticationContext authenticationContext = new AuthenticationContext(aadInstance + tenantID, new ADALTokenCache(signedInUserID));
            AuthenticationResult authResult = await authenticationContext.AcquireTokenAsync("https://graph.microsoft.com/", creds);

            request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);
        }
    }
}

Admin.cshtml (where all the users end up)

@using Microsoft.Ajax.Utilities
@using Microsoft.Graph
@model List<User>

@{
    ViewBag.Title = "Admin";
}
<h2>@ViewBag.Title.</h2>

<div class="input-group">
    // search text box
</div>

<table id="userTable" class="table table-bordered table-responsive">
    <thead>
    <tr>
        <th>Name</th>
        <th>Email</th>
    </tr>
    </thead>
    @foreach (var user in Model
        .Where(u => !u.Surname.IsNullOrWhiteSpace()))
    {
        <tr>
            <td>@user.DisplayName</td>
            <td>@user.Mail</td>
        </tr>
    }
</table>

<script>
    // search function
</script>

Here is a photo of Admin showing that I can get my basic info: Admin page

Index.cshtml (where just the signed-in user's info should be, but instead get the error)

@model Microsoft.Graph.User

@{
    ViewBag.Title = "User Profile";
}
<h2>@ViewBag.Title.</h2>

<table class="table table-bordered table-striped">
    <tr>
        <td>Display Name</td>
        <td>@Model.DisplayName</td>
    </tr>
    <tr>
        <td>First Name</td>
        <td>@Model.GivenName</td>
    </tr>
    <tr>
        <td>Last Name</td>
        <td>@Model.Surname</td>
    </tr>
    <tr>
        <td>Job Title</td>
        <td>@Model.JobTitle</td>
    </tr>
</table>

Here is the error I get going to my Index page: Redirect from Index to UserProfile Graph Api Token Error

Here is the only App Permissions in the Azure Portal:

App Permissions

So my GetAllUsers() function works fine, but not my GetMe() function, and I don't know why as it's very similar code.

Any help or ideas?


Edit: Reopening the question

TL;DR: I'm still stuck, please help (preferably with ASP.NET code examples)

I accepted an answer to this question roughly a week ago and have since been trying my best to figure out how to implement, or convert to, "Authorization Code Flow" in my program, and simply put: I haven't figured out how.
I still need help with this. So I'm removing the answered check mark for now.

So rather than reposting basically same question, I'm going to reopen this one and add to it.

What I want my app to do: (So we can know what my goal is and hopefully based off that encourage more suggestions. I want to stress that I'm not asking you to write my program for me, I just want us to be on the same page. If you want to show how though, I would love to see it! A lot of this is new to me.)

  • Have users sign in with their AAD credentials linked to my tenant's AAD, as this is for employees only (so far seems to be working)
  • Be able to pull the signed in user's basic info from AAD (original reason for this post, still can't do without using Microsoft.IdentityModel.Clients.ActiveDirectory aka ADAL)
  • Be able to pull basic info about all users in the AAD (able to do with MS Graph currently)
  • Determine what role a user has based off the Azure manifest associated with the web app (so far I seem to be doing this with ADAL as well)
  • Restrict access to certain pages/content with the [Authorize(Roles = "Admin")] type tags based off a user's assigned manifest roles (Doing this with a mix of ADAL and System.Security.Claims)
  • Be able to assign an AAD user to the app's group and subsequently a manifest defined role (Haven't gotten this far yet)

What I'm still stuck on:

  • I'm still struggling to understand how to perform Authorization Code Flow in a .NET environment, using Microsoft Graph API and Azure AD, so I can perform these two Microsoft Graph Calls, mainly the bottom one:

graphServiceClient.Users.Request().Top(500).GetAsync();
graphServiceClient.Me.Request().GetAsync();

  • A lot of the examples online for using Microsoft Graph and AAD use Implicit Flow which is not what I want, or they end up being explanations of how tokens move back and forth with little to no code examples.
  • Can I do Authentication Flow with ADAL? Because my app will only have Azure AD accounts using it, I should probably stick with ADAL until MSAL is more fleshed out.

I'm fine starting from scratch, I just want to learn how to do this.


Minor Update:

With the help of Nan Yu, I was able to use the Microsoft.Graph graphServiceClient how I wanted when I switched to Easy Auth for authentication.
Check out my other post here: Can I use the Microsoft Graph SDK with Azure AD Easy Auth
Make sure to also read the article by cgillum that is linked at the top that goes over setting up some needed settings in the Azure Portal / Resource website.

1

There are 1 best solutions below

5
On

You're using the Client Credential flow which only authenticates your App. Without a user authenticating, there is no me in context to retrieve data against. If you wish to use me you'll need to switch to the Authorization Code flow.

UPDATE:

You want to use Microsoft Authentication Library (MSAL), it is the most recent library.

Unless you need a specific feature that is only supported in v1, I generally recommend using the Azure AD v2 endpoint. Most of the documentation and examples for Microsoft Graph API assume you're using this endpoint.

The Implicit Flow is almost identical to Authorization Code Flow but carries some limitations. For example, you can't refresh a token retrieved using the Implicit Flow so when your token expires (~1hr) the user will need to re-authenticate. Generally you only use the Implicit Flow when there is no other option such as a Single Page App that needs to authenticate using client-side javascript.

I have some articles that talk about how the v2 Endpoint Flows work that might provide some background:

If you're using ASP.NET then these are examples that leverage MDAL and the v2 endpoint using ASP.NET and the Microsoft Graph .NET Client Library: