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:
Name
Azure Gov Endpoint
Portal
https://portal.azure.us
Microsoft Graph API
https://graph.microsoft.us/
Active Directory Endpoint and Authority
https://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.
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:
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.
Configure the Application permission with at least a delegated Microsoft Graph ‘User.Read’ permission and grant Admin Consent on the permission.
on-prem domain already federated with Azure AD via ADFS Server.
Testing needs to be done with a domain user account synced to Azure AD.
Performing Token Acquisition
Getting a SAML Assertion from ADFS
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.
Send a Home Realm Discovery request to Azure AD to get the ‘federation_active_auth_url’ value
GET https://login.microsoftonline.com/common/userrealm/user@contoso.com?api-version=1.0
or
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:
Your PostMan set up should look like the following:
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
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_type
urn:ietf:params:oauth:grant-type:saml1_1-bearer
client_id
your Application ID
assertion
base64 encoded SAML Assertion
scope
https://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.
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.
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:
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 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}" );
}
}
}
}
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.
In this blog, we will use Azure SDK for .NET to perform the following tasks:
Create an Azure Active Directory (Azure AD) user, query for the created user, and delete the user.
Create an Azure AD group, query for the created group, and delete the group.
Add a user to the group’s members.
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: 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:
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.
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.
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.
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
From Visual Studio 2019, create a new C# Console App (.Net Framework)
Install this Nuget Package: Install-Package Microsoft.Azure.Management.Fluent
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:
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/
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…
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…
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…
First, disable the Public Client configuration within the application registration…
Then perform the step above under “If it’s a Web Application” to assign users, groups, or service principals to the application.
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.)
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…
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…
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.
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.
Property
Sign-in request portion
Value
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…
Property
Sign-in request portion
Value
Aad-Instance
{Aad-Instance}
login.microsoftonline.com
Tenant-Id
{Tenant-Id} portion of the sign-in request
common
App-Id
{App-Id} portion of the sign-in request
1f92960d-1442-4cd2-8c76-d13c5dcb30bf
Scope
{Scope} portion of the sign-in request
Openid+User.Read+Directory.Read.All
App-URI-Id
V1 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/api
https://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…
Review “Users can consent to apps accessing company data on their behalf“
If “Yes” is selected, then users can consent to permissions which do not require Admin consent. Move on to the next step.
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.
Ensure you switch to the correct tenant based on the {Tenant-Id} from the table above.
Go to Enterprise applications.
Switch Application Type to All Applications and search for the {App-Id} from the table above.
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…
If the application is found from the step above (Step 3), go ahead and select that application.
Go to permissions…
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…
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…
Have the admin (user with the Global/Company administrator role or a Application Administrator role) access the application normally.
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.
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…
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)
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…
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).
Go to Azure Active Directory.
Go to Audit logs.
Set your filter to…
Category: ApplicationManagement
Status: Failure
Activity: Consent to application
Find and select the app that’s failing to consent.
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.
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;
}
}
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;
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:
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;
}
}
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.
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.
Build a raw HTTP POST request for the v2 token endpoint (code below)
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.
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:
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