Integrate Microsoft Graph With .NET CORE Web APIs

Hello All!,
 
I started working on .NET Core just recently. My first ever task for client was to write some web APIs to communicate with the Azure AD and fetch all the users and groups from it. I was naive to .NET Core and had just heard about the active directory thing, but never worked on it, so I planned to go by the traditional process!
 
Following the said process, I started studying Microsoft Graph APIs (obviously from the documents provided by Microsoft) and side by side, read some blogs and posts on how to implement them in our application. I also referred some boiler plates and sample codes to get my job started.

What I found was, many of them had some UI part associated with them for authentication process. i.e. A login screen pops in where you need to type your Microsoft account id and password and submit the form. This certainly was not required at my end, reason being, I was working on web APIs and this authentication process was supposed to be handled somehow at the back end itself.
 
Thus, I had to collect all the bits and pieces and try to integrate them down in a single project. So, I thought that I should document all my tasks and share with others believing that it might save your time and reduce your efforts had you been working on similar thing. Also, being a newbie in writing, I am trying to put down my understandings in a fashion, as simple as it can be, so as to help you understand better. Feel free to correct me wherever needed and pardon me if I am not very clear on some concepts or in case I miss something out.
 
Whoosh! Finally done with the description and now we can get start working on the actual thing why we are here.
 
Prerequisites
  1. You need to have a registered application on Azure Active Directory. Note down the Tenant Id, Application Id and Secret Key for the registered application. (You can refer this for guide and walkthrough )
  2. Visual Studio Professional 2017. You can also opt for other versions of VS / Visual Studio Code as per your convenience.

Actual Work

 
Note
If you are familiar with creating projects in Visual Studio, you can directly jump to step number 2.
 
Step 1
 
Create a new project in Visual Studio.
 
Go to File -> New -> Project.
 
From the templates, under Visual C# section, select .NET Core.
 
Select ASP.NET Core Web Application project.
 
Give the desired project name and location and click OK.
 
From the new window, select ASP.NET Core version 2.1 and API template. Click OK.
 
Now, your new project will load in the solution explorer with a default ValuesController under controller folder. (You can delete this controller as and when required, for now, let us add a new controller and get started).
 
Go to appsettings.json and add a new key named AzureAD which will have all the values that we need. These values would be those that we noted while registering our application on Azure AD (mentioned in prerequisites (1)).
 
After adding this code, your appsetting.json would look as follows,
  1. {
  2.    "Logging":
  3.    {"LogLevel":
  4.    {"Default": "Warning"}},
  5.    "AllowedHosts": "*",
  6.    "AzureAD": {
  7.                "ClientId": "Your Application Id goes here",
  8.                "ClientSecret": "Your Secret Key goes here",
  9.                "TenantId": "Your Tenant Id goes here",
  10.                "Instance": "https://login.microsoftonline.com/",
  11.                "GraphResource": "https://graph.microsoft.com/",
  12.                "GraphResourceEndPoint": "v1.0"
  13.    }
  14. }
Step 2 - Creating Models.
 
Right click on the project name in solution explorer,
 
Select Add -> New Folder. Name it as Models.
 
Right click the Models folder, select Add -> Class.
 
We would be adding 3 class files, namely AzureAD.cs, Group.cs and User.cs
 
The classes are as follows,
 
AzureAD.cs
  1. public class AzureAD
  2. {
  3.    public string ClientId { get; set; }
  4.    public string ClientSecret { get; set; }
  5.    public string TenantId { get; set; }
  6.    public string Instance { get; set; }
  7.    public string GraphResource { get; set; }
  8.    public string GraphResourceEndPoint { get; set; }
  9. }
Group.cs
  1. public class Group
  2. {
  3.    public string id { get; set; }
  4.    public string displayName { get; set; }
  5. }
  6. public class Groups
  7. {
  8.    public int itemsPerPage { get; set; }
  9.    public int startIndex { get; set; }
  10.    public int totalResults { get; set; }
  11.    public List resources { get; set; }
  12. }
User.cs
  1. public class User
  2. {
  3.    public string id { get; set; }
  4.    public string givenName { get; set; }
  5.    public string surname { get; set; }
  6.    public string userPrincipalName { get; set; }
  7.    public string email { get; set; }
  8. }
  9. public class Users
  10. {
  11.    public int itemsPerPage { get; set; }
  12.    public int startIndex { get; set; }
  13.    public int totalResults { get; set; }
  14.    public List resources { get; set; }
  15. }
The AzureAD models class would be used while getting values from out config file (from appsettings.json) and the other 2 classes , as their names suggest, would be used while actually retrieving data from the AD. Our models are ready. Lets head to the next step.
 
Step 3 - Adding a Controller
 
Right click the Controllers folder,
 
select Add -> Controller. Give name as MyDirectory (MyDirectoryController is the desired name) and click Add. 
 
Add the following code to MyDirectoryController.cs
  1. using System.Collections.Generic;
  2. using System.Threading.Tasks;
  3. using Microsoft.AspNetCore.Mvc;
  4. using WebApiWithGraph.Models;
  5. using WebApiWithGraph.Services;
  6. using Microsoft.Graph;
  7. using System.Net;
  8. namespace WebApiWithGraph.Controllers{
  9. [Produces("application/json")]
  10. [Route("directory")]
  11. public class MyDirectoryController : Controller
  12. {
  13. internal static class RouteNames
  14. {
  15.    public const string Users = nameof(Users);
  16.    public const string UserById = nameof(UserById);
  17.    public const string Groups = nameof(Groups);
  18.    public const string GroupById = nameof(GroupById);
  19. }
  20. [HttpGet("users/{id}", Name = RouteNames.UserById)]
  21. public async Task GetUser(string id)
  22. {
  23.    Models.User objUser = new Models.User();
  24.    try
  25.    {
  26.       if(string.IsNullOrEmpty(id) || string.IsNullOrWhiteSpace(id)
  27.       {
  28.          return BadRequest();
  29.       }
  30.       // Initialize the GraphServiceClient.
  31.       GraphServiceClient client = await MicrosoftGraphClient.GetGraphServiceClient();
  32.       // Load user profile.
  33.       var user = await client.Users[id].Request().GetAsync();
  34.       // Copy Microsoft-Graph User to DTO User
  35.       objUser = CopyHandler.UserProperty(user);
  36.       return Ok(objUser);
  37.    }
  38.    catch (ServiceException ex)
  39.    {
  40.       if (ex.StatusCode == HttpStatusCode.BadRequest)
  41.       {
  42.          return BadRequest();
  43.       }
  44.       else
  45.       {
  46.          return NotFound();
  47.       }
  48.    }
  49. }
  50. [HttpGet("users/")]
  51. public async Task GetUsers()
  52. {
  53.    Users users = new Users();
  54.    try
  55.    {
  56.    users.resources = new List();
  57.    // Initialize the GraphServiceClient.
  58.    GraphServiceClient client = await MicrosoftGraphClient.GetGraphServiceClient();
  59.    // Load users profiles.
  60.    var userList = await client.Users.Request().GetAsync();
  61.    // Copy Microsoft User to DTO User
  62.    foreach (var user in userList)
  63.    {
  64.       var objUser = CopyHandler.UserProperty(user);
  65.       users.resources.Add(objUser);
  66.    }
  67.    users.totalResults = users.resources.Count;
  68.    return Ok(users);
  69.    }
  70.    catch (ServiceException ex)
  71.    {
  72.       if (ex.StatusCode == HttpStatusCode.BadRequest)
  73.       {
  74.          return BadRequest();
  75.       }
  76.       else
  77.       {
  78.          return NotFound();
  79.       }
  80.    }
  81. }
  82. [HttpGet("groups/{id}", Name = RouteNames.GroupById)]
  83. public async Task GetGroup(string id)
  84. {
  85.    Models.Group objGroup = new Models.Group();
  86.    try
  87.    {
  88.    // Initialize the GraphServiceClient.
  89.    GraphServiceClient client = await MicrosoftGraphClient.GetGraphServiceClient();
  90.    // Load group profile.
  91.    var group = await client.Groups[id].Request().GetAsync();
  92.    // Copy Microsoft-Graph Group to DTO Group
  93.    objGroup = CopyHandler.GroupProperty(group);
  94.    return Ok(objGroup);
  95.    }
  96.    catch (ServiceException ex)
  97.    {
  98.       if (ex.StatusCode == HttpStatusCode.BadRequest)
  99.       {
  100.          return BadRequest();
  101.       }
  102.       else
  103.       {
  104.          return NotFound();
  105.       }
  106.    }
  107. }
  108. [HttpGet("groups/")]
  109. public async Task GetGroups()
  110. {
  111.    Groups groups = new Groups();
  112.    try
  113.    {
  114.       groups.resources = new List();
  115.       // Initialize the GraphServiceClient.
  116.       GraphServiceClient client = await MicrosoftGraphClient.GetGraphServiceClient();
  117.       // Load groups profiles.
  118.       var groupList = await client.Groups.Request().GetAsync();
  119.       // Copy Microsoft-Graph Group to DTO Group
  120.       foreach (var group in groupList)
  121.       {
  122.          var objGroup = CopyHandler.GroupProperty(group);
  123.          groups.resources.Add(objGroup);
  124.       }
  125.       groups.totalResults = groups.resources.Count;
  126.       return Ok(groups);
  127.    }
  128.    catch (ServiceException ex)
  129.    {
  130.       if (ex.StatusCode == HttpStatusCode.BadRequest)
  131.       {
  132.          return BadRequest();
  133.       }
  134.       else
  135.       {
  136.          return NotFound();
  137.       }
  138.    }
  139. }
  140. }
  141. }
Now, we have implemented 4 methods. These methods would serve the purpose exactly how their names suggest i.e. get user by id, get all users from the AD, get group by id and get all groups from the AD respectively. 
The 2 things that are missing now are MicrosoftGraphClient and CopyHandler. Head over to next steps for the same.
 
Step 4 - Adding MicrosoftGraphClient
 
Add a new folder in our project as we did earlier and name it as Services. Add a new class to this folder which would be MicrosoftGraphClient.cs
 
The following code goes there,
  1. using WebApiWithGraph.Models;
  2. using Microsoft.Extensions.Configuration;
  3. using Microsoft.Graph;
  4. using Microsoft.IdentityModel.Clients.ActiveDirectory;
  5. using System;
  6. using System.Linq;
  7. using System.Net.Http.Headers;
  8. using System.Threading.Tasks;
  9. namespace WebApiWithGraph.Services{
  10. public static class MicrosoftGraphClient
  11. {
  12.    private static GraphServiceClient graphClient;
  13.    private static IConfiguration configuration;
  14.    private static string clientId;
  15.    private static string clientSecret;
  16.    private static string tenantId;
  17.    private static string aadInstance;
  18.    private static string graphResource;
  19.    private static string graphAPIEndpoint;
  20.    private static string authority;
  21. static MicrosoftGraphClient()
  22. {
  23.    configuration = new ConfigurationBuilder().SetBasePath(System.IO.Directory.GetCurrentDirectory())
  24.                                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
  25.                                .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
  26.                                .AddEnvironmentVariables()
  27.                                .Build();
  28.    SetAzureADOptions();
  29. }
  30. private static void SetAzureADOptions()
  31. {
  32.    var azureOptions = new AzureAD();
  33.    configuration.Bind("AzureAD", azureOptions);
  34.    clientId = azureOptions.ClientId;
  35.    clientSecret = azureOptions.ClientSecret;
  36.    tenantId = azureOptions.TenantId;
  37.    aadInstance = azureOptions.Instance;
  38.    graphResource = azureOptions.GraphResource;
  39.    graphAPIEndpoint = $"{azureOptions.GraphResource}{azureOptions.GraphResourceEndPoint}";
  40.    authority = $"{aadInstance}{tenantId}";
  41. }
  42. public static async Task GetGraphServiceClient()
  43. {
  44.    // Get Access Token and Microsoft Graph Client using access token and microsoft graph v1.0 endpoint
  45.    var delegateAuthProvider = await GetAuthProvider();
  46.    // Initializing the GraphServiceClient
  47.    graphClient = new GraphServiceClient(graphAPIEndpoint, delegateAuthProvider);
  48.    return graphClient;
  49. }
  50. private static async Task GetAuthProvider()
  51. {
  52.    AuthenticationContext authenticationContext = new AuthenticationContext(authority);
  53.    ClientCredential clientCred = new ClientCredential(clientId, clientSecret);
  54.    // ADAL includes an in memory cache, so this call will only send a message to the server if the cached token is expired.
  55.    AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(graphResource, clientCred);
  56.    var token = authenticationResult.AccessToken;
  57.    var delegateAuthProvider = new DelegateAuthenticationProvider((requestMessage) =>{
  58.                                                                   requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token.ToString()); 
  59.                                                                   return Task.FromResult(0);
  60.                                                                   });
  61.    return delegateAuthProvider;
  62. }
  63. }
  64. }
Here, we have 3 methods : SetAzureADOptions, GetGraphServiceClient and GetAuthProvider. You can see that in the constructor of the MicrosoftGraphClient, we have built the configuration.
 
The SetAzureADOptions method will read the config for us and get the required details that we have stored in appsettings.json in Step (1).
 
The GetGraphServiceClient method will call GetAuthProvider method internally and return that value along with graphAPIEndpoint (which is constructed in SetAzureADOptions method ).
 
The GetAuthProvider method would take our client id and secret and authenticate these credentials with the graph resource (i.e. https://graph.microsoft.com/) and get a token for us. This token is the bearer token that is required to pass in the header of all the requests made to the Microsoft Graph API.
 
Thus, in a nutshell, the MicrosoftGraphClient authenticates our credentials with the graph resource and returns a client to the controller. Now, in the controller, we can request and retrieve the desired data from this client.Now, we are almost at the end of our work.
 
You can head to the last step now i.e. Step 5.
 
Step 5 - Adding CopyHandler
 
This step can be said to be optional. This handler is used just for type conversion (i.e. to convert the Microsoft.Graph.User to our defined class Models .User, and, to convert the Microsoft.Graph.Group to our defined class Models.Group). So, you can either use this handler, or you can also handle the type casting then and there in the controller as per your convenience. Add a new class to the Services folder and name it CopyHandler. The following code goes in CopyHandler.cs
  1. using WebApiWithGraph.Models;
  2. namespace WebApiWithGraph.Services
  3. {
  4. public class CopyHandler
  5. {
  6.    public static User UserProperty(Microsoft.Graph.User graphUser)
  7.    {
  8.       User user = new User();
  9.       user.id = graphUser.Id;
  10.       user.givenName = graphUser.GivenName;
  11.       user.surname = graphUser.Surname;
  12.       user.userPrincipalName = graphUser.UserPrincipalName;
  13.       user.email = graphUser.Mail;
  14.       return user;
  15.    }
  16.    public static Group GroupProperty(Microsoft.Graph.Group graphGroup)
  17.    {
  18.       Group group = new Group();
  19.       group.id = graphGroup.Id;
  20.       group.displayName = graphGroup.DisplayName;
  21.       return group;
  22.    }
  23. }
  24. }
Running the application
 
Now, our coding part is done and we can go ahead and check it's working. This can be done with tools like swagger, advanced rest client, postman, etc, just as we test the APIs for the data. We can also see the output in the browser (I used Chrome) by just changing the URLs.Run the application through Visual Studio using IIS Express in Chrome. This will open a new window of Google Chrome with the URL somewhat like,
 
https://localhost:44385/api/values
 
Change the URL to,
 
https://localhost:44385/directory/users
 
and hit enter.
 
You should see the list of users from your Azure AD in JSON format. Grab and copy anyone user id from the above mentioned data and now change the URL to,
 
https://localhost:44385/directory/users/123abc-12ab-12ab-12ab-12ab12ab
 
and hit enter.
 
You should see the JSON data for that single user!
 
Repeat the same for getting groups and group by id. The URLs would be,
 
https://localhost:44385/directory/groups
 
and
 
https://localhost:44385/directory/groups/123abc-12ab-12ab-12ab-12ab12ab
 
respectively.
 

Conclusion

 
So, here I think I should conclude this topic.
 
Hope this post will help you in case you face the same situation that I did. If I have missed anything, let me know in the comments and I'll add it in! You can get the sample application here
 
Cheers!