Tags

, ,

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. I thought it was going to be straightforward, as the process for setting up authentiation and using the [Authorize] annotations on controller methods is well-documented, and I’ve done it a few times before.

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

  • The React application has an Azure App Registration.
  • 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.
  • 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, using a try/catch to determine what a controller method should do if a given group ID is reyurned, etc., so I have abbreviated things a little here.

Getting .NET Graph Packages Installed

Using NuGet, install the latest version of System.IdentityModel.Tokens.Jwt, and the latest stable version of Microsoft.Graph and Microsoft.Azure.ActiveDirectory.GraphClient, in that order. I had all sorts of problems with the beta release of Graph. The *GraphClient* in the latest versions return more data that’s useful in debugging.

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 returned by GetRequestorGroup() match the ReadOnly or ReadWrite variables.

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’s somewhat 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&#8221; };

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, but my first attempts kept returning RequestBuilder, with no internal data objects, for all the possible data fields.

I ended up adding the following code:

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<string>:

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.