Microsoft Graph API

Microsoft Graph API is a RESTful API that lets developers connect to a wide range of Microsoft 365 services, such as Outlook, OneDrive, Teams, and Azure Entra tenant.

With a single endpoint https://graph.microsoft.com one can easily access and manage data like user profiles, emails, calendars, files, and more. It’s like a gateway to all the important information in Microsoft cloud services. Whether you need to retrieve user details, send emails, or manage documents, Graph API makes it simple by offering a consistent and easy-to-use interface.

By using Graph API you can automate tasks, build apps and create workflows that integrate smoothly with Microsoft’s cloud ecosystem while ensuring your data remains secure through Azure Entra tenant.

In this article, I will demonstrate ways to leverage Microsoft Graph API to fetch service principal and user details on the Entra account. We will see how to authenticate securely using Azure Entra, query the necessary data, handle API responses effectively and be able to retrieve detailed information about users and service principals such as their roles, permissions and attributes from the Entra tenant.

In an upcoming article I will demonstrate how to this gathered information is relevant with Microsoft Fabric.

You can watch the video on the code walkthrough here.

SetUp

To get started create a new service principal and grant ServicePrincipalEndpoint.ReadWrite.All and User.ReadWrite.All delegated permissions. Ensure that you have the Admin consent for these permissions.

I created a service principal named Graph API and granted the required permissions.

Graph API

Add the following Nuget packages to your console application

dotnet add package Azure.Core --version 1.31.0
dotnet add package Azure.Identity --version 1.9.0
dotnet add package Microsoft.Extensions.Configuration --version 6.0.0
dotnet add package Microsoft.Graph --version 5.14.0
dotnet add package Microsoft.Identity.Client --version 4.41.0
dotnet add package System.IdentityModel.Tokens.Jwt --version 6.22.0

Note: I will override the clientcredetials class of the Client Credentials flow which by default is required for the GraphServiceClient. By doing so I wont have to store the client secret anywhere in the application.

For more details please refer to an article on the topic here.

Code

In a new console application add the following references

using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Graph;
using Microsoft.Identity.Client;
using System.IdentityModel.Tokens.Jwt;

Declare a bunch of variables

  private static string clientId = "";
  private static string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
  private static string Authority = "https://login.microsoftonline.com/organizations";
  private static string RedirectURI = "http://localhost";
  private static string access_token = "";
  private static GraphServiceClient graph_Service_Client;

Class to override ClientSecretCredentials class. Reference.

    public class AccessTokenCredential : ClientSecretCredential
    {

        public AccessTokenCredential(string accessToken)
        {
            AccessToken = accessToken;
        }

        private string AccessToken;

        public AccessToken FetchAccessToken()
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

        public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
        {
            return new ValueTask<AccessToken>(FetchAccessToken());
        }

        public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

    }

In local.settings.json add the following key ClientId. Note that we dont need the clientsecret value in the settings.

{
  "AllowedHosts": "*",
  "ClientId": "Service Principal Client Id"  
}

Method to read the config file

public static void ReadConfig()
{
    var builder = new ConfigurationBuilder()
    .AddJsonFile($"local.settings.json", true, true);
    var config = builder.Build();
    clientId = config["ClientId"];
}

Method to authenticate and generate a bearer token

    public async static Task<AuthenticationResult> ReturnAuthenticationResult()
    {
        string AccessToken;
        PublicClientApplicationBuilder PublicClientAppBuilder =
            PublicClientApplicationBuilder.Create(clientId)
            .WithAuthority(Authority)
            .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
            .WithRedirectUri(RedirectURI);

        IPublicClientApplication PublicClientApplication = PublicClientAppBuilder.Build();
        var accounts = await PublicClientApplication.GetAccountsAsync();
        AuthenticationResult result;
        try
        {
            result = await PublicClientApplication.AcquireTokenSilent(scopes, accounts.First())
                             .ExecuteAsync()
                             .ConfigureAwait(false);
        }
        catch
        {
            result = await PublicClientApplication.AcquireTokenInteractive(scopes)
                             .ExecuteAsync()
                             .ConfigureAwait(false);
        }
        access_token = result.AccessToken;
        return result;
    }

}

Method to return a list of Entra users

 public static async Task<IDictionary<string, object>> GetUserDetails(string filter)
 {
     AuthenticationResult authresult = await ReturnAuthenticationResult();
     AccessTokenCredential tokenCredential = new AccessTokenCredential(authresult.AccessToken);
     graph_Service_Client = new GraphServiceClient(tokenCredential, scopes);

     Microsoft.Graph.Models.UserCollectionResponse result =
         await graph_Service_Client.Users.GetAsync((requestConfiguration) => 
         requestConfiguration.QueryParameters.Top = 999);

     IDictionary<string, object> dictionary = new Dictionary<string, object>();
     int i = 0;
     foreach (var str in result.Value)
     {
         dictionary.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
         i++;
     }

     if (filter == "All") { return dictionary.ToDictionary(); } else { return dictionary.Where(kvp => kvp.Key.Contains(filter)).ToDictionary(); }
     return null;
 }

Note : By default, GraphServiceClient will return 100 items per request. This number can be extrapolated to 999 by using the top parameter seen below.

 Microsoft.Graph.Models.UserCollectionResponse result =
         await graph_Service_Client.Users.GetAsync((requestConfiguration) => 
         requestConfiguration.QueryParameters.Top = 999);

but beyond that you will have to use odata.nextlink property.

Please refer to this link for more details and the sample code.

Method to return a list of Service Principal

 public static async Task<IDictionary<string, object>> GetServicePrincipalDetails(string filter)
 {

     AuthenticationResult authresult = await ReturnAuthenticationResult();
     AccessTokenCredential tokenCredential = new AccessTokenCredential(authresult.AccessToken);
     graph_Service_Client = new GraphServiceClient(tokenCredential, scopes);

     Microsoft.Graph.Models.ServicePrincipalCollectionResponse result = await graph_Service_Client.ServicePrincipals.GetAsync((requestConfiguration) =>
     {
         requestConfiguration.QueryParameters.Filter = "tags/any(t:t eq 'WindowsAzureActiveDirectoryIntegratedApp')";
         requestConfiguration.QueryParameters.Top = 999;
     });

     IDictionary<string, object> dictionary = new Dictionary<string, object>();
     int i = 0;
     foreach (var str in result.Value)
     {
         dictionary.Add(result.Value[i].AppDisplayName.ToString(), result.Value[i]);
         i++;
     }

     if (filter == "All") { return dictionary.ToDictionary(); } else { dictionary.Where(kvp => kvp.Key.Contains(filter)).ToDictionary(); }
     return null;
 }

Notice the filter tags/any(t:t eq 'WindowsAzureActiveDirectoryIntegratedApp') in the code above. This filter returns only a list of those service principals that were not system generated. If we remove the filter it will return a list of all the service principals.

Complete Code

using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Graph;
using Microsoft.Identity.Client;
using System.IdentityModel.Tokens.Jwt;

namespace GraphAPI
{
    internal class Program
    {
        private static string clientId = "";
        private static string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
        private static string Authority = "https://login.microsoftonline.com/organizations";
        private static string RedirectURI = "http://localhost";
        private static string access_token = "";
        private static GraphServiceClient graph_Service_Client;

        static async Task Main(string[] args)
        {

            ReadConfig();
            var principaldetails = await GetServicePrincipalDetails("All");
            var userdetails = await GetUserDetails("All");

        }
        public static void ReadConfig()
        {
            var builder = new ConfigurationBuilder()
            .AddJsonFile($"local.settings.json", true, true);
            var config = builder.Build();
            clientId = config["ClientId"];
        }

        public static async Task<IDictionary<string, object>> GetServicePrincipalDetails(string filter)
        {
            AuthenticationResult authresult = await ReturnAuthenticationResult();
            AccessTokenCredential tokenCredential = new AccessTokenCredential(authresult.AccessToken);
            graph_Service_Client = new GraphServiceClient(tokenCredential, scopes);

            Microsoft.Graph.Models.ServicePrincipalCollectionResponse result = await graph_Service_Client.ServicePrincipals.GetAsync((requestConfiguration) =>
            {
                requestConfiguration.QueryParameters.Filter = "tags/any(t:t eq 'WindowsAzureActiveDirectoryIntegratedApp')";
                requestConfiguration.QueryParameters.Top = 999;
            });

            IDictionary<string, object> dictionary = new Dictionary<string, object>();
            int i = 0;
            foreach (var str in result.Value)
            {
                dictionary.Add(result.Value[i].AppDisplayName.ToString(), result.Value[i]);
                i++;

            }

            if (filter == "All") { return dictionary.ToDictionary(); } else { dictionary.Where(kvp => kvp.Key.Contains(filter)).ToDictionary(); }
            return null;
        }

        public static async Task<IDictionary<string, object>> GetUserDetails(string filter)
        {

            AuthenticationResult authresult = await ReturnAuthenticationResult();
            AccessTokenCredential tokenCredential = new AccessTokenCredential(authresult.AccessToken);
            graph_Service_Client = new GraphServiceClient(tokenCredential, scopes);

            Microsoft.Graph.Models.UserCollectionResponse result =
                await graph_Service_Client.Users.GetAsync((requestConfiguration) => requestConfiguration.QueryParameters.Top = 999);

            IDictionary<string, object> dictionary = new Dictionary<string, object>();
            int i = 0;
            foreach (var str in result.Value)
            {
                dictionary.Add(result.Value[i].DisplayName.ToString(), result.Value[i]);
                i++;
            }

            if (filter == "All") { return dictionary.ToDictionary(); } else { return dictionary.Where(kvp => kvp.Key.Contains(filter)).ToDictionary(); }
            return null;

        }

        public async static Task<AuthenticationResult> ReturnAuthenticationResult()
        {
            string AccessToken;
            PublicClientApplicationBuilder PublicClientAppBuilder =
                PublicClientApplicationBuilder.Create(clientId)
                .WithAuthority(Authority)
                .WithCacheOptions(CacheOptions.EnableSharedCacheOptions)
                .WithRedirectUri(RedirectURI);

            IPublicClientApplication PublicClientApplication = PublicClientAppBuilder.Build();
            var accounts = await PublicClientApplication.GetAccountsAsync();
            AuthenticationResult result;
            try
            {

                result = await PublicClientApplication.AcquireTokenSilent(scopes, accounts.First())
                                 .ExecuteAsync()
                                 .ConfigureAwait(false);
            }
            catch
            {
                result = await PublicClientApplication.AcquireTokenInteractive(scopes)
                                 .ExecuteAsync()
                                 .ConfigureAwait(false);
            }
            access_token = result.AccessToken;
            return result;
        }

    }

    public class AccessTokenCredential : ClientSecretCredential
    {

        public AccessTokenCredential(string accessToken)
        {
            AccessToken = accessToken;
        }

        private string AccessToken;

        public AccessToken FetchAccessToken()
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

        public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
        {
            return new ValueTask<AccessToken>(FetchAccessToken());
        }

        public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
        {
            JwtSecurityToken token = new JwtSecurityToken(AccessToken);
            return new AccessToken(AccessToken, token.ValidTo);
        }

    }

}

Code Walkthrough

Conclusion

In this article, I tried to touch base on the basic approach to Microsoft Graph API covering its core functionalities, authentication mechanisms and how it can be leveraged to return list of objects that exist in the Entra account.

In the next article we will explore the relevance and significance of this data for Microsoft Fabric.

Thanks for reading !!!

0
Subscribe to my newsletter

Read articles from Sachin Nandanwar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sachin Nandanwar
Sachin Nandanwar