Receiving Error “AADSTS900439 – USGClientNotSupportedOnPublicEndpoint”

Problem:

This blog provides more information about the error “AADSTS900439 – USGClientNotSupportedOnPublicEndpoint”.  This error typically occurs when a user uses a public cloud endpoint to sign in to an application registered in Azure Government sovereign cloud.

It is known fact that the official Azure Active Directory (AAD) Authority for Azure Government changed from `https://login-us.microsoftonline.com` to `https://login.microsoftonline.us`. This change also applied to Microsoft 365 GCC High and DoD, which Azure Government AAD also services.

Azure AD will now start enforcing the correct sign in endpoint.  One can no longer sign in to an application registered in a Azure Government cloud using the public .com endpoint.  The error happens due to this violation.

Here is the documentation on Azure Government Endpoint Mappings, which shows the mapping between some Azure services and Azure Government endpoints. Few to mention:

NameAzure Gov Endpoint
Portalhttps://portal.azure.us
Microsoft Graph APIhttps://graph.microsoft.us/
Active Directory Endpoint and Authorityhttps://login.microsoftonline.us

Each national cloud environment is unique and different than the Microsoft global environment. It is important to be aware of some of these below key differences when you develop applications for national cloud environments.

For example:- Registering Applications – App Registration Endpoints, Acquiring Tokens – AAD Authentication Endpoints, and calling the Microsoft Graph API can be different.

This article provides information about the different Microsoft Graph national cloud deployments and the capabilities that are available to developers within each.

Here is the sample for implementation : https://blogs.aaddevsup.xyz/2020/06/configure-net-application-to-call-microsoft-graph-in-a-national-cloud-tenant/

References:

Using PostMan to request an Azure AD token via SAML Assertion Grant flow

Azure Active Directory (Azure AD) supports an OAuth2 Extension Grant called “SAML Bearer Assertion flow” which allows an application to request an JWT OAuth2 token from Azure AD by providing a SAML Assertion (Token) acquired during an authentication process to a different Authorization Server. As you can imagine in order for this token exchange mechanism to happen, a trust relation between Azure AD and that Authorization Server must have already been established via a process called Federation. A common scenario is a hybrid environment where Active Directory Federation Server (ADFS) is used to federate between an Active Directory on-prem domain with Azure AD.

In this blog post I’ll show how to use Postman to request a SAML token (or SAML Assertion) from ADFS server using WS-Trust protocol and then use that SAML token to exchange for an OAuth2 JWT token from Azure AD. There are a few pre-requisites:

  1. Create an App Registration in Azure AD. For this demo I create a single tenant application and set the default client type to be public by selecting ‘Yes’. If you set ‘No’ on the Default client type, you will also need to provide a secret later on when exchanging a SAML Assertion for the OAuth2 JWT token.
  1. Configure the Application permission with at least a delegated Microsoft Graph ‘User.Read’ permission and grant Admin Consent on the permission.
  2. on-prem domain already federated with Azure AD via ADFS Server.
  3. Testing needs to be done with a domain user account synced to Azure AD.

Performing Token Acquisition

Getting a SAML Assertion from ADFS

  1. The first step in using SAML Assertion Grant flow is to get a SAML Assertion from ADFS (or whoever your Federation IDP is).This is typically done via WS-Trust protocol. There are a couple of ways to find the correct endpoint to send the WS-Trust request to.
    1. Send a Home Realm Discovery request to Azure AD to get the ‘federation_active_auth_url’ value
    2. GET https://login.microsoftonline.com/common/userrealm/user@contoso.com?api-version=1.0
      or
    3. Find the usernamemixed endpoint from ADFS’s Tools Management Console

Note: There are a couple of usernamemixed endpoints, use the WS-Trust 2005 type.

Send the POST request to the federation_active_auth_url (aka usernamemixed endpoint) with the following parameter in PostMan:

As noted, you need to supply your own parameters (username, password, and ADFS usernamemixed URL) in the request on line 25,29, and 30:

[gist id=”31594d7f0e64586b78e4c13c71e9f94a” file=”WSTrustBody.xml”]

Your PostMan set up should look like the following:

HTTP Headers

And if everything goes right you should get a SAML Response from the ADFS Server like the following.

Note: In this case ADFS returns a SAML 1.1 token as indicated in the highlighted Assertion (MajorVersion = 1, MinorVersion = 1)

Exchanging a SAML Token for JWT Token

  1. The second step in the SAML Assertion Grant flow is to exchange the SAML Assertion acquired from ADFS Server for a JWT OAuth2 token from Azure AD.

In PostMan change the response view from the last step to Raw View and search for the substring <saml:Assertion …>….</saml:Assertion>


Copy the entire Assertion substring including the node name and paste it into a Base64 encoder of your choice. I use this online tool to do the encoding. We need to base64-encode this SAML Assertion


Copy out the base64-encoded output. This will be used for the next PostMan request.

Note: It is very important that there should not be any spaces between the different nodes in this Assertion substring when doing the encoding since having the spaces can change the encoded output and can cause unexpected error such as ‘AADSTS50006: Signature verification failed because of an invalid signature’ in the following token exchange step.

Use PostMan to perform the following request:

POST https://login.microsoftonline.com/<tenant name>.onmicrosoft.com/oauth2/v2.0/token

POST Body:

grant_typeurn:ietf:params:oauth:grant-type:saml1_1-bearer
client_idyour Application ID
assertionbase64 encoded SAML Assertion
scopehttps://graph.microsoft.com/.default

And you should get a JWT token from this grant:

Note: I set the grant_type parameter to be “urn:ietf:params:oauth:grant-type:saml1_1-bearer” because this is a SAML 1.1 token/Assertion. If the Authorization server returns a SAML 2.0 token (Version attribute in the Assertion element is “2.0”), the grant_type parameter should be set to “urn:ietf:params:oauth:grant-type:saml2-bearer”.

By default, ADFS Server returns a SAML 1.1 token. To specify the version of the returned SAML token, you can use the TokenType element with the value in the following table in the WS-Trust request body.

SAML Token VersionTokenType Value
1.1 Tokenhttp://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1
2.0 Tokenhttp://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0

source: https://www.oasis-open.org/committees/download.php/16768/wss-v1.1-spec-os-SAMLTokenProfile.pdf

Here is an example of a request for a SAML 2.0 Assertion:

Configure .Net Application to call Microsoft Graph in a National Cloud Tenant

When authenticating with Azure and making a Microsoft Graph request, for commercial tenants, this is all done with the .com endpoints ( https://login.micorosoftonline.com/… and https://graph.microsoft.com ) and your token audience is for the .com endpoint as well. However, when performing the requests against a National Cloud tenant, you must use the appropriate endpoints. In this article, I will show you how to configure the Microsoft Graph .Net SDK in a .Net application for use in a US National Cloud.

In this sample, I will be using the Graph Authentication Provider class for the client_credentials grant flow. I previously wrote about this class here. To use the sample, you will need to configure an app registration in your tenant and grant it the Microsoft Graph User.Read.All permission as the sample request will pull a list of users in your tenant. You will also need to configure a client secret since this is required for that flow. You can download the sample here.

In the sample, you can configure the 2 endpoints in the AuthSettings.cs file. The sample is already set for the .us endpoints. You will need to set the tenant_id, the client_id ( app id ) and the client_secret.

namespace GraphServiceClient_ConfidentialClient
{
    class AuthSettings
    {
        public static String instance = "https://login.microsoftonline.us"; // gov tenants would be .us where commercial would be .com
        public static String graph_endpoint = "https://graph.microsoft.us"; // gov tenants would be .us where commercial would be .com
        public static String graph_version = "v1.0"; // values are v1.0 or beta
        public static String tenant_id = ""; // can be the tenant guid or name
        public static String client_id = ""; // the app id of the app registration in the tenant
        public static String client_secret = ""; // a client secret must be configured on the app registration for the client credentials flow
        
        public static String authority = $"{instance}/{tenant_id}";
        // for the client credentials flow, must always use the .default and any application permissions 
        // consented to will appear in the token. You cannot do dynamic scopes
        public static String scope = $"{graph_endpoint}/.default"; 
        public static String graph_baseURL = $"{graph_endpoint}/{graph_version}";
    }
}

Compile and run and you should see a list of users from your national cloud tenant like this:

Some key points here. The Scope string is going to use the .us endpoint from the graph_endpoint value. If you do not provide a scope, the Graph Service Client will default to the .com endpoint. Also, you will see that in the Program.cs file, in the Get_Users method, line 42, I specify the base url endpoint. If you do not provide the base url, the Graph Service Client will default to the .com endpoint. These are the 2 main sticking points for national cloud tenants as they try to use the graph client on their tenant.

And that is it!. There are only a couple of things that must be changed to get the graph client to point to your national cloud endpoint.

Receiving error “Change enumeration is not supported for requested tenant.” from MS Graph Delta query

Problem

Customer receives the following error when running an MS Graph delta query, for instance the following:

GET https://graph.microsoft.com/beta/users/delta
'error': {
   'code': 'Request_UnsupportedQuery',
   'message': 'Change enumeration is not supported for requested tenant.',
   'innerError': {
      'request-id': 'xxx',
      'date': '2020-05-22T13:17:45'
   }
}

Root Cause

This error can happen if the tenant is an Azure AD B2C tenant.

More Information

Differential or Delta query is currently not supported in an Azure AD B2C tenant. It only works for a regular tenant. There are some discussion of this in the following links:

Delta Query for Azure AD B2C

Azure AD B2C Change enumeration is not supported for requested tenant

If you are interested in having this feature implemented, please vote up on the following feedback request:
Support MS Graph Delta for Azure AD B2C tenants

How to get and display the user photo with Graph SDK for .Net in a WPF application

Occasionally, we get requests asking how to get the user photo using the Graph SDK. This blog post will show you how to do that in a WPF application but I will also show you a method for getting the photo and saving it to disk with a .Net console application.

You can download the sample project here: https://github.com/RayGHeld/GraphClient_GetUserPhoto This project also utilizes the Authentication Provider code in this blog post.


You will need to modify the AuthSettings.cs file with your own app registration settings. Specifically, the client_id, tenant_id and redirect_uri values. But first, please ensure you have created an app registration for this project. The only permissions needed for this is the Microsoft Graph User.Read permission, which is enabled by default when you create an app registration in Azure. For the redirect uri, in the Authentication blade, I just selected a mobile platform for MSAL only.

Running the project is really basic. When loading, it will prompt you for credentials and then immediately, display the user photo for the logged in user. There is a requirement for the photo to exist — you must have an O365 license associated with the user and that photo uploaded to the user. If not, then no photo will exist. Otherwise, you will get a simple screen that shows the photo, name and title for the user like so:

Please note: this may or may not be my actual photo 😉

namespace GraphClient_GetPhoto_WPF
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private static GraphServiceClient graphClient = GraphClient.ServiceClient( AuthenticationProviders.InteractiveAuthentication.AuthenticationProvider );


        public MainWindow()
        {
            InitializeComponent();

            Get_Me(imagePhoto, userInfo);
        }

        static async void Get_Me(System.Windows.Controls.Image img, System.Windows.Controls.Label label)
        {
            User u = null;
            try
            {
                u = await graphClient.Me.Request().GetAsync();
                
                label.Content = $"{u.DisplayName}\n{u.JobTitle}";
            } catch ( Exception ex )
            {
                Console.WriteLine( $"Error getting user info: {ex.Message}" );
            }

            try
            {
                using ( Stream stream = await graphClient.Users[u.Id].Photo.Content.Request().GetAsync() )
                {

                    BitmapImage bitmap = new BitmapImage();
                    bitmap.BeginInit();
                    bitmap.StreamSource = stream;
                    bitmap.EndInit();

                    img.Source = bitmap;
                }
            }
            catch ( Exception ex )
            {
                Console.WriteLine( $"Error getting image: {ex.Message}" );
            }

        }

    }
}

WPF is a little bit tricky since I have to pass in the control in order to be able to update it.  I referenced this great blog for how to do this since I am not a WPF developer:  https://www.c-sharpcorner.com/UploadFile/mahesh/using-xaml-image-in-wpf/

Now, in a console application, here is how you can simply download the image if needed:

        static async Task Get_Photo(String path)
        {
            try
            {
                using ( Stream stream = await graphClient.Me.Photo.Content.Request().GetAsync() )
                {
                    System.Drawing.Image image = System.Drawing.Image.FromStream( stream );
                    image.Save( path );
                    Console.WriteLine( $"Image saved to: {path}" );
                }
            } catch ( Exception ex )
            {
                Console.WriteLine( $"Error getting and saving image: {ex.Message}" );
            }
        }
    }

If performing this from the Graph Explorer tool, you must use the $value query parameter to get the image to display in that tool like so:

And that is it.  As demonstrated, with a little bit of work, you can get the graph client to retrieve and display or save a user photo if one exists for the user.

Using Azure Management Libraries for .NET to manage Azure AD users, groups, and RBAC Role Assignments

In this blog, we will use Azure SDK for .NET to perform the following tasks:

  1. Create an Azure Active Directory (Azure AD) user, query for the created user, and delete the user.
  2. Create an Azure AD group, query for the created group, and delete the group.
  3. Add a user to the group’s members.
  4. Create an Role-based Access Control (RBAC) Role Assignment: we will assign the group created above ‘Storage Blob Data Reader’ role to an Azure Storage Blob container.

Note: RBAC Role Assignment is performed via Azure Resource Manager (ARM) REST endpoint.

As of version 1.33.0, Azure Management Libraries for .NET uses ADAL.NET to authenticate to Azure AD using Client Credentials Grant Flow via the legacy V1 endpoint. It also uses Azure Active Directory Graph (https://graph.windows.net) to query Azure AD Directory Objects.

Note: In general we encourage customers to use the feature-rich MSAL.NET library to authenticate to Azure AD instead of ADAL.NET library. Furthermore using Microsoft Graph REST endpoint (https://graph.microsoft.com) is recommended over the legacy Azure AD Graph (https://graph.windows.net) REST endpoint for managing Azure AD Directory objects.

Prerequisites:

  1. Create an Application Registration in Azure AD. Configure Azure Active Directory Graph Application permission: Directory.ReadWrite.All, and grant Admin Consent to the configured permissions. This permission is needed to create user and group objects. You also need to create a client secret from the “Certificates & secrets” blade.
  1. Add this Application (really Service Principal) to Azure AD’s ‘User Administrator‘ role. This role is essential for deleting the user and group object since the Application Permission Directory.ReadWrite.All does not include user and group deletion per documentation. From the Azure portal, select ‘Azure Active Directory’ -> Roles and Administrators -> User Administrator -> Assignments -> Add Assignment -> add your application to this role.
  1. In order to manage RBAC App Role Assignments, we need to give the Application either the Owner or User Access Administrator RBAC Role at either the resource level or any parent level as documented here. In this example I give the Owner role to this Application at the Azure Storage level since we will be creating an App Role Assignment for the Azure Storage’s Blob Container.

  1. Get the Role Definition ID for ‘Storage Blob Data Reader’ role. You can use the following Az PowerShell commands to get this:
# Make sure you install the PowerShell Az module before running the following command.  See https://docs.microsoft.com/en-us/powershell/azure/new-azureps-module-az?view=azps-3.8.0 for more info.

Connect-AzAccount -Tenant "<Tenant Name>"
Get-AzRoleDefinition -Name "Storage Blob Data Reader" | Select Name,Id

#  Here is my my output:
<# 
Name                     Id
----                     --
Storage Blob Data Reader 2a2b9908-6ea1-4ae2-8e65-a410df84e7d1
#>

The Visual Studio Project

  1. From Visual Studio 2019, create a new C# Console App (.Net Framework)
  2. Install this Nuget Package: Install-Package Microsoft.Azure.Management.Fluent
  3. Replace the relevant sections in your Program.cs file with the following code containing your Application Registration info and the ARM path in the Role Assignment section:

[gist id=”c3dd1385e23ceb3f34c9cab163646979″ file=”Program.cs”]

Running the application should produce the following result after creating user, group, and role assignment:

Group and User created

REST endpoint for Role Assignment

If you want to do the above RBAC Role Assignment using Azure Resource Manager (ARM) REST endpoint the request is below:

PUT https://management.azure.com/{scope}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName}?api-version=2018-01-01-preview

Body:
{
  "properties": {
    "roleDefinitionId": "/{scope}/providers/Microsoft.Authorization/roleDefinitions/{roleDefinitionId}",
    "principalId": "{principalId}"
  }
}

Note:  
roleAssignmentName can be any GUID created from GUID Generator tool
principalId - this is the Object ID of the Azure AD Directory Object (Group Object ID)
roleDefinitionId - this is the RoleDefinitionID from step 4
scope - subscriptions//resourceGroups//providers/Microsoft.Storage/storageAccounts//blobServices/default/containers/

For other Azure Manage for .Net samples, see https://github.com/Azure/azure-libraries-for-net/tree/master/Samples

Control access to your apps in Azure AD

We get this kind of question all the time. It comes in many variations and forms like…

“I only want to consent for some users to access the app.”

“I only want my service account to access this app”


Before we get started…

First and foremost, only consenting for allowed users is not the solution. This is not the purpose for consent. Consent is to inform a user or admin what the application is accessing and to give the user or admin an option to accept or deny the requested permissions. Administrators should not be using this to determine who has access to an application. Once the permissions are consented, then we will only allow the app to access the requested permissions and no more.

For more information about the Azure AD Consent Framework…

https://docs.microsoft.com/en-us/azure/active-directory/develop/consent-framework


Let’s get started…

If you want to control access to an application, then you should be enabling the requirement of user assignment on the Enterprise application then assign the user, group, or service principal to the application.

First: Perform admin consent

Make sure an administrator has performed an Admin consent on the required permissions. This is a requirement in order to restrict user access. Otherwise, you will see various consent related messages or you need admin approval.

For troubleshooting consent issues, see the following article…


Second: Enable User assignment required

https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/assign-user-or-group-access-portal#configure-an-application-to-require-user-assignment


Third: Assign users, groups, service principals

Once user assignment is required, now assign the user, groups, or service principal to the application…

If it’s a Web Application…

https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/assign-user-or-group-access-portal#assign-users-or-groups-to-an-app-via-the-azure-portal

If it’s a Public Client…

If the application is enabled as a Public Client type so that you can use Windows Authentication, Resource Owner Password Credential flow, or Device Code flow, then this is a bit trickier…

  1. First, disable the Public Client configuration within the application registration…
  2. Then perform the step above under “If it’s a Web Application” to assign users, groups, or service principals to the application.
  3. Then re-enable the Public Client configuration.

Optionally, you can also use Azure AD PowerShell… (Especially if you do not own the application to disable Public client type.)

https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/assign-user-or-group-access-portal#assign-users-or-groups-to-an-app-via-powershell


When user assignment is required…

Once user assignment is required, then only those users will be able to access the application. Users who do not have access will get the following similar error message…

AADSTS50105: The signed in user ‘{EmailHidden}’ is not assigned to a role for the application ‘{app-id}'({app-display-name}).


Other Tips…

You can also do this for other resources such as an API. Just follow the steps above applying them to the resource’s Enterprise app. Be careful and don’t do this for Microsoft first-party apps as you might break apps like Outlook, Power BI, SharePoint, ect…

Troubleshooting consent in Azure AD

This is a general guide for troubleshooting consent in Azure AD. It will help resolve majority of the consent related scenarios (Not all of them). In general, the application is trying to sign-in or get an access token for a resource which has not been consented by the user or admin. In general, you want to make sure all of the permissions needed by the application have been consented to.

This article only applies to OpenID Connect and OAuth2 based authentications. SAML based applications which may throw the same type of errors however may have a different solution. Generally, the issue will be inside the SAMLRequest. Either the configuration on the third-party SAML Service Provider or Azure AD does not match.


Overview

You get one of the following similar messages…

  • Need admin approval
  • AADSTS65001: The user or administrator has not consented to use the application with ID ‘{App-Id}’ named ‘{Name-of-App}’. Send an interactive authorization request for this user and resource.
  • AADSTS650056: Misconfigured application. This could be due to one of the following: The client has not listed any permissions for ‘AAD Graph’ in the requested permissions in the client’s application registration. Or, The admin has not consented in the tenant. Or, Check the application identifier in the request to ensure it matches the configured client application identifier. Please contact your admin to fix the configuration or consent on behalf of the tenant.
  • AADSTS90094: An administrator of <tenantDisplayName> has set a policy that prevents you from granting <name of app> the permissions it is requesting. Contact an administrator of <tenantDisplayName>, who can grant permissions to this app on your behalf. 
  • AADSTS90008: The user or administrator has not consented to use the application with ID ‘162841d6-3c61-4676-a2c1-5a9c1e68ccf3’. This happened because application is misconfigured: it must require access to Windows Azure Active Directory by specifying at least ‘Sign in and read user profile’ permission

As you can see from the list above, there are lots of variations of how we basically say, an admin needs to consent to the application.

There are a lot of different reasons for getting a message about admin approval or admin consent is required, or one of the other various messages.

Here are some of the high-level scenarios of what to look for…

  • User.Read permission is missing.
  • User consent is disabled.
  • User Assignment required is enabled.
  • Service principal does not exist in tenant for client app.
  • Service principal does not exist in tenant for resource.
  • Hitting the consent url (prompt=admin_consent & prompt=consent).
  • Scopes requested in sign-in request that have not been consented to yet.
  • The scope/permission requires Admin consent.
  • User Consent Blocked For Risky Apps

Simply adding permissions to an application registration is NOT consenting to the permissions. This is probably the most common mistake. So, what do we mean when we say “consented” to permissions?

Understand Application registrations vs Enterprise applications

In Azure AD, we have an application model that consists of “Application” objects also called “Application registrations” and “ServicePrincipal” objects also called “Enterprise applications” and how their relationship works together based on the required permissions set up on the Application object.

To learn more about that relationship, review the following article…

https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals

Assigning permissions to the ServicePrincipal object is what defines when we say “consented” to.

Understand Delegated permissions vs Application permissions

There are two types of permissions: Delegated and Application permissions.

To learn more about permissions in Azure AD, review the following articles…

You need to make sure you apply the correct permission configuration in the Application registration and consent to that permission.

Understand the Azure Active Directory Consent Framework model

There is a reason why we are asking for consent. In most cases, the sign-in request or based on the application configuration, something has not been consented to yet that requires consent. Understand our consent framework…


Make sure the Application has the permission to allow a user to sign in. (I.e. User.Read permission)

First and foremost before we go into any troubleshooting, lets make sure your app allows users to sign in.

If you own the Application registration, at minimum, any application where you expect a user to sign in, you should at least have the Microsoft Graph “User.Read” or “Openid” Delegated permission added to the Application registrations API Permissions and the permission has been consented to…


Troubleshooting

I suppose that’s enough of concepts and theory. Let’s start with the troubleshooting.

Step 1: Get the sign-in request sent to Azure AD.

Based on the parameters being passed to Azure AD, we can start figuring out why the consent screen is being prompted and why it is failing.

First, we need to understand the request sent to Azure AD.

A sign-in request looks something like this…

Azure AD V1 OAuth2 endpoint:

https://{Aad-Instance}/{Tenant-Id}/oauth2/authorize?client_id={App-Id}&response_type=code&redirect_uri={redirect-uri}&resource={App-URI-Id}&scope={Scope}&prompt={Prompt}

Or

Azure AD V2 OAuth2 endpoint:

https://{Aad-Instance}/{Tenant-Id}/oauth2/v2.0/authorize?client_id={App-Id}&response_type=code&redirect_uri={redirect-uri}& scope={Scope}&prompt={Prompt}

If you’re not sure how to get this or see this sign-in request…

  • If you’re using a browser, look at the address bar
  • If you’re not using a browser or still can’t see the address bar in the browser, then a HTTP capture tool like Fiddler will be required.

If you just want to skip finding the root cause and go straight to resolving the issue, go to Step: Perform admin consent.

Here is a quick table for you to use and track the information obtained from the sign-in request.

PropertySign-in request portionValue
Aad-Instance{Aad-Instance}
Tenant-Id{Tenant-Id} portion of the sign-in request 
App-Id{App-Id} portion of the sign-in request 
Scope{Scope} portion of the sign-in request 
App-URI-Id{App-URI-Id} portion of the sign-in request 
Prompt{Prompt} portion of the sign-in request 

For example, your table might look like this…

PropertySign-in request portionValue
Aad-Instance{Aad-Instance}login.microsoftonline.com
Tenant-Id{Tenant-Id} portion of the sign-in requestcommon
App-Id{App-Id} portion of the sign-in request1f92960d-1442-4cd2-8c76-d13c5dcb30bf
Scope{Scope} portion of the sign-in requestOpenid+User.Read+Directory.Read.All
App-URI-IdV1 endpoint: {App-URI-Id} portion of the sign-in request   V2 endpoint: For resources other than Microsoft Graph, this would be the portion before the scope name. For example… https://analysis.windows.net/powerbi/api/App.Read.All App.Read.All is the scope name so the App-Uri-Id is https://analysis.windows.net/powerbi/apihttps://graph.microsoft.com
Prompt{Prompt} portion of the sign-in request 

The information obtained from this sign-in request will be used throughout the troubleshooting steps. I will reference them like this…

{App-Id} from the table above.

Step 2: Do you allow users to consent?

First check if User consent is allowed in your organization…

  1. Sign in to the Azure portal @ https://portal.azure.com
  2. Go to Azure Active Directory
  3. Go to Enterprise applications
  4. Go to User settings
  5. Review “Users can consent to apps accessing company data on their behalf
    1. If “Yes” is selected, then users can consent to permissions which do not require Admin consent. Move on to the next step.
    2. If “No” is selected, Users will always get the “Need admin approval” message. An admin must perform admin consent.
      Go to Step: Perform admin consent
    Note: If an admin believes he has already consented to these permissions, most likely either not all of the required permissions listed in the sign-in request were consented to or the wrong application was used based on the {App-Id} from the table above.
  • Step 3 will cover the App-Id in more detail.
  • Step 4 will cover the permissions in more detail.

Step 3: Verify the application being used.

Verify if the application exists in the tenant.

  1. Sign in to the Azure portal @ https://portal.azure.com
  2. Ensure you switch to the correct tenant based on the {Tenant-Id} from the table above.
  3. Go to Enterprise applications.
  4. Switch Application Type to All Applications and search for the {App-Id} from the table above.
  5. If the application is not found, this would be the cause of you getting the consent messages. Go ahead and skip to Step: Perform admin consent.

If the application is found, go to the next step.

Step 4: User assignment required

While in the Enterprise application, go to Properties and review the User assignment required setting.

If user assignment is required, an admin must consent to this application. Go to Step: Perform admin consent.

If user assignment is not required, go to next step.

There is always confusion that when you consent to an application for all users in the organization that this will allow all users to access the application. This is not true; we will still follow the user assignment rules. Only those users assigned to the application can access it.

Otherwise, if you really do not want to perform admin consent, then the only other option would be to turn off user assignment required, have the user consent when they access the application, and turn user assignment required back on.

Step 5: Verify the permissions.

Let’s next verify that the scopes (or also called permissions) in the sign-in request are listed in the permissions section of the Enterprise App…

  1. If the application is found from the step above (Step 3), go ahead and select that application.
  2. Go to permissions…
  3. Compare what is listed on the permissions page and what is listed as {Scope} from the table above in the sign-in request. The permissions listed on this page are the permissions that have been consented.Note: Pay extra attention to the permission type. Keep in mind that Delegated permissions are for when users sign in and Application permissions are for when the service principal is used to authenticate via the client credential flow. Note: OpenID Connect scopes are generally not listed in the Enterprise application. Don’t worry if the following are not listed…
  • Openid: Sign users in
  • Email: View users’ email address
  • Profile: View users’ basic profile
  • Offline_access: Maintain access to data you have given it access to

Note: if {Scope} from table above is blank or contains less than what is listed on the permissions page, go ahead to the next step.

If there are other scopes in {Scope} from the table above that are not on the permissions page, then go to Step: Perform admin consent. These missing permissions still need to be consented.

Step 6: Verify the resource exists in your tenant.

Easiest way to do this is generate a request that looks like this…

https://{Aad-Instance}/{Tenant-Id}/oauth2/authorize?response_type=code&client_id={App-Id}&resource={App-Uri-id}

You might get one of 4 behaviors…

  • You will be allowed to sign-in (This is the behavior you want to see) Your good to go to next step. In most cases, if you see the “code” parameter in the address bar, this means authentication piece was successful.
  • AADSTS650052: The app needs access to a service (\”https://api.contosocloud.williamfiddes.onmicrosoft.com\”) that your organization \”mycloude5.onmicrosoft.com\” has not subscribed to or enabled. Contact your IT Admin to review the configuration of your service subscriptions.   This means the resource does not exist in your organization. To resolve this, use the following consent URL… https://login.microsoftonline.com/{Tenant-Id}/oauth2/authorize?response_type=code&client_id={App-Uri-id}&prompt=admin_consent
  • AADSTS650057: Invalid resource. The client has requested access to a resource which is not listed in the requested permissions in the client’s application registration. Client app ID: {App-Id}({App-Display-Name}). Resource value from request: ‘{App-Uri-Id}‘. Resource app ID:{Resource-App-Id}. List of valid resources from app registration: 00000002-0000-0000-c000-000000000000 In order for a client application to sign-in and get an access token for a resource, that resource must be assigned to the client applications required API permissions… For example, for a client application to access Azure Key Vault…
    Only the application owner can do this.
  • AADSTS500011: The resource principal named ‘{App-Uri-Id}‘ was not found in the tenant named ‘{Tenant-Id}‘. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.This means either the {App-Uri-Id} specified is not valid at all or is only available as a single tenant application. Otherwise meaning this resource can not be accessed by external organizations or does not exist. You will need to work with the application owner. You will need to verify that {App-Uri-Id} and {Tenant-Id} is correct. If the {App-Uri-Id} is owned by a different {Tenant-Id} then the app registration for {App-Uri-Id} must be set up as a multi-tenant application. Otherwise the {Tenant-Id} must be the same tenant as where the app registration for {App-Uri-Id} is located.

Step 7: Verify if the prompt parameter is being passed.

Sometimes, the way in which your signing into the application is always passing the prompt parameter of consent or admin_consent. Once the application has been consented to, make sure the prompt parameter is not specified. Otherwise, your users might always get the consent error.

You sign-in request might look something like this…

https://login.microsoftonline.com/contoso.onmicrosoft.com/oauth2/authorize?client_id=1f92960d-1442-4cd2-8c76-d13c5dcb30bf&response_type=code&redirect_uri=https://www.contoso.com&scope=openid+profile+User.Read+Directory.Read.All&prompt=consent

So simply remove the prompt parameter and now it should look something like this…

https://login.microsoftonline.com/contoso.onmicrosoft.com/oauth2/authorize?client_id=1f92960d-1442-4cd2-8c76-d13c5dcb30bf&response_type=code&redirect_uri=https://www.contoso.com&scope=openid+profile+User.Read+Directory.Read.All

Step: Perform admin consent.

  1. Have the admin (user with the Global/Company administrator role or a Application Administrator role) access the application normally.
  2. When the consent screen appears, review the request permissions.
    Note: If an admin is not sure what the permissions allow, then the admin must work with the application vendor to understand the permissions and what they are used for. Microsoft support may not know what these permissions do or why the permissions are needed.
  3. If the admin approves the permissions requested, ensure the checkbox is selected to “Consent on behalf of your organization”  

Step: Force Admin Consent

If the admin does not get the consent screen,

Remember step 1? Grab that sign-in address and add &prompt=consent

So, for example, the sign-in request the admin should use will look something like this…

https://login.microsoftonline.com/contoso.onmicrosoft.com/oauth2/authorize?client_id=1f92960d-1442-4cd2-8c76-d13c5dcb30bf&response_type=code&redirect_uri=https://www.contoso.com&scope=openid+profile&tresource=https://graph.microsoft.com&prompt=consent

If the permissions needed are not in the application registration, then the V2 endpoint can be used… (V2 endpoint requires each permission scope to be passed in the scope parameter)

https://login.microsoftonline.com/contoso.onmicrosoft.com/oauth2/v2.0/authorize?client_id=1f92960d-1442-4cd2-8c76-d13c5dcb30bf&response_type=code&redirect_uri=https://www.contoso.com&scope=openid+profile+User.Read+Directory.Read.All&prompt=consent

Permission scopes used by the application must be provided by the application owner.


Other tips

  • Consent for Application permissions will always require admin consent from a Global/Company administrator. Application permissions must be added within the application registration on the applications owning tenant.
  • Application admins can also consent to Delegate permissions which require admin consent.
  • When using the adminconsent URL, the permissions must already be configured with the application registration. Otherwise meaning, the application owner must have their application correctly configured with Azure AD. adminconsent URL looks something like this… https://login.microsoftonline.com/{Tenat-Id}/adminconsent?client_id={App-Id}

For more information about troubleshooting consent issues…

You can use the Azure AD Audit logs to get more details…

  1. Sign in to the Azure Portal @ https://portal.azure.com (Use a account that has permission to read Audit logs. Like a Global Admin or Security Reader).
  2. Go to Azure Active Directory.
  3. Go to Audit logs.
  4. Set your filter to…
    1. Category: ApplicationManagement
    2. Status: Failure
    3. Activity: Consent to application
  5. Find and select the app that’s failing to consent.
  6. Observe the STATUS REASON. This might give you more details. For certain scenarios like “Microsoft.Online.Security.UserConsentBlockedForRiskyAppsException” will require you to perform a Admin consent even though you may allow users to consent and the permission normally does not require an admin to consent.

For more information, see the following…

https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/application-sign-in-unexpected-user-consent-error#requesting-not-authorized-permissions-error

https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/application-sign-in-unexpected-user-consent-prompt

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