Azure Active Directory: How to get the signed in users groups when there is a groups overage claim in an Access token.

Azure AD has a maximum number of groups that can be returned in an access token when you have selected to include the groups claim for your access token. This post will show you how to reproduce the scenario and then how to get the users groups using Microsoft Graph when a groups overage claim is present in the token instead of actual groups.

For a JWT token, Azure has a limit of 200 groups that can be present in the token. If the user is a member of more than 200 groups when requesting an access token for the resource that has the groups claim configured on it, instead of getting the groups you will get an overage claim, which is basically a URL that should be called to get the groups list instead. Please read about the “groups” claim in this document. The current groups claim is using the AAD Graph endpoint ( https://graph.windows.net…. ) and since this endpoint is being deprecated, we are transforming the claim to be the Microsoft Graph endpoint instead.

Configure access tokens for the groups claim

Optional claims can be configured from the Azure Portal to include Groups. Please see this document to enable the groups claim. For access tokens, you would need to configure the groups claim in the app registration for your API. If this is a 1st party app ( Microsoft App ) you will not be able to configure the groups claims — you will only be able to configure this with your own app registration. If you want the claim in the client application, you would have to configure it in the ID token instead.

Reproduce the scenario

Step 1: Configure an app registration for this sample app.
Since the application will be performing both a public client flow and a confidential client flow, you will need to configure a web redirect ( for the public client flow ) and a client secret ( for the confidential client flow ). As well, the confidential client version will need the Microsoft Graph application permission of Group.Read.All. The reason being, the confidential client must go to the users endpoint and look up the groups based on a user id, which we get from the initial sign-in token. The public client will just go to the ‘me’ endpoint, since there is a user context.

Step 2: Run the PowerShell script to recreate the scenario ( if needed )

The sample project has a text file called Create_TestGroup.ps1.txt. You can download it from the Git Repository here. To run the script, remove the .txt extension. Also, before running it, you will need an object id of a user to add to the test groups. You must be in a directory role that can create groups and add users to the groups. The sample will create 255 groups with a format of “TEST_0001”, “TEST_0002”, etc. and the object id will be added that you provide to each group.

At the bottom of the script, you will see that it will log you into Azure, then run the command to create the groups and then log you back out. There is a sample cleanup method that is commented out at line 53:

Sample project

The sample project can be located in this Git Repository.

The sample project must first be configured to work with your tenant. Once that is configured, you will need to update the “appsettings.json” file with the appropriate values:

For the AppScopes, the key is that you must have a scope for which the groups claim has been configured. Normally, this would be an api in your tenant but in this case, adding the Azure SQL database with the user_impersonation permission works for this scenario and the scope I have works with that API. This is because the groups claim has been configured on that API already.

For the Graph Scopes, add the application permissions “Group.Read.All” ( needed to get the group display name ) and “User.Read.All” ( needed to get the groups list using the client credentials flow ) You must provide admin consent for those permissions. The delegated permission “User.Read” should already be there but if not, please add that as well.

Once the app registration is configured, plug in the client id ( application id ), client secret, tenant id into the .net applications appsettings.json file.

Running the application

The application is a console application so authentication will happen in a browser window. Once you have signed in, you can close that browser window and you will then be brought back to the .Net console application.

The access token is presented so you can copy that to the clipboard and go to https://jwt.ms and paste it there to see the encoded token. It will just be a user token. However, if the groups overage claim is present in that token because the user is a member of too many groups, then the console app will display the original group overage url as well as the new group overage url that will be used in the http client request. That endpoint will not be used in the graph sdk request. The next step in the flow of the application is to get an access token for Microsoft Graph. You can get an access token using the currently signed in users refresh token ( a delegated token ) or get an access token using the client credentials grant flow ( an application token ). Make a selection for the type of token you want to get.

Next, you will then be given a chance as to choice which method to use to get the groups. This sample is going to demonstrate both using the .Net HTTP Client and also the Graph SDK client.

The groups will then be displayed in the console window:

About the Code

This application is using MSAL.Net ( Microsoft.Identity.Client ) for authenticating the user and acquiring access tokens. It is using System.Net.Http for the http client and Microsoft.Graph sdk for the graph client. To parse the JSON, System.Text.Json and for getting the claims from the token, it is using System.IdentityModel.Tokens.Jwt.

Getting the groups overage claim in the token is done with the JwtSecurityToken provider. The premise of the method I have to get the claim is simple, if there exists in the token the claims “_claim_names” and “_claim_sources” then there is a group overages claim in the token and I simply get the user id ( oid ) and build the url to call for the groups list and return that value. For educational purposes only, I am displaying the original value in the console. If either of those 2 values do not exist, then the try/catch block will handle the error and a string.empty value is returned instead to let the caller know that there is not a groups overage claim in the token.

		/// <summary>
		/// Looks for a groups overage claim in an access token and returns the value if found.
		/// </summary>
		/// <param name="accessToken"></param>
		/// <returns></returns>
		private static string Get_GroupsOverageClaimURL(string accessToken)
        {
			JwtSecurityToken token = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
			string claim = string.Empty;
			string sources = string.Empty;
			string originalUrl = string.Empty;
			string newUrl = string.Empty;

            try
            {
				// use the user id in the new graph url since the old overage link is for aad graph which is being deprecated.
				userId = token.Claims.First(c => c.Type == "oid").Value;

				// getting the claim name to properly parse from the claim sources but the next 3 lines of code are not needed,
				// just for demonstration purposes only so you can see the original value that was used in the token.
				claim = token.Claims.First(c => c.Type == "_claim_names").Value;
				sources = token.Claims.First(c => c.Type == "_claim_sources").Value;
				originalUrl = sources.Split("{\"" + claim.Split("{\"groups\":\"")[1].Replace("}","").Replace("\"","") + "\":{\"endpoint\":\"")[1].Replace("}","").Replace("\"", "");
				
				// make sure the endpoint is specific for your tenant -- .gov for example for gov tenants, etc.
				newUrl = $"https://graph.microsoft.com/v1.0/users/{userId}/memberOf?$orderby=displayName&$count=true";

				Console.WriteLine($"Original Overage URL: {originalUrl}");
				//Console.WriteLine($"New URL: {newUrl}");


			} catch {
				// no need to do anything because the claim does not exist
			} 

			return newUrl;
        }

There is a public client application configuration for signing in a user and getting access tokens and a Confidential Client Application for signing in as an application and getting access tokens ( the client credentials grant flow ). I am using just a simple ManualTokenProvider for the Graph Service Client to pass the service an access token verses having graph obtain an access token.

There is also an appsettings file and a class to store those settings ( AzureConfig.cs ) during runtime. The public static property AzureSettings will pull the settings from teh config file using a configuration builder ( similar to the asp.netcore applications ). I had to add this in since it isn’t native to a console application.

		static AzureConfig _config = null;
		public static AzureConfig AzureSettings
		{
			get
			{
				// only load this once when the app starts.
				// To reload, you will have to set the variable _config to null again before calling this property
				if (_config == null)
				{
					_config = new AzureConfig();
					IConfiguration builder = new ConfigurationBuilder()
						.SetBasePath(System.IO.Directory.GetCurrentDirectory())
						.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
						.Build();

					ConfigurationBinder.Bind(builder.GetSection("Azure"), _config);
				}

				return _config;
			}
		}

For the Authentication provider for the Graph service client, I will just be using a custom manual token provider so that I can set the access token for the client since I am already obtaining access tokens using MSAL.

using Microsoft.Graph;

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace MSAL.Net_GroupOveragesClaim.Authentication
{
    class ManualTokenProvider : IAuthenticationProvider
    {
        string _accessToken;

        public ManualTokenProvider ( string accessToken)
        {
            _accessToken = accessToken;
        }

        async Task IAuthenticationProvider.AuthenticateRequestAsync(HttpRequestMessage request)
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
            request.Headers.Add("ConsistencyLevel", "eventual");
        }
    }
}

The HTTP method has 2 parts, the method “Get_Groups_Http_Method” will call “Graph_Request_viaHTTP” to get the list of groups and then display’s that list in the console window.

		/// <summary>
		/// Entry point to make the request to Microsoft graph using the .Net HTTP Client
		/// </summary>
		/// <param name="graphToken"></param>
		/// <returns></returns>
		private static async Task Get_Groups_HTTP_Method(string graphToken, string url)
        {
			List<Group> groupList = new List<Group>();
						
			groupList = await Graph_Request_viaHTTP(graphToken, url);
			foreach (Group g in groupList)
			{
				Console.WriteLine($"Group Id: {g.Id} : Display Name: {g.DisplayName}");
			}
		}
		/// <summary>
		/// Calls Microsoft Graph via a HTTP request.  Handles paging in the request
		/// </summary>
		/// <param name="user_access_token"></param>
		/// <returns>List of Microsoft Graph Groups</returns>
		private static async Task<List<Group>> Graph_Request_viaHTTP(string user_access_token, string url)
        {
			string json = string.Empty;
			//string url = "https://graph.microsoft.com/v1.0/me/memberOf?$orderby=displayName&$count=true";
			List<Group> groups = new List<Group>();

			// todo: check for the count parameter in the request and add if missing

			/*
			 * refer to this documentation for usage of the http client in .net
			 * https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-5.0
			 * 
			 */

			// add the bearer token to the authorization header for this request
			_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( "Bearer", user_access_token);
			
			// adding the consistencylevel header value if there is a $count parameter in the request as this is needed to get a count
			// this only needs to be done one time so only add it if it does not exist already.  It is case sensitive as well.
			// if this value is not added to the header, the results will not sort properly -- if that even matters for your scenario
			if(url.Contains("&$count", StringComparison.OrdinalIgnoreCase))
            {
                if (!_httpClient.DefaultRequestHeaders.Contains("ConsistencyLevel"))
                {
					_httpClient.DefaultRequestHeaders.Add("ConsistencyLevel", "eventual");
                }
            }
			
			// while loop to handle paging
			while(url != string.Empty)
            {
				HttpResponseMessage response = await _httpClient.GetAsync(new Uri(url));
				url = string.Empty; // clear now -- repopulate if there is a nextlink value.

				if (response.IsSuccessStatusCode)
				{
					json = await response.Content.ReadAsStringAsync();

					// Console.WriteLine(json);

					using (JsonDocument document = JsonDocument.Parse(json))
					{
						JsonElement root = document.RootElement;
						// check for the nextLink property to see if there is paging that is occuring for our while loop
						if (root.TryGetProperty("@odata.nextLink", out JsonElement nextPage))
                        {
							url = nextPage.GetString();
                        }
						JsonElement valueElement = root.GetProperty("value"); // the values

						// loop through each value in the value array
						foreach (JsonElement value in valueElement.EnumerateArray())
						{
							if (value.TryGetProperty("@odata.type", out JsonElement objtype))
							{
								// only getting groups -- roles will show up in this graph query as well.
								// If you want those too, then remove this if filter check
								if (objtype.GetString() == "#microsoft.graph.group")
								{
									Group g = new Group();

									// specifically get each property you want here and populate it in our new group object
									if (value.TryGetProperty("id", out JsonElement id)) { g.Id = id.GetString(); }
									if (value.TryGetProperty("displayName", out JsonElement displayName)) { g.DisplayName = displayName.GetString(); }

									groups.Add(g);
								}
							}
						}
					}
				} else
                {
					Console.WriteLine($"Error making graph request:\n{response.ToString()}");
                }
			} // end while loop
	
			return groups;
        }

In a similar fashion, the Graph sdk has an entry method ( “Get_Groups_GraphSDK_Method” ) that will call “Get_GroupList_GraphSDK” to get the list of groups and then display them in the console window.

		/// <summary>
		/// Entry point to make the request to Microsoft Graph using the Graph sdk and outputs the list to the console.
		/// </summary>
		/// <param name="graphToken"></param>
		/// <returns></returns>
		private static async Task Get_Groups_GraphSDK_Method(string graphToken, bool me_endpoint)
        {
			List<Group> groupList = new List<Group>();

			groupList = await Get_GroupList_GraphSDK(graphToken, me_endpoint);
			foreach (Group g in groupList)
			{
				Console.WriteLine($"Group Id: {g.Id} : Display Name: {g.DisplayName}");
			}
		}

To get the group list, there is logic to determine if we are going to use the “me” endpoint to get the group list or the “users” endpoint. If you used the client credentials grant flow to get the access token for Microsoft Graph, then it will be using the “users” endpoint. If not ( i.e. a delegated flow was used for the access token ) then it will be using the “users” endpoint.

		/// <summary>
		/// Calls the Me.MemberOf endpoint in Microsoft Graph and handles paging
		/// </summary>
		/// <param name="graphToken"></param>
		/// <returns>List of Microsoft Graph Groups</returns>
		private static async Task<List<Group>> Get_GroupList_GraphSDK(string graphToken, bool use_me_endpoint)
        {

			GraphServiceClient client;

			Authentication.ManualTokenProvider authProvider = new Authentication.ManualTokenProvider(graphToken);

			client = new GraphServiceClient(authProvider);
			IUserMemberOfCollectionWithReferencesPage membershipPage = null;

			HeaderOption option = new HeaderOption("ConsistencyLevel","eventual");

			if (use_me_endpoint)
            {
                if (!client.Me.MemberOf.Request().Headers.Contains(option))
                {
					client.Me.MemberOf.Request().Headers.Add(option);
                }

				membershipPage = await client.Me.MemberOf
					.Request()
					.OrderBy("displayName&$count=true") // todo: find the right way to add the generic query string value for count
					.GetAsync();
            } else
            {
                if (!client.Users[userId].MemberOf.Request().Headers.Contains(option))
                {
					client.Users[userId].MemberOf.Request().Headers.Add(option);
                }

				membershipPage = await client.Users[userId].MemberOf
					.Request()
					.OrderBy("displayName&$count=true")
					.GetAsync();
            }

			List<Group> allItems = new List<Group>();			
			
			if(membershipPage != null)
            {
				foreach(DirectoryObject o in membershipPage)
                {
					if(o is Group)
                    {
						allItems.Add((Group)o);
                    }
                }

				while (membershipPage.AdditionalData.ContainsKey("@odata.nextLink") && membershipPage.AdditionalData["@odata.nextLink"].ToString() != string.Empty)
                {
					membershipPage = await membershipPage.NextPageRequest.GetAsync();
					foreach (DirectoryObject o in membershipPage)
					{
						if (o is Group)
						{
							allItems.Add(o as Group);
						}
					}
				}

            }

             return allItems;

		}

Regardless of the method used, the code will handle paging since, by default, only 100 records per page will be returned. Paging is determined via the “@odata.nextLink” value. If there is a value for that property, then that full url is called for the next page of data. See this document for more information about paging.

Summary

Access tokens have a limit to how many groups will appear in the token. When the limit is exceeded, Azure will provide the “groups overage claim” which is a URL to call in order to get the list of groups that normally would have been present in the token. This blog post gives 2 examples how you can obtain the group list for the currently signed in user.

How to use postman to perform a Client Credentials Grant flow with a certificate

This post will demonstrate a couple of things:

  1. How to create a signed jwt token (aka Client Assertion) using Powershell.
  2. How to use this generated Client Assertion in Postman to get an Access Token Using Client Credentials Grant Flow.

To get an Access Token using Client-Credentials Flow, we can either use a Secret or a Certificate. This post will use a self-signed certificate to create the client assertion using both the nuget packages Microsoft.IdentityModel.Tokens and MIcrosoft.IdentityModel.JsonWebTokens. The idea in this blog is borrowed from the documentation Generating proof of possession tokens for rolling keys. Thanks to my team members Ray Held and Bac Hoang for assisting me with this sample. 🙂

Pre-requisites

To run the script in this blog you should have the following:

  1. Application created on the Azure portal under App registrations Blade.
  2. Assign Application level permissions under Microsoft Graph resource. Make sure you give the admin consent.
  3. Signing certificate

Here is the reference for Creating a self-signed Certificate. By running the Powershell script given in this reference, you will have the private key pfx and public key cer files created in the specified folder. Next you will need to upload that .cer file in the App registrations, like shown below.

Once you upload the certificate this is how it looks:

1. Create a signed jwt token (aka Client Assertion) using Powershell.

About few Objects used in this PowerShell script:

  • $x509cert = This will give the certificate from the .pfx file. You will need the full path to the .pfx file and the password for the pfx.
  • $signingCredentials = Represents an X.509 token used as the signing credential.
  • $securityTokenDescriptor = Place holder for all the attributes related to the issued token.

Note: This PowerShell script may not work in PS core environment.

$ClientID           = "XXXX" # Application/Client id used for the cert name 
$TenantID      = "XXXX" # tenant id is used for the cert name 
$CertPassWord       = "XXXX" # Password used for creating the certificate
$aud                = "https://login.microsoftonline.com/$TenantID/v2.0/"
$CertificatePath_Pfx           = "Get the path to the .pfx file" # Path where the certificate is saved

Function JsonWeb-Libraries{

    if ( ! (Get-ChildItem $HOME/IdentityModel/lib/Microsoft.IdentityModel.Logging.* -erroraction ignore) ) {
        install-package -Source nuget.org -ProviderName nuget -SkipDependencies Microsoft.IdentityModel.Logging -Destination $HOME/IdentityModel/lib -force -forcebootstrap | out-null
    }
    [System.Reflection.Assembly]::LoadFrom((Get-ChildItem $HOME/IdentityModel/lib/Microsoft.IdentityModel.Logging.*/lib/net45/Microsoft.IdentityModel.Logging.dll).fullname) | out-null
    if ( ! (Get-ChildItem $HOME/IdentityModel/lib/Microsoft.IdentityModel.Tokens.* -erroraction ignore) ) {
        install-package -Source nuget.org -ProviderName nuget -SkipDependencies Microsoft.IdentityModel.Tokens -Destination $HOME/IdentityModel/lib -force -forcebootstrap | out-null
    }
    [System.Reflection.Assembly]::LoadFrom((Get-ChildItem $HOME/IdentityModel/lib/Microsoft.IdentityModel.Tokens.*/lib/net45/Microsoft.IdentityModel.Tokens.dll).fullname) | out-null

    if ( ! (Get-ChildItem $HOME/IdentityModel/lib/Microsoft.IdentityModel.JsonWebTokens.* -erroraction ignore) ) {
        install-package -Source nuget.org -ProviderName nuget -SkipDependencies Microsoft.IdentityModel.JsonWebTokens -Destination $HOME/IdentityModel/lib -force -forcebootstrap | out-null
    }
    [System.Reflection.Assembly]::LoadFrom((Get-ChildItem $HOME/IdentityModel/lib/Microsoft.IdentityModel.JsonWebTokens.*/lib/net45/Microsoft.IdentityModel.JsonWebTokens.dll).fullname) | out-null
}
  
Function Get-ClientAssertion {

    JsonWeb-Libraries
    $x509cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CertificatePath_Pfx, $CertPassWord)
    $claims = new-object 'System.Collections.Generic.Dictionary[String, Object]'
    $claims['aud'] = $aud
    $claims['iss' ] = $ClientId
    $claims['sub'] = $ClientId
    $claims['jti'] = [GUID]::NewGuid().ToString('D')
                   
    $signingCredentials = [Microsoft.IdentityModel.Tokens.X509SigningCredentials]::new($x509cert)
    $securityTokenDescriptor = [Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor]::new()
    $securityTokenDescriptor.Claims = $claims
    $securityTokenDescriptor.SigningCredentials = $signingCredentials 

    $tokenHandler = [Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler]::new()
    $clientAssertion = $tokenHandler.createToken($securityTokenDescriptor)
    write-host $clientAssertion
}  
$myvar = Get-ClientAssertion

Once you get the Client-Assertion , you can decode it using jwt.ms . Here is how the decoded Client Assertion looks like.

The reference for Client – Assertion Format: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials

2. Use this generated Client Assertion in Postman to get an Access Token Using Client Credentials Grant Flow.

Reference: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate

Use the TenantID and ClientID which are used while running the powershell script. And For client_assertion parameter, use the output from the PowerShell script.

POST: https://login.microsoftonline.com/{TenantID}/oauth2/v2.0/token

BODY:

scope=https://graph.microsoft.com/.default
&client_id={ClientID}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion= {assertion you received from the above script}
&grant_type=client_credentials

Here is the Postman screen shot:

You can decode the access token using jwt.ms.

Using MSAL.Net to perform the client credentials flow with a certificate instead of a client secret in a .NetCore console appliction.

The sample files for this post can be found in this GitHub repository: https://github.com/RayGHeld/NetCore_ClientCredentials_withCert

There is also a Powershell script there to create a certificate for this sample.

You can perform the OAuth2 client_credentials grant flow to sign in as an application for your automated type of services. This flow does not require an interactive user to authenticate and should only be run in secure environments. There are 2 methods to handle securing the authentication portion: 1) use a client secret and 2) use a certificate. This blog post will show you how to authenticate using this flow in a .NetCore console application and a certificate.

The flow that is being followed is documented here. To begin the process, you will need a certificate and for this post, I will be using a self signed certificate using Powershell. I borrowed the Powershell example from this blog post and made modifications for this post so that the certificate that is created lines up with code since I am demonstrating multiple ways to get the certificate from the certificate store.

The first step is to create the certificate. You can find the Powershell script in the GitHub link at the start of this post. I have masked my tenant id and client id. Please remember to use your own. Also, note the folder location that the cert is created in ( you can change that value as well ) but you will need that in the .NetCore app.

# Create self-signed certificate and export pfx and cer files.  You will need to run the script in PowerShell Admin window to create this self-signed certificate

# please be aware that running this script will create a new cert each time in your certificate store.  
# If you need to rerun it, I recommend deleting the previous certs first in the store.
# Duplicates are only really an issue if you are using the GetCertByName method

$tenant_id = 'xxxx' # tenant id is used for the cert name / subject name and matches the .netcore code sample that goes with this script
$client_id = 'xxxx' # client id is used for the cert name / subject name and cert password and matches the .netcore code sample that goes with this script
# Change the following file path to your own
$FilePath = 'C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\'
$StoreLocation = 'CurrentUser' # be aware that LocalMachine requires elevated privileges
$expirationYears = 1

$SubjectName = $tenant_id + '.' + $client_id
$cert_password = $client_id

$pfxFileName = $SubjectName + '.pfx'
$cerFileName = $SubjectName + '.cer'

$PfxFilePath = $FilePath + $pfxFileName
$CerFilePath = $FilePath + $cerFileName

$CertBeginDate = Get-Date
$CertExpiryDate = $CertBeginDate.AddYears($expirationYears)
$SecStringPw = ConvertTo-SecureString -String $cert_password -Force -AsPlainText 
$Cert = New-SelfSignedCertificate -DnsName $SubjectName -CertStoreLocation "cert:\$StoreLocation\My" -NotBefore $CertBeginDate -NotAfter $CertExpiryDate -KeySpec Signature
Export-PfxCertificate -cert $Cert -FilePath $PFXFilePath -Password $SecStringPw 
Export-Certificate -cert $Cert -FilePath $CerFilePath 

Once the certificate is created, you can find it in your certificate store on the computer the script was ran from. To get to the certificate store on the computer, I simply did a search in the tool bar search for “Certificate” and then used the Manage user certificates link that appeared.

Once the certificate is created, you can upload that to an app registration in the Azure portal.

For the .netcore application, you will need to make similar changes to the client id, tenant id and also the folder path. Also, you can enter the thumbprint here as well since there is a method to search for the certificate by thumbprint ( which is actually the best way of doing this ).

        static String accessToken = string.Empty;
        static String client_id = "xxxx";
        static String cert_password = client_id;
        static String tenant_id = "xxxx";
        static String authority = $"https://login.microsoftonline.com/{tenant_id}";
        static List<String> scopes = new List<String>() { $"https://graph.microsoft.com/.default" };
        static String thumbprint = "xxxxxx";

There are 3 methods that will search for the certificate. I have 2 of them commented out ( you can only use one method here so choose one, comment the other 2 out when testing ) and they are part of the confidential client builder class — see the method called GetToken.

            IConfidentialClientApplication ica = ConfidentialClientApplicationBuilder.Create(client_id)
                .WithCertificate(GetCert(@"C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\"))
                //.WithCertificate(GetCertByThumbprint( StoreLocation.CurrentUser, thumbprint ) )
                //.WithCertificate( GetCertByName( StoreLocation.CurrentUser, $"{tenant_id}.{client_id}" ) )
                .WithTenantId(tenant_id)
                .WithAuthority(authority)
                .Build();

And, that is it. Your application will perform the client credentials flow using the certificate you created. This demo app does not make any api calls. It is only getting a access token. You can copy that access token and decode it at http://jwt.ms to see what it contains.

The full module with the functions being called to get a certificate:

using Microsoft.Identity.Client;

using System;
using System.Collections.Generic;
using System.Runtime.ConstrainedExecution;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace MSAL_ClientCred_Cert
{

    /*
     * Reference: https://blogs.aaddevsup.xyz/2019/01/console_keyvault_clientcredentials/ -- for instructions on how to create a certificate
     */
    class Program
    {
        static String accessToken = string.Empty;
        static String client_id = "xxx";
        static String cert_password = client_id;
        static String tenant_id = "xxx.com";
        static String authority = $"https://login.microsoftonline.com/{tenant_id}";
        static List<String> scopes = new List<String>() { $"https://graph.microsoft.com/.default" };
        static String thumbprint = "xxx";

        static void Main(string[] args)
        {

            Console.WriteLine("Hello World!");
            GetToken();
            Console.ReadKey();
        }

        static async void GetToken()
        {
            accessToken = string.Empty;

            IConfidentialClientApplication ica = ConfidentialClientApplicationBuilder.Create(client_id)
                .WithCertificate(GetCert(@"C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\"))
                //.WithCertificate(GetCertByThumbprint( StoreLocation.CurrentUser, thumbprint ) )
                //.WithCertificate( GetCertByName( StoreLocation.CurrentUser, $"{tenant_id}.{client_id}" ) )
                .WithTenantId(tenant_id)
                .WithAuthority(authority)
                .Build();

            AuthenticationResult authResult = null;

            try
            {
                authResult = await ica.AcquireTokenForClient(scopes).ExecuteAsync();
                accessToken = authResult.AccessToken;

                Console.WriteLine($"Access Token: {accessToken}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"MSAL Error: {ex.Message}");
            }


        }

        /// <summary>
        /// Gets a certificate by the .pfx name from the file system
        /// </summary>
        /// <param name="basePath">The folder where the cert is stored -- filename is derived from the settings in this application</param>
        /// <returns></returns>
        static X509Certificate2 GetCert(String basePath)
        {
            String certPath = $"{basePath}{tenant_id}.{client_id}.pfx";
            X509Certificate2 cer = new X509Certificate2(certPath, cert_password, X509KeyStorageFlags.EphemeralKeySet);

            return cer;
        }

        /// <summary>
        /// Gets a certificate based on the name -- please note, there is an issue using this method
        /// if there are multiple certs with the same name.
        /// </summary>
        /// <param name="storeLoc"></param>
        /// <param name="subjectName"></param>
        /// <returns></returns>
        static X509Certificate2 GetCertByName(StoreLocation storeLoc, String subjectName)
        {

            //String certPath = @"C:\Users\myuser\OneDrive - Microsoft\Documents\Certs\MSAL_ClientCred_Cert.cer";
            //X509Certificate2 cer = new X509Certificate2( certPath, client_id, X509KeyStorageFlags.EphemeralKeySet );
            //store.Certificates.Add( cer ); -- use this to import the certificate to the store

            // read certificates already installed
            X509Store store = new X509Store(storeLoc);
            X509Certificate2 cer = null;

            store.Open(OpenFlags.ReadOnly);

            // look for the specific cert by subject name -- returns the first one found, if any.  Also, for self signed, set the valid parameter to 'false'
            X509Certificate2Collection cers = store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false);
            if (cers.Count > 0)
            {
                cer = cers[0];
            };
            store.Close();

            return cer;

        }

        /// <summary>
        /// Gets a certificate by Thumbprint -- this is the preferred method
        /// </summary>
        /// <param name="storeLoc"></param>
        /// <param name="thumbprint"></param>
        /// <returns></returns>
        static X509Certificate2 GetCertByThumbprint(StoreLocation storeLoc, String thumbprint)
        {
            X509Store store = new X509Store(storeLoc);
            X509Certificate2 cer = null;

            store.Open(OpenFlags.ReadOnly);

            // look for the specific cert by thumbprint -- Also, for self signed, set the valid parameter to 'false'
            X509Certificate2Collection cers = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
            if (cers.Count > 0)
            {
                cer = cers[0];
            };
            store.Close();

            return cer;
        }
    }
}

Implement Client Credentials flow for Graph Java Client

In my previous article , I showed you how to modify our great Graph Client for Java sample to add some additional options for things like filtering, setting the max retries for 429 errors, etc.  That sample uses the Oauth2 Device Code flow.  In this article, I will show you how to convert that and use the Client Credentials Flow.  Although, you will not be able to retrieve the same information as in the previous example, since the client credentials relies on application permissions instead of delegated permissions ( see this great article by Bac Hoang for the differences between the two ). I have modified it to show some examples using this flow.  If you haven’t already downloaded and gotten the sample project working, please do so here.

Update the App Registration

The first step you will need to make is go to the azure portal and to the app registration you created for the sample app.  You will need to add a client secret to the app registration like so:

Be sure to copy the secret that is generated and store that somewhere secure as you will not be able to retrieve that later if you don’t ( see how my value is masked with ****** ). Next, we need to add some application permissions to the app registration.  For this change, let’s add the Microsoft Graph Application Permission “User.Read.All” and “Calendars.ReadWrite”.  You can remove the delegated permissions if you like or leave them if you want the previous sample to still work.  Be sure to provide Admin Consent.

Now, back to the project to make our code changes…

Modify the java code

You will need to modify the file src\main\resources\com\contoso\oAuth.properties to change the scopes value ( I just commented out my old version ) and add the client secret.  For this, I am using the short hand notation of /.default for the Microsoft Graph endpoint:  https://graph.microsoft.com/.default otherwise, I would have to list each one separately like so:  https://graph.microsoft.com/Calendars.ReadWrite https://graph.microsoft.com/User.Read.All
As you can see, it is easier to just use /.default and only have to write the resource one time.

Next, let’s modify the file Graph.java under src\mail\java\com\contoso

Add the import: import com.microsoft.graph.models.extensions.Calendar;  at the top.

Let’s modify the method getUser.  We will add an additional parameter to specify the user since we are doing a client credentials flow, there is no user context so we need this method to look up a user based on a User Principal Name (upn) we are going to pass in.  You can see where I just commented out the old “me” code:

    public static User getUser(String accessToken, String upn) {
        ensureGraphClient(accessToken);

        // GET /me to get authenticated user
        /*
        User me = graphClient
            .me()
            .buildRequest()
            .get();
        */

        try{
            User user = graphClient
                .users(upn)
                .buildRequest()
                .get();
            return user;       
        } catch ( Exception ex ) {
            System.out.println("Error getting user " + ex.getMessage());
            return null;
        }
    }

We will need to create a GetCalendar method:

    public static Calendar GetCalendar(String accessToken, String upn){
        ensureGraphClient(accessToken);
        try {
            Calendar cal = graphClient
                .users(upn)
                .calendar()
                .buildRequest()
                .get();
            return cal;
        } catch (Exception ex){
            System.out.println("Error getting calendar " + ex.getMessage());
            return null;
        }
    }

Next, let’s modify Authentication.java to use the new client credentials.

Some new imports that need to be added:
import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.IClientCredential;
import com.microsoft.aad.msal4j.SilentParameters;
import java.net.MalformedURLException;

In public class Authentication, add a new static variable:  clientSecret

    private static String applicationId;
    // Set authority to allow only organizational accounts
    // Device code flow only supports organizational accounts
    private static String authority;
    private static String clientSecret;

Modify the initialize method like so:

    public static void initialize(String applicationId, String authority, String clientSecret) {
        Authentication.authority = authority;
        Authentication.applicationId = applicationId;
        Authentication.clientSecret = clientSecret;
    }

You can next comment out all of the devicecode flow code like so:

        /*
        PublicClientApplication app;
        try {
            // Build the MSAL application object with
            // app ID and authority
            app = PublicClientApplication.builder(applicationId)
                .authority(authority)
                .build();
        } catch (MalformedURLException e) {
            return null;
        }

        
        // Create consumer to receive the DeviceCode object
        // This method gets executed during the flow and provides
        // the URL the user logs into and the device code to enter
        Consumer<DeviceCode> deviceCodeConsumer = (DeviceCode deviceCode) -> {
            // Print the login information to the console
            System.out.println(deviceCode.message());
        };

        // Request a token, passing the requested permission scopes
        IAuthenticationResult result = app.acquireToken(
            DeviceCodeFlowParameters
                .builder(scopeSet, deviceCodeConsumer)
                .build()
        ).exceptionally(ex -> {
            System.out.println("Unable to authenticate - " + ex.getMessage());
            return null;
        }).join();
        */

Then, below that comment block but before the if(result != null) block, add this code:

		IClientCredential cred = ClientCredentialFactory.createFromSecret(clientSecret);
        ConfidentialClientApplication app;
        try {
            // Build the MSAL application object for a client credential flow
            app = ConfidentialClientApplication.builder(applicationId, cred ).authority(authority).build();
        } catch (MalformedURLException e) {
            System.out.println("Error creating confidential client: " + e.getMessage());
            return null;
        }
        
        IAuthenticationResult result;
        try{
            SilentParameters silentParameters = SilentParameters.builder(scopeSet).build();
            result= app.acquireTokenSilently(silentParameters).join();
        } catch (Exception ex ){
            if (ex.getCause() instanceof MsalException) {

                ClientCredentialParameters parameters =
                        ClientCredentialParameters
                                .builder(scopeSet)
                                .build();
   
                // Try to acquire a token. If successful, you should see
                // the token information printed out to console
                result = app.acquireToken(parameters).join();
            } else {
                // Handle other exceptions accordingly
                System.out.println("Unable to authenticate = " + ex.getMessage());
                return null;
            }
        }

My Full file looks like this:

package com.contoso;

import java.net.MalformedURLException;
// import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;

import javax.security.auth.login.Configuration.Parameters;

import com.microsoft.aad.msal4j.AuthorizationCodeParameters;
import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.DeviceCode;
import com.microsoft.aad.msal4j.DeviceCodeFlowParameters;
import com.microsoft.aad.msal4j.IAuthenticationResult;
import com.microsoft.aad.msal4j.IClientCredential;
import com.microsoft.aad.msal4j.MsalException;
import com.microsoft.aad.msal4j.PublicClientApplication;
import com.microsoft.aad.msal4j.SilentParameters;


/**
 * Authentication
 */
public class Authentication {

    private static String applicationId;
    // Set authority to allow only organizational accounts
    // Device code flow only supports organizational accounts
    private static String authority;
    private static String clientSecret;

    public static void initialize(String applicationId, String authority, String clientSecret) {
        Authentication.authority = authority;
        Authentication.applicationId = applicationId;
        Authentication.clientSecret = clientSecret;
    }

    public static String getUserAccessToken(String[] scopes) {
        if (applicationId == null) {
            System.out.println("You must initialize Authentication before calling getUserAccessToken");
            return null;
        }

         Set<String> scopeSet = new HashSet<>();
        
        Collections.addAll(scopeSet, scopes);
        /*
        PublicClientApplication app;
        try {
            // Build the MSAL application object with
            // app ID and authority
            app = PublicClientApplication.builder(applicationId)
                .authority(authority)
                .build();
        } catch (MalformedURLException e) {
            return null;
        }

        
        // Create consumer to receive the DeviceCode object
        // This method gets executed during the flow and provides
        // the URL the user logs into and the device code to enter
        Consumer<DeviceCode> deviceCodeConsumer = (DeviceCode deviceCode) -> {
            // Print the login information to the console
            System.out.println(deviceCode.message());
        };

        // Request a token, passing the requested permission scopes
        IAuthenticationResult result = app.acquireToken(
            DeviceCodeFlowParameters
                .builder(scopeSet, deviceCodeConsumer)
                .build()
        ).exceptionally(ex -> {
            System.out.println("Unable to authenticate - " + ex.getMessage());
            return null;
        }).join();
        */

        
        IClientCredential cred = ClientCredentialFactory.createFromSecret(clientSecret);
        ConfidentialClientApplication app;
        try {
            // Build the MSAL application object for a client credential flow
            app = ConfidentialClientApplication.builder(applicationId, cred ).authority(authority).build();
        } catch (MalformedURLException e) {
            System.out.println("Error creating confidential client: " + e.getMessage());
            return null;
        }
        
        IAuthenticationResult result;
        try{
            SilentParameters silentParameters = SilentParameters.builder(scopeSet).build();
            result= app.acquireTokenSilently(silentParameters).join();
        } catch (Exception ex ){
            if (ex.getCause() instanceof MsalException) {

                ClientCredentialParameters parameters =
                        ClientCredentialParameters
                                .builder(scopeSet)
                                .build();
   
                // Try to acquire a token. If successful, you should see
                // the token information printed out to console
                result = app.acquireToken(parameters).join();
            } else {
                // Handle other exceptions accordingly
                System.out.println("Unable to authenticate = " + ex.getMessage());
                return null;
            }
        }


        if (result != null) {
            // System.out.println("Access Token - " + result.accessToken());
            return result.accessToken();
        }

        return null;
    }
}

Finally, let’s modify the App.java file located in the same folder.

You will need to add a final String for the clientSecret like so:
        final String clientSecret = oAuthProperties.getProperty(“app.clientSecret”);

Then, for authentication to occur, you can initialize authentication with this block:

        Authentication.initialize(appId, authority, clientSecret);
        final String accessToken = Authentication.getUserAccessToken(appScopes);         System.out.println(“Access token = ” + accessToken

The rest of the code is to add some additional functionality to work with the new permissions we have setup.  I have added a promptForUPN method so we can change the upn of the user we are working with and a couple of new choice options in the menu.  The full file is here:

package com.contoso;

import java.util.InputMismatchException;
import java.util.Scanner;

import com.microsoft.graph.models.extensions.DateTimeTimeZone;
import com.microsoft.graph.models.extensions.Event;
import com.microsoft.graph.models.extensions.User;
import com.microsoft.graph.models.extensions.Calendar;

import java.io.Console;
import java.io.IOException;
import java.util.Properties;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.List;

/**
 * Graph Tutorial
 *
 */
public class App {
    public static void main(String[] args) {
        System.out.println("Java Graph Tutorial");
        System.out.println();

        // Load OAuth settings
        final Properties oAuthProperties = new Properties();
        try {
            oAuthProperties.load(App.class.getResourceAsStream("oAuth.properties"));
        } catch (IOException e) {
            System.out.println("Unable to read OAuth configuration. Make sure you have a properly formatted oAuth.properties file. See README for details.");
            return;
        }

        final String appId = oAuthProperties.getProperty("app.id");
        final String[] appScopes = oAuthProperties.getProperty("app.scopes").split(",");
        final String authority = oAuthProperties.getProperty("app.authority");
        final String clientSecret = oAuthProperties.getProperty("app.clientSecret");

        // Get an access token
        //Authentication.initialize(appId, authority);j
        Authentication.initialize(appId, authority, clientSecret);
        final String accessToken = Authentication.getUserAccessToken(appScopes);
        System.out.println("Access token = " + accessToken);
        
        String upn = promptForUPN();
        
        // Greet the user
        User user = Graph.getUser(accessToken,upn);
        if(user!=null){
            System.out.println("You have select user " + user.displayName);
        }
        
        Scanner input = new Scanner(System.in);

        int choice = -1;

        while (choice != 0) {
            System.out.println();
            System.out.println("Please choose one of the following options:");
            System.out.println("0. Exit");
            System.out.println("1. Display access token");
            System.out.println("2. Input upn to work with");
            System.out.println("3. Get this users info");
            System.out.println("4. Get this users calender");

            try {
                choice = input.nextInt();
            } catch (InputMismatchException ex) {
                // Skip over non-integer input
                input.nextLine();
            }

            // Process user choice
            switch(choice) {
                case 0:
                    // Exit the program
                    System.out.println("Goodbye...");
                    break;
                case 1:
                    // Display access token
                    System.out.println("Access token: " + accessToken);
                    break;
                case 2:
                    upn = promptForUPN();
        
                    // Greet the user
                    user = Graph.getUser(accessToken,upn);
                    if(user!=null){
                        System.out.println("You have selected user " + user.displayName);
                    }
                    break;

                case 3:
                    if(user!=null){
                        System.out.println("User info:");
                        System.out.println("    id= " + user.id);
                        System.out.println("    mail= " + user.mail);
                    } else {
                        System.out.println("*** No user selected ***");
                    }
                    break;
                case 4:
                    if(user!=null){
                        Calendar cal = Graph.GetCalendar(accessToken,upn);
                        System.out.println("Calendar info:");
                        System.out.println("    id= " + cal.id );                        
                    } else {
                        System.out.println("*** No user selected ***");
                    }
                    break;
                default:
                    System.out.println("Invalid choice");
            }
        }

        input.close();
    }

    private static String formatDateTimeTimeZone(DateTimeTimeZone date) {
        LocalDateTime dateTime = LocalDateTime.parse(date.dateTime);
    
        return dateTime.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)) + " (" + date.timeZone + ")";
    }

    private static String promptForUPN(){
        String upn;
        Console console = System.console();
        upn = console.readLine("Enter user upn: ");
        return upn;
    }
  }

Reference for the client credentials provider for java:  https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-daemon-acquire-token?tabs=java#acquiretokenforclient-api

Run the code

You will notice now when you run the project, you are not prompted for any kind of authentication.  Instead, right away, you will see the access token come back in the console window, and then we will be prompted to enter a upn so we can get details about this user:

Looking at the access token in http://jwt.ms you can see we have an application only token for Microsoft graph:

After the access token gets displayed and the initial user information for the upn you entered, you will get the menu of actions you can use for this app, including being prompted for a new upn if desired.

Press 3 to get this users info and you will get the id and mail attribute ( if set ).

Summary

As you can see, we are now able to look up users using the client credentials flow.  This is only recommended for scenarios where the application is running in a secure environment.  I do not recommend this flow being used in any kind of distributed application as the client secret is easily retrieved.  This should be server side scripts that users do not have access to.  Also, be careful when assigning application permissions because you can potentially give the application far more permissions than is needed which may put you in a compromised state.

Using PowerShell to get Azure AD audit logs

In my previous blog, I talked about how to use PowerShell with Microsoft Graph Reporting API. In that blog, I used the Client Credentials grant flow to acquire an access token for Microsoft Graph against the V1 endpoint. Also the code sample in that blog only works if all the reporting data result set is small. In this blog I’ll discuss how to get a Microsoft Graph access token using Client Credentials grant flow against the V2 endpoint and also add paging support to handle large result set which can span multiple pages.

As a pre-requisite, you will have to create an Application Registration in Azure Active Directory and configure the application to have Microsoft Graph Application Permission “AuditLog.Read.All” as laid out in my last blog. Remember to grant admin consent to the Microsoft Graph permissions as well.

Getting an Access Token

There are a couple of ways you can get an access token.

  1. Build a raw HTTP POST request for the v2 token endpoint (code below)
$ClientID = "<client id>"
$ClientSecret = "<client secret>"
$loginURL       = "https://login.microsoftonline.com"
$tenantdomain   = "<tenant name>.onmicrosoft.com" 
$scope1 = "https://graph.microsoft.com/.default"

$body = @{grant_type="client_credentials";scope=$scope1;client_id=$ClientID;client_secret=$ClientSecret}
$oauth = Invoke-RestMethod -Method Post -Uri $("$loginURL/$tenantdomain/oauth2/v2.0/token") -Body $body
Write-Host "Access Token: " $oauth.access_token
  1. Use MSAL.Net to acquire a token

I actually like this approach better than the first approach mainly due to MSAL’s capability of always getting a valid access token. By default, an access token is only valid for 1 hour. This can be problematic (especially with the first approach) if an application takes longer than 1 hour to page through a large result set with the same token. Once the token expires, MS Graph will return an error with regards to expired token. To be able to handle this scenario more effectively the application will have to take care of checking for token validity and sending out new request to get a fresh access token should the current one becomes expired. With MSAL, you don’t have to worry about token expiration. MSAL maintains its own token cache. With every AcquireTokenxxx API call, MSAL returns the token from its cache only if the token is still valid. If the existing cached token is about to expire or has expired, MSAL will automatically send out a new request to get a fresh token and return that new token to the client. Below is a sample PowerShell snippet using MSAL to acquire an access token for Microsoft Graph and then use the token for getting user sign-ins report.

Note: This PowerShell script has been tested with MSAL.Net v4.11.0 and Windows PowerShell v5.1. It might not be compatible with PowerShell Core.

$ClientID = "<client id>"
$ClientSecret = "<client secret>"
$loginURL       = "https://login.microsoftonline.com"
$tenantdomain   = "<tenant name>.onmicrosoft.com"
[string[]] $Scopes = "https://graph.microsoft.com/.default"

# Download MSAL.Net module to a local folder if it does not exist there
if ( ! (Get-ChildItem $Env:USERPROFILE/MSAL/lib/Microsoft.Identity.Client.* -erroraction ignore) ) {
    install-package -Source nuget.org -ProviderName nuget -SkipDependencies Microsoft.Identity.Client -Destination $Env:USERPROFILE/MSAL/lib -force -forcebootstrap | out-null
}

# Load the MSAL assembly -- needed once per PowerShell session
[System.Reflection.Assembly]::LoadFrom((Get-ChildItem $Env:USERPROFILE/MSAL/lib/Microsoft.Identity.Client.*/lib/net45/Microsoft.Identity.Client.dll).fullname) | out-null

$global:app = $null

$ClientApplicationBuilder = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create($ClientID)
[void]$ClientApplicationBuilder.WithAuthority($("$loginURL/$tenantdomain"))
[void]$ClientApplicationBuilder.WithClientSecret($ClientSecret)
$global:app = $ClientApplicationBuilder.Build()

Function Get-GraphAccessTokenFromMSAL {
    [Microsoft.Identity.Client.AuthenticationResult] $authResult  = $null
    $AquireTokenParameters = $global:app.AcquireTokenForClient($Scopes)
    try {
        $authResult = $AquireTokenParameters.ExecuteAsync().GetAwaiter().GetResult()
    }
    catch {
        $ErrorMessage = $_.Exception.Message
        Write-Host $ErrorMessage
    }
   
    return $authResult
}

$myvar = Get-GraphAccessTokenFromMSAL
Write-Host "Access Token: " $myvar.AccessToken
$global:headerParams = @{}

Function Set-AuthHeader {
$global:headerParams = @{
    "Authorization" = "Bearer $((Get-GraphAccessTokenFromMSAL).AccessToken)"
    }
}

$queryUrl = "https://graph.microsoft.com/v1.0/auditLogs/signIns"
$counter = 1
$d=(get-date -Uformat %Y%m%d).ToString()
$ReportPath = "$Env:USERPROFILE\$($d)_Report.csv"

do 
{
    
    Set-AuthHeader
    Write-Host "Getting Page " $counter
    $result = Invoke-RestMethod -Method Get -Uri $queryUrl -Headers $global:headerParams
    $content = $result.value
    # Write out content to csv file
    $content | Export-Csv -Path $ReportPath -Append -NoTypeInformation
    # Get the next page
    $queryUrl = $result."@odata.nextLink"
    $counter++

} 
until (!($queryUrl))

Write-Host "The report can be found at " $ReportPath
Write-Host "end"

Different Types of Directory Reports

You can change the Microsoft Graph query above to get other types of Directory reports. Below are all the different report types available under the auditLogs path.

User Sign-ins reports

https://docs.microsoft.com/en-us/graph/api/signin-list?view=graph-rest-beta&tabs=http

The query used in the above PowerShell code is for getting a list of all the user sign-ins report. Below are some other example queries using $filter:


List all sign-ins where appDisplayname starts with ‘Postman

https://graph.microsoft.com/v1.0/auditLogs/signIns?$filter=startswith(appDisplayName,'Postman')

List all user sign-ins between 2 different times

https://graph.microsoft.com/v1.0/auditLogs/signIns?$filter=createdDateTime ge 2020-04-06T11:54:00Z and createdDateTime le 2020-04-08T11:54:00Z


Note: The ‘$’ character in PowerShell has a special meaning to denote PowerShell variables. To use the above queries in PowerShell you need to escape the ‘$’ character using an Grave Accent or tick mark (`) character. An example of this is below:

$url = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=startswith(appDisplayName,'Postman')"

Directory Audit reports

https://docs.microsoft.com/en-us/graph/api/directoryaudit-list?view=graph-rest-1.0&tabs=http

Use the query below to get a list of audit logs generated by Azure Active Directory.

https://graph.microsoft.com/v1.0/auditLogs/directoryAudits

Provisioning reports

https://docs.microsoft.com/en-us/graph/api/provisioningobjectsummary-list?view=graph-rest-beta&tabs=http

Use the query below to get all the provisioning events in your tenant

https://graph.microsoft.com/beta/auditLogs/provisioning

Query Parameters

As of the time of this writing the Reporting API only support these query clauses: $filter, $top, and $skiptoken. Depending on object types, only certain attributes support $filter query. Check the documentation on each audit log type for more info. The $select query is not supported either so if you want to limit the number of fields saved out to the Excel file you can modify line 59 in the above PowerShell snippet as followed

# only save out these fields to csv file:  appDisplayName, appId, and ipAddress
$content | select appDisplayName, appId, ipAddress | Export-Csv -Path $Env:USERPROFILE\Report.csv -Append -NoTypeInformation

Other Report Types

There are other types of reports existing under the “reports” path. These include Office 365 usage, ADFS application activity, Application sign-in summary, and Registration and usage. These endpoints will require different API Permission set. Check out the Reports documentation for more info.

References

Microsoft-Authentication-with-PowerShell-and-MSAL
Pull Azure AD Audit Report- Updated
Fetch Data from Microsoft Graph with PowerShell (Paging Support)
Adam Edwards’s PowerShellGraphDemo

Setup Postman to call Microsoft Graph using a Client Credentials Grant Access Token from the v2 endpoint

To use the V1 endpoint, please refer to this post.  Our documentation for the client credentials grant type can be found here.

You can setup postman to make a client_credentials grant flow to obtain an access token and make a graph call ( or any other call that supports application permissions ). This is very useful for testing code you plan to run as a script or in a Daemon application where you do not want user interaction.

The first thing you need is an app registration in your Azure Tenant to perform this flow. Create a new application registration:

Note the application id as you will need it later – this is the client_id used in the setup for postman:

Click on the API permissions and assign application permissions. By default, Microsoft Graph is already selected with a delegated permission of User.Read. I will leave that there to demonstrate that permission will not be present in the access token when performing this flow. Lets add an application permission for Microsoft Graph of Group.Read.All :

Once added, you will now see your permission listed however, all application permissions always require a tenant admin to consent to these permissions:

Grant admin consent:

Next, the client_credentials flow requires a client secret. Go to the Certificates and Secrets blade and create a new client secret:

The value is only shown one time so be sure to copy it to the clipboard with the copy to clipboard button and store that somewhere safe. You don’t want that secret to get out!

The first method I will demonstrate to you for obtaining an access token with Postman is through the Authorization UI. Start a new request, then click on the Authorization tab and select OAuth 2.0 from the drop-down type list:

To configure the flow, select Client Credentials from the Grant Type drop-down box then plug in your values for the settings it requests. For the scope, I am using https://graph.microsoft.com/.default which will pull the default values configured for Microsoft Graph from the app registration – in this case, we are expecting the groups.read.all permission. Use https://login.microsoftonline.com/{your-tenant}/oauth2/v2.0/token/ for the Access token URL ( obtained from the portal “Endpoints” – see next image ), the client id from the app registration and of course, the client_secret you configured on the app registration.

The Access Token URL can be obtained from the portal in the App Registrations blade by clicking on the “Endpoints” button:

Grab the OAuth 2.0 token endpoint (v2). You can also use your tenant name in place of the tenant id ( guid ):

Save those values in the postman screen and then click on “Request Token”

You should get an access token like so:

To use this token, scroll to the bottom and click on the “Use Token” button:

You can now make a graph call ( based on the permissions you setup in the app registration – groups in this example ) – the rest endpoint in this example is https://graph.microsoft.com/v1.0/groups :

If you copy the access token and paste it in http://jwt.ms you can decode the token to see the claims it has. If you notice, our token only has permissions for group.read.all – which shows up as a “role”. Application permissions always show up as roles. Notice, there is not a delegated permission in this token because delegated permissions, even if consented to, will not appear in the token for a client_credentials grant type.

You can also just do a raw post request to the authority endpoint with the same values like this, again, you need to add the client_id, client_secret, scope but you also need to add grant_type when doing it this way and for that value, be sure to use “client_credentials”:

The values go in the body of the request in this scenario.

Summary:

You can use postman to obtain an access token for testing your api calls, etc. using the client_credentials flow as demonstrated.

Setup POSTMAN to get Azure access tokens automatically for you

Introduction

Postman is an HTTP request tool that is very handy for developing and testing your Azure requests. This is the Postman website: https://learning.getpostman.com/

Postman does make it easy to setup authentication and acquire access tokens but it normally is a multi-step process. The purpose of this blog post is to show you how you can setup Postman to automatically handle authentication for you so you don’t have to go get a new token manually to test with. This example will concentrate on using the Client_Credentials flow targeting Microsoft Identity Platform V2 endpoint.

Client credentials flow V1 endpoint

Client credentials flow V2 endpoint

Setup the Environment

The first step in this process is to setup your environment so you can create variables that will be used in your headers and or body. To do this, you can use the “New” button on the upper left corner of the screen. There is also a gear icon in the upper right hand corner of the screen where you can create the environment as well but lets start with the New button

Click on “New” Ă  “Environment” and give it a meaningful name. In my example, I am using a dummy tenant name.

Lets add some environment variables in the Variable chart for that environment. You could add the variables on the collection side but I prefer using the environment so I can switch values easily just by selecting the environment.

Add the following variables:

  • client_id
  • client_secret
  • token_endpoint
  • scope
  • access_token

Set the initial and current values on the variables. Make sure you have a properly setup app registration with Microsoft Graph application permissions for User.Read.All to test this script and you have performed admin consent on those permissions. Also, note that all variables are case sensitive!

The V2 endpoint would look like this: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token/ (where {tenant} is where you would also add the tenant id – you cannot use a variable here )

Note: If you’re going to hit the v1 endpoint, you will not be using a scope parameter and would instead, need the resource parameter.

The scope would obviously depend on the resource you’re needing a token for. For Microsoft Graph, it would be: https://graph.microsoft.com/.default — just pulling the Microsoft graph permissions off your app registration. Your access token would then be for Microsoft Graph.

Next, create a collection to save your scripts to

Click on “New” Ă  Collection. Give your collection a meaningful name and description if you like, then on the Pre-requests Scripts tab, add this script:

[gist id=”9bb52b6f4c4df275a7b6350e1a10fbd4″]

Then click update.

Build a simple Test Request

Now, build a simple request and save it into the Collection folder you have created. You can build a new request by right clicking on the new collection you’ve just created and then selecting “Add Request” and it will automatically be added to the collection.

See how I’ve used my variable names in key places for this request.

My URL for the GET request is: https://graph.microsoft.com/beta/users

I’ve added one simple Header for “Authorization” and set the value to: “Bearer {{access_token}}”

If you did not right click on the collection to add a new request, you can save the request setup to the collection by clicking on the Save drop-down box, then Save As:

Be sure to select the collection you created that has the pre request script on it and then save:

Run the request

When running your request now, make sure you have the correct environment selected to get the proper variable values

Click the send button and you should get the list of users in your tenant.

View the console for additional information

As you can see in the script, I am outputting some information for you into the console window. You can show the console window before running the script by clicking on View Ă  Show Postman Console

You can see information in the console window such as the token that was generated and the calls that were made:

Summary

You can setup postman to make building requests for testing and troubleshooting purposes for the client_credentials flow by easily setting up a few variables, adding the pre-request script and then plugging the variables into your request. Each time the request is sent, you can get a new access token and use that as the bearer token for the request. You can open the console screen in postman (View/console) and see the token that was generated if you want to view that in http://jwt.ms as the script is outputting the token into the console, for additional troubleshooting purposes.

References

https://liftcodeplay.com/2018/03/18/how-to-automatically-set-a-bearer-token-for-your-postman-requests/

Walkthrough: how to retrieve an Azure Key Vault secret from an Azure Function App using client credentials flow with certificate

Introduction:

This post builds on the information from the previous post and I will assume that you already have an Azure Key Vault, an AAD Application registration, and a certificate file. We will cover the following steps in this blog in order to use a certificate from an Azure Function App:

  1. Create an Azure Function App
  2. Upload the certificate to the Function App
  3. Configure the Function App to load certificate file
  4. Create an Azure Function to consume the certificate

Create an Azure Function App

From the Azure Market Place in the Azure portal, create an Azure Function App. The Hosting Plan can either be Consumption Plan or App Service Plan.

Upload the certificate to the Azure Function App

Go to the Function App resource we just created => click on Platform features tab => click on SSL link

From the SSL blade => click on Private Certificates (.pfx) tab => click the Upload Certificate link

Go through the Certificate Upload wizard to provide a pfx file and password. Once the upload is complete, the certificate info should appear under the Private Certificate section

Configure the Function App to load the certificate

From the Function App blade, click on the Application settings link under the Platform features tab

Under Application settings section, add a new setting called WEBSITE_LOAD_CERTIFICATES and set the value to be the certificate thumbprint. Click on Save button to save the new setting

Create an Azure Function

From Visual Studio 2017, create a new Azure Functions project. If you don’t see the “Azure Functions” template, you may need to install “Azure Functions and Web Jobs Tools” extension. See https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs for more info.

For the Function trigger, I just choose Timer trigger for the purpose of this blog. You an choose any trigger you like. Trigger is just a mean to invoke an Azure Function. If you have a storage account then go ahead and configure the storage account for the Function under Storage Account setting. The schedule setting uses CRON expression to define a timer schedule for the Function App. The prepopulated expression 0 */5 * * * * means that the Function is invoked every 5 minutes.

Now we need to write the code for this Function:

  1. Install the following Nuget packages from the Package Manager Console:

Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory

Install-Package Microsoft.Azure.KeyVault

  1. Replace the code in Function1.cs with the following code:

[gist id=”069082b4a4a2df7b0e3f9ef4ccf4498b” file=”Function1.cs”]

  1. Build and publish this Azure Function to our Azure Function App by right-click on the Project name and select “Publish…”

    Note: If you have not done this already, make sure you log into Visual Studio with your Azure AD account.


Select the target to be our existing Azure Function App and click “Publish”

Select the right Azure Function App under the right Resource Group and click OK to publish

Once the publishing is finished, we should see our Function 1 appearing in the Azure portal. You can click on the “Run” button to see the output in the Logs section at the bottom.

References

https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs

https://azure.microsoft.com/en-us/blog/using-certificates-in-azure-websites-applications/

Using Postman to Call the Microsoft Graph API Using Client Credentials

Introduction

This blog post is to help users stand up an Azure Active Directory Application Registration, explain what permissions will be needed to added to the AAD Application Registration, how to make rest calls to get an access token, and then finally call the Microsoft Graph API to do whatever call you would like to do.

 

Please note, that not all permissions are going to be within Azure. i.e. If you are making a Microsoft Graph call to Outlook and would like access to a calendar using a delegated permission, and the user making the call doesn’t have access to said calendar, the call still will not work as the user does not have access to the calendar.

 

Also Please note that there are two different versions of the Graph API, the Azure Active Directory Graph API and the Microsoft Graph API.

In addition to that, note that the Microsoft Graph API has two endpoints right now, the beta endpoint and the V1 endpoint, do not confuse this with the v1 and v2 endpoints for Azure portal as the azure v1 and v2 endpoints are not related to the Microsoft Graph’s v1 and beta endpoints.

 

Setting Up the AAD Application

The first step to getting access to the Microsoft Graph REST API is to setup an AAD Application Registration.

First we are going to want to create the AAD Application registrations in the portal. For this we will need to configure the application to be able to work with Postman so that we can make the call to the Microsoft Graph API. First we go to the Azure Active Directory Blade, go to App Registrations, and then create a new application registration.

2018-03-31 19_16_00-Create - Microsoft Azure - Internet Explorer

From there we are going to want to create a web app with any name. Here I have set the name as web app, and set the Sign-On URL as the callback for Postman: https://www.getpostman.com/oauth2/callback, it is unimportant what the callback url is, but for our case we are using that callback URL for consistency sake. Just make sure that the callback you put down for your AAD Application Registration is the same in your postman call. This is unimportant for the grant type client credentials but more important for other grant type flows.

image

You will have to click out of the sign-on URL to make it check whether or not if it’s correct.

After that we have created our web app, we will want to create a secret. Please keep track of the secret as you won’t be able to see the secret again. You will have to press save in order for the secret to generate.

 

image

With this information in hand, we will be able to move forward and connect to this AAD registration. But without the correct permissions we won’t be able to get an access token to make calls to the Microsoft Graph API.

 

Finding Which Permissions We Need for a Microsoft Graph Call

Assuming we would like to have granular control on what the AAD Application registration has access to and what it doesn’t have access to. We are going to want to make sure that the AAD Application registration only has the permissions it needs to make the MSFT Graph API calls that we are wanting to make.

There has been a separate blog post on finding the correct permissions for your graph API call listed below :

https://blogs.msdn.microsoft.com/aaddevsup/2018/05/21/finding-the-correct-permissions-for-a-microsoft-or-azure-active-directory-graph-call/

 

For this client credentials flow, we will want to set the required permission for Read all users’ full profiles under Application Permissions. If you check the delegated permission you won’t get the correct permissions because the client credentials flow only gets the application permissions. This permission is shown below.

 

image

 

 

Accessing the AAD App Registration and Calling the Microsoft Graph API

 

Now lets get a secret for our AAD Application registration by going to the keys blade and adding a secret. Please make sure to keep track of this key as you will not be able to retrieve it again once you leave the blade.

 

image

 

Be sure to press the save button in order to see the value after putting in the description for your key.

 

Now our AAD Application registration is ready to go and we can utilize postman to get an access token using the AAD Application registration to use the list users functionality in the Microsoft Graph API.

 

Please keep track of the client id as well which is the application id for your app registration. This is shown below.

image

 

We are going to want to get our token and authorize endpoints, we can find this next to the new application registration button in the App Registrations blade shown in the picture below.

 

image

 

Now we can go to our postman to try to get an Access token. Below is a screenshot of how the postman should be setup with the variables we got from the steps above.

 

We will use the token endpoint as the URL to post, we will then add the client id, client secret, resource, and grant type in the body of x-www-form-urlencoded fields.

 

The only item that isn’t gotten in the steps above is the resource which is different depending on what you’re trying to access. Here we are trying to access the Microsoft Graph which is https://graph.microsoft.com you will need to look through your respective documentation to find what the resource url is for the resource you’re trying to utilize if it’s not the Microsoft Graph.

 

image

 

After this we have now received an Access token back from the token endpoint for your Azure Active Directory tenant. With this access token we can create a new request with a single authorization header and the list user calls to the Microsoft Graph API.

 

image

 

We to have a header with the value as “Bearer <access token>” with the key “Authorization”.

 

image

 

We have now gotten the list of users from the tenant that the AAD Application Registration.

 

 

Conclusion

After going through the steps to create an AAD Application registration, finding what call we want to make, finding what permissions we need to make the call and then getting all the configurations we need to get an access token from the token endpoint. We were able to get an access token from the token endpoint and then make a call to the Microsoft Graph API with our access token to list the users in our tenant. I hope that you can utilize this flow to make the other calls to the Microsoft Graph API. If you have any issues feel free to open a support ticket and one of our support engineers will reach out to you as soon as possible, please be sure to have a fiddler trace of the error that you are experiencing.

Implementing Service to Service Authorization and Getting the Access Token with Postman Utilizing Client Credential Grant Type

Introduction

This article is meant to show how one can set up a client application to obtain a service to service access token, to get access to a web API from a web App. This document will be following the grant type client credential flow to do this, and will utilize Postman to get the access token via client credentials. This tutorial will not set up the backend web API, and assumes that web API is validating the token, you can click this link on validating the JWT token here. Also in addition to that, the link included assumes that the Audience has the correct permissions. As the client credentials flow can get a different audience within your tenant, the end of this article will review what additional steps, to make sure that the access token has the correct permissions.

 

Creating the AAD Application Registrations in the Portal

First we are going to want to create the AAD Application registrations in the portal. For this we will implement the application to be able to work with Postman so that we can display getting the access token pretty easily. First we go to the Azure Active Directory Blade, go to App Registrations, and then create a new application registration.

 

 

2018-03-31 19_16_00-Create - Microsoft Azure - Internet Explorer

 

From there we are going to want to create a web app with any name. Here I have set the name as web app and then we want to set the callback url to : https://www.getpostman.com/oauth2/callback and set the application type to web app/ API.

 

image

 

You will have to click out of the sign-on URL to make it check whether or not if it’s correct.

 

After that we have created our web app, we will want to create a secret. Please keep track of the secret as you won’t be able to see the secret again. You will have to press save in order for the secret to generate.

 

image

 

With this information in hand, we will be able to move forward and connect to this AAD registration. But without the correct permissions we won’t be able to connect to the web API registration with an access token from this web API.

 

image

 

So here I create the API registration, with the sign-on URL as the localhost. How you plan on setting up your application will change what the sign-on URL will be.

 

 

Modifying the Application Manifest to Add Your Own Permissions

Since we are utilizing Client Credentials, we will need to add the permission in the Web API as an Application Permission as the Client Credential doesn’t utilize the delegated permissions. In addition to that we will most likely want to define what sort of permissions the web app should have for the web API. In order to do this we will have to add these permissions directly into the web API’s application manifest. You can do this by going to the AAD Application registration, clicking on Manifest and then adding the field “approles” into it. This is shown below.

The approle I added here is below. By setting the allowedmembertypes to “Application” we are setting it as an application role.

 

“appRoles”: [
{
“allowedMemberTypes”: [
“Application”
],
“displayName”: “Test Application Permission”,
“id”: “3ea51f40-2ad7-4e79-aa18-12c45156dc6a”,
“isEnabled”: true,
“description”: “I am a Test Application Permission.”,
“value”: “Test.Application.Permission”
}
]

 

To learn more about these values you can go to the link here describing the application manifest :

https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-application-manifest

 

To learn more about the App roles for an application please go here:

https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles

 

That being said, the one thing to note is that the allowedmembertypes isn’t described fully in the documentation above. By setting it to User, we set it as a delegated permission and have to ability to do RBAC access with the delegated permission. By setting it to an Application, we are able to set it as an application permission, letting anyone who can access the application to be able to access web API with the permissions granted to the web app.

 

You can learn more about the differences between delegated and application permissions here.

 

If you are interested in utilizing RBAC to control access in a service to service scenario you can follow this guide :

https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-webapp-roleclaims/

 

In addition to that, the ID is created by utilizing the method below in PowerShell.

[System.Guid]::NewGuid()

 

Adding the Permission into the Web App

After adding the application permission in the application manifest, we are able to add the required permission to the web app in order to get an access token with the correct claims saying that this web app has application access to the web api.

image

 

 

image

 

 

Now we have the Application permission and we will be able to get it in the claims when we authenticate with the web app. Make sure to grant permissions, otherwise you won’t see the role in the access token as the application technically hasn’t been given permission yet as it requires a global admin to give the application access to the permission. Notice how it says yes under require admin in the last picture.

 

 

image

 

 

 

 

Getting the Access Token with Postman

 

After granting permissions for the application, we are now able to authenticate with the application and get the correct claims and audience in the access token. We will utilize client credentials to get an access token from Web App’s application ID with the secret and then with the resource set as the app ID URI for the web API, we will get the audience as the app id uri, and the roles for whatever permissions were granted to the web app.

 

image

 

 

 

image

 

 

 

image

 

image

 

From here, you can access the web api, assuming that you have set it up to authorize based on the audience, role, iss, and signature is correct (i.e. the access token hasn’t been tampered with and it is issued from the AAD tenant with the AAD Application registration in it). For more information on validating a JWT token you can find that here.

 

Conclusion

 

We have now gone through all the steps to set up the AAD application registration, modify the application manifest in the web API to create the permissions, add permissions in the web app aad application registration, grant permissions, and get the Access token using Postman to see if all the claims in the JWT token are correct. After all of this your web api will need to still manually validate that the jwt token hasn’t been tampered with and that all the claims are correct. After doing all these steps you should have now setup a service to service authorization using client credentials.