Azure AD, apps and consent grant (service accounts)

In the past months. I’ve seen a lot of tickets and question from developer, service owner and some IT pros in regards of OAuth, consent and permissions (delegated/application).

Especially 3rd party applications and their documentation is missing very often some details. Therefore, I’m writing this post.

Background

With the shift to cloud services, a lot of vendors, developers and IT pros starting to look into OAuth2.0, which is used in Azure AD when it comes to authorization and authentication. Microsoft simplified the process to register an application in your tenant as described here:

Quickstart: Register an application with the Microsoft identity platform

When it comes to permissions it starts to get complicated for some:

There are two types of permission and listed here. Here my brief explanation:

  • Delegated permissions: always involves user information. A user delegated an application permissions and act on-behalf of the user (e.g.: https://graph.microsoft.com/Calendar.Read). Unless an administrator granted admin consent, a user needs to consent to the app, before it can act on-behalf of the user.
  • Application permissions: these permissions do not involve any user information and therefore an administrator needs to grant admin consent. This means without additional configuration, apps with these permissions can act on-behalf of any user.

What delegated and application permissions have in common is the fact that someone needs to grant consent. No matter what, at least one!

And with this the most are struggling. I have seen implementations of OAuth2.0 for M365 using all kinds of OAuth2.0 flows as described here.

What’s the problem?

Very often the fact that consent has to be granted is missed. This leads as expected to a failure like this:

AADSTS65001: The user or administrator has not consented to use the application with ID ” named ”. Send an interactive authorization request for this user and resource.

But even when such an error is not thrown, they might see this this:

AADSTS7000218: The request body must contain the following parameter: ‘client_assertion’ or ‘client_secret’.

In order to prevent this you need to make sure the following has been done:

  • consent was granted
  • app has been configured according to OAuth2.0 flow

Grant consent

There are several ways in order to grant consent to an application for a service account:

  • admin consent
  • user specific consent

Grant admin consent

Even this can be done on different ways. The most commons might be using the UI. Even in the UI you can do this on Enterprise applications level:

Grant admin consent for any permissions

Or you do it more specific for the configured permissions only on App registrations level:

Granting admin consent only for configured permissions

In case you want to use an constructed URL, you just need to know the AppId and either the GUID of your tenant or registered domain:

https://login.microsoftonline.com/{tenant-id|domain}/adminconsent?client_id={client-id}

This is also documented on Docs and can be found here:

Grant tenant-wide admin consent

If you want to be more specific and grant admin consent only for a subset of configured permissions, you could still use either Microsoft Graph oauth2PermissionGrants or you need to build another URL as follows:

https://login.microsoftonline.com/{tenant-id|domain}/oauth2/v2.0/authorize?client_id={client-id}&response_type=code&response_mode=query&scope={URL decoded list of space separated scopes}&state=12345&prompt=consent
# Example
# load additional module
Add-Type -AssemblyName System.Web
$tenant = '2e9dea3a-cce5-42cc-b083-0b70458298b8'
$clientID = 'c07b78d3-2eb0-48c0-814f-6bac5a8031f1'
$scopes = 'https://graph.microsoft.com/email https://graph.microsoft.com/offline_access https://graph.microsoft.com/openid'
# URL encode scopes
$scopes = [System.Web.HttpUtility]::UrlEncode($scopes)
$URL = "https://login.microsoftonline.com/$($tenant)/oauth2/v2.0/authorize?client_id=$($clientID)&response_type=code&response_mode=query&scope=$($scopes)&state=12345&prompt=consent"
# start default browser with constructed URL. Alternative copy and paste the URL to your preferred browser
[System.Diagnostics.Process]::Start($URL)

In this example I requested an authorization code for the scopes Microsoft Graph email, offline_access and openid. In the request I asked for consent prompt. Here the breakdown:

# authority
https://login.microsoftonline.com/
# tenantid (could be also domain)
2e9dea3a-cce5-42cc-b083-0b70458298b8/
# endpoint for authorization
oauth2/v2.0/authorize?
# clientID
client_id=c07b78d3-2eb0-48c0-814f-bac5a8031f1
# response type (in this case code)
&response_type=code
# response mode (in this case query)
&response_mode=query
# scope URL encoded
&scope=https%3a%2f%2fgraph.microsoft.com%2femail+https%3a%2f%2fgraph.microsoft.com%2foffline_access+https%3a%2f%2fgraph.microsoft.com%2fopenid
# random state
&state=12345
# the important prompt: consent
&prompt=consent

Here some screenshots when opening the URL in a browser. I signed-in with Global Admin account and granted consent:

User consent

User consent for permissions, which do not need admin consent, can be granted as soon as the user sign-ins and the prompt is shown. This can happen in different ways. Using the auth code flow as mentioned above is one way. This means you can construct the URL as mentioned above and sign-in with the credential of the service account and grant consent, without checking the box for the organization.

I have seen by a few vendors another flow:

OAuth2.0 device code flow

This flow can be very convenient, but has also some pitfalls. Here an example how to construct the request for this flow in PowerShell:

Add-Type -AssemblyName System.Web
$tenant = '2e9dea3a-cce5-42cc-b083-0b70458298b8'
$clientID = 'c07b78d3-2eb0-48c0-814f-6bac5a8031f1'
$scopes = 'https://graph.microsoft.com/email https://graph.microsoft.com/offline_access https://graph.microsoft.com/openid'
# create body
$deviceCodeBody = @{
    client_id = $clientId
    scope = $scopes
}
# build parameters
$deviceCode = @{
    ContentType = 'application/x-www-form-urlencoded'
    Method = 'POST'
    Body = $deviceCodeBody
    Uri = "https://login.microsoftonline.com/$($tenant)/oauth2/v2.0/devicecode"
}
# invoke the request
$code = Invoke-RestMethod @deviceCode -Verbose

When you send this request, you will receive several data, but the important one is the user_code, which needs to be used for authentication:

But here the issues may start:

  • device code flow is not using a redirect URI and you will retrieve the error AADSTS7000218 as Allow public client flows is not enabled
  • the code will expire in 900 seconds by default (I have no knowledge about changing timeout)

If you have configure everything correct and overcome all issues, you will be able to retrieve an access token:

Here the PowerShell code I used:

# create bode for acquiring access token
$accessTokenBody = @{
    grant_type = 'urn:ietf:params:oauth:grant-type:device_code'
    client_id = $clientID
    device_code = $code.device_code
}
# build parameters
$accessToken = @{
    ContentType = 'application/x-www-form-urlencoded'
    Method = 'POST'
    Body = $accessTokenBody
    Uri = "https://login.microsoftonline.com/$($tenant)/oauth2/v2.0/token"
}
# acquire access token
$token = Invoke-RestMethod @accessToken -Verbose

Conclusion

I hope this post helps you to tackle some issues. I would love to see that vendors (even large ones) would spent more time and effort for proper documentation and background info how their product is working in details (at least for some portions).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s