Implementing Microsoft Graph requests in a .NET API

Recently I was tasked with implementing role-based security for a .NET API, based on the Active Directory groups the user is a member of. Easy, I thought initially, as there’s a well-documented way of doing this by adding [Authorize] annotations to controller methods.

The first problem I ran into was the API was designed to be used by a Node.js/React application. In this case (and this is a simplified description), users obviously don’t authenticate themselves directly with the API, but instead with the React application, via Active Directory Single Sign-On.

  1. The React application has an Azure App Registration.
  2. When the user accesses the application, it redirects to the Azure Single Sign-On view, and Azure will return a JSON Web Token (JWT) that authenticates the user session with the application.
  3. The React application relays the JWT to the API.

Unfortunately the users’ Active Directory Group IDs aren’t included in the JWT in this case. What I needed to do was extract the user’s identifier from the JWT, and use Microsoft’s Graph API to read the group IDs for that identifier from Active Directory.

A lot of code went into the API to achieve this - reading the group IDs from the configuration file, getting the identifier from the JWT, etc., so I have abbreviated things a little.

Getting .NET Graph Packages Installed

Using NuGet, install the latest version of System.IdentityModel.Tokens.Jwt, and the latest stable versions of and Microsoft.Graph and Microsoft.Azure.ActiveDirectory.GraphClient, in that order. I had all sorts of problems with earlier versions of those packages.

Calling Method

This is one of the base controller methods where the authentication/authorisation is to be applied. The first lines read the user name from the JWT and passes it to GetRequestorGroup() to get a list of IDs for the Active Directory groups the user is a member of.

protected async Task<ActionResult> GetAllAsync()
{
      string userName = User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value;
      var requestorGroup = GetSessionRequestGroup.GetRequestorGroup(userName).Result.ToList();

     var ReadOnlyGroup = GetSessionRequestGroup.GetReadOnlyGroup();
      var ReadWriteGroup = GetSessionRequestGroup.GetReadWriteGroup();

      if (requestorGroup.Any().ToString() == ReadOnlyGroup || requestorGroup.Any().ToString() == ReadWriteGroup)
      {
            ...
      }
      else
      {
            _logger.LogError(userName + " is not a member of an authorised group");
            return BadRequest(new Response { Status = false });
      }
}

Here I’ve placed the controller method logic itself in a basic if-then statement that checks whether the IDs for the ReadOnlyGroup or ReadWriteGroup match any of the IDs returned by GetRequestorGroup().

The method that does all the Microsoft Graph work is GetRequestorGroup().

GetRequestorGroup()

The following is the code I ended up with in my Microsoft Graph lookup method. It does appear quite different to other examples provided by Microsoft and others online, but it’s what I needed to get the Graph client working.

The first several lines reads the TenantID, ClientID and ClientSecret from the appsettings file.

var thisConfig = Utils.GetConfig().AsEnumerable();

var tenantId = thisConfig.Where(o => o.Key == "AzureAd:TenantId").FirstOrDefault().Value.ToString();
var clientId = thisConfig.Where(o => o.Key == "AzureAd:ClientId").FirstOrDefault().Value.ToString();
var clientSecret = thisConfig.Where(o => o.Key == "AzureAd:ClientSecret").FirstOrDefault().Value.ToString();
var ClientScopes = new string[] { "https://graph.microsoft.com/.default" };

The ClientScopes value here appears to be correct. I’ve seen examples that include User.Read, Directory.Read.All and other API permissions being declared as the scope, but they appear to be superfluous in my implementation of the Graph client.

The following several lines sets the authentication parameters, and instantiates GraphServiceClient as ‘graphClient’.

var options = new TokenCredentialOptions
{
      AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};

ClientSecretCredential clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret, options);
GraphServiceClient graphClient = new GraphServiceClient(clientSecretCredential, ClientScopes);

At this point I’ve tried multiple ways of using the client to read user data from Active Directory. It turned out to still not be as straightforward as in the examples provided by Microsoft and elsewhere online. It kept returning RequestBuilder for all the possible data fields, instead of any actual data, so my code obviously wasn’t doing anything with the RequestBuilders. It must be appended by ‘.Result()’.

var graphRequest = graphClient.Users[requestorName].MemberOf.GetAsync().Result;

Because I do expect the group IDs to be returned as a collection or list, I want the method to return this to the base controller as a List:

var userGroups = graphRequest.Value.Select(o => o.Id).AsEnumerable();
List<string> groupMemberships = new List<string>();
foreach (var i in userGroups)
{
      groupMemberships.Add(i.ToString());
}

return groupMemberships;

If the GraphServiceClient is unable to perform the request, it’s pretty good at returning exception messages that indicate whether a credential value is missing or incorrect, and whether the application is authorised to read the data from Active Directory. Other times it’s pretty useless at telling us where the problem might be, as it returns a status message with a null InnerException.

Troubleshooting

In order for a service to get Active Directory information through Microsoft Graph, the following permissions are needed for the App Registration:

  • Directory.Read.All
  • User.Read.All
  • User.ReadBasic.All

The fastest way to test API calls, check permissions and see what data is returned for a given request is to use Microsoft’s Graph Explorer. The ‘Code snippets’ tab above the response window might display code that can be copied and pasted into the requesting method.code can be copied and pasted, and should, in theory, do the same thing in the .NET application.