Ryan Taite
Ryan Taite

Reputation: 839

Azure AD - Pull All Users Basic Info into a List

I'm new to all things Azure and I'm working with a ASP.NET MVC 4 template project.
My goal is to pull all the users from Azure AD into an enumerable list that I can then search against later.

Currently, I'm getting either this error:

Server Error in '/' Application
Object reference not set to an instance of an object
...
Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.

Or this one, depending on which .Where(...) clause I comment out:

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

Clicking the link calls this URL:

https://login.microsoftonline.com/<MY TENANT GUID>/oauth2/authorize?client_id=<MY APP ID>&response_mode=form_post&response_type=code+id_token&scope=openid+profile&state=OpenIdConnect.AuthenticationProperties%<Bunch of alphanumeric gibberish>&nonce=<More alphanumeric gibberish>-client-SKU=ID_NET&x-client-ver=1.0.40306.1554

Clicking the link attempts something, but just drops me back on the same page with the same error and doesn't do anything else.

UserProfileController.cs

private ApplicationDbContext db = new ApplicationDbContext();
private string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
private string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private string graphResourceID = "https://graph.windows.net";

public async Task<Collection<IUser>> GetAllUsers()
{
    var userList = new Collection<IUser>();
    try
    {
        string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
        Uri servicePointUri = new Uri(graphResourceID);
        Uri serviceRoot = new Uri(servicePointUri, tenantID);
        ActiveDirectoryClient activeDirectoryClient = new ActiveDirectoryClient(serviceRoot,
            async () => await GetTokenForApplication());

        // use the token for querying the graph to get the user details

        var result = await activeDirectoryClient.Users
            //.Where(u => u.JobTitle.Equals("Cool Dudes"))    // Works fine when uncommented, otherwise gives me a server error
            .ExecuteAsync();

        while (result.MorePagesAvailable)
        {
            userList = userList.Concat(result.CurrentPage.ToList()) as Collection<IUser>;
            await result.GetNextPageAsync();
        }
    }
    catch (Exception e)
    {
        if (Request.QueryString["reauth"] == "True")
        {
            // Send an OpenID Connect sign-on request to get a new set of tokens.
            // If the user still has a valid session with Azure AD, they will not
            //  be prompted for their credentials.
            // The OpenID Connect middleware will return to this controller after
            //  the sign-in response has been handled.
            HttpContext.GetOwinContext()
                .Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationType);
        }

        return userList;
    }

    return userList;
}

public async Task<ActionResult> Admin()
{
    try
    {
        var user = await GetAllUsers();

        return View(user
            //.Where(u => u.JobTitle.Equals("Cool Dudes"))  // When this is uncommented and the one in GetAllUsers is commented out, I get an error saying "The token for accessing the Graph API has expired. Click here to sign-in and get a new access token."
            );
    }
    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);
}

public async Task<string> GetTokenForApplication()
{
    string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
    string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
    string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

    // get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
    ClientCredential clientcred = 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 authenticationResult = await authenticationContext.AcquireTokenSilentAsync(graphResourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
    return authenticationResult.AccessToken;
}

Admin.cshtml

@using Microsoft.Azure.ActiveDirectory.GraphClient
@model IEnumerable<IUser>

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

<table class="table table-bordered table-striped">
    @foreach (var user in Model)
    {
        <tr>
            <td>Display Name</td>
            <td>@user.DisplayName</td>
            <td>Job Title</td>
            <td>@user.JobTitle</td>
        </tr>
    }
</table>

What am I missing here? Is my while loop logic wrong? Am I perhaps using a now outdated way to read this information? Is it a permissions issue?

Edit:

Narrowing it down:

So I think GetAllUsers is not returning data correctly.

Upvotes: 3

Views: 9658

Answers (2)

Ryan Taite
Ryan Taite

Reputation: 839

Based on this blog post by Jonathan Huss, I was able to convert this part of my code from default-in-the-project Azure AD Graph API to the newer Microsoft Graph API

In my Models folder (could probably be placed in a Utility folder or something) I add this code:

AzureAuthenticationProvider.cs

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

namespace <PROJECT_NAME>.Models
{
    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);
        }
    }
}

Back over in UserProfileController.cs we have:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Configuration;
using System.Linq;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;
using System.Threading.Tasks;
using Microsoft.Azure.ActiveDirectory.GraphClient;  // Will eventually be removed
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OpenIdConnect;
using <PROJECT_NAME>.Models;
using Microsoft.Graph;
using User = Microsoft.Graph.User;  // This is only here while I work on removing references to Microsoft.Azure.ActiveDirectory.GraphClient

namespace <PROJECT_NAME>.Controllers
{
    [Authorize]
    public class UserProfileController : Controller
    {
        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(); // The hard coded Top(500) is what allows me to pull all the users, the blog post did this on a param passed in
            userResult.AddRange(users);

            while (users.NextPageRequest != null)
            {
                users = await users.NextPageRequest.GetAsync();
                userResult.AddRange(users);
            }

            return userResult;
        }

        // Return all users from Azure AD as a proof of concept
        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");
            }
        }
    }
}

The RefreshSession and GetTokenForApplication methods in my original post are still there, but is likely going to be replaced by the AzureAuthenticationProvider class as I rework the code

Finally, a small change in Admin.cshtml, I changed

@using Microsoft.Azure.ActiveDirectory.GraphClient
@model IEnumerable<IUser>

to

@using Microsoft.Graph
@model List<User>

Upvotes: 5

Fei Xue
Fei Xue

Reputation: 14649

Based on the error message, it shouldn't relative to the where cause. The issue was caused the token was expired.

And in this scenario, you can using the client credential flow get the app token instead of delegate token since there is no usage of the context of current user. For this flow, you can using the method AcquireTokenAsync(string resource, ClientCredential clientCredential).

Please let us know if it helps.

Upvotes: 0

Related Questions