At Readify yesterday, I saw two different co-workers encounter the same issue within a few hours of each other. Time for a blog post!
Scenario
Problem 1:
I’m trying to use Power BI AAD App Registration to access Power BI REST API using ClientId and Key. The idea is to automate Power BI publishing activities through VSTS without a need of using my OrgId credentials. Pretty much I follow this guide but in PowerShell. The registration through https://dev.powerbi.com/apps sets the app registration up and configures API scopes. I’m able to get a JWT but when I call any of Power BI endpoints I’m getting 401 Unauthorised response. I’ve checked JWT and it’s valid but much different from the one I’m getting using my OrgId.
Problem 2:
Anyone familiar with App Registrations in Azure? Specifically I’m trying to figure out how to call the Graph API (using AppID and secret key generated on the portal), to query for any user’s group memberships. We already have an app registration that works. I’m trying to replicate what it’s doing. The end result is that while I can obtain a token for my new registration, trying to access group membership gives an unauthorized error. I have tried checking every permission under delegated permissions but it doesn’t seem to do the trick.
They were both trying to:
- Create an app registration in AAD
- Grab the client ID and secret
- Immediately use these secrets to make API calls as the application (not on behalf of a user)
Base Principles
App Registrations
AAD has the concept of App Registrations. These are essentially a manifest file that describes what an application is, what its endpoints are, and what permissions it needs to operate.
It’s easiest to think of app registrations from the perspective of a multi-tenant SaaS app. There’s an organisation who publishes an application, then multiple organisations who use that app. App registrations are the publisher side of this story. They’re like the marketplace listing for the app.
App registrations are made/hosted against a tenant which represents the publisher of the app. For example, “MegaAwesome App” by Readify would have its app registration in the readify.onmicrosoft.com
directory. This is a single, global registration for this app across all of AAD, regardless of how many organisations use the app.
App registrations are primarily managed via https://portal.azure.com or https://aad.portal.azure.com. There’s also a somewhat simplified interface at https://apps.dev.microsoft.com, and some workload-specific ones like https://dev.powerbi.com/apps. They’re all just different UIs on top of the same registration store.
Enterprise Apps
These are like ‘instances’ of the application, or ‘subscriptions’. In the multi-tenant SaaS app scenario, each consumer gets an Enterprise App defined.
The enterprise app entry describes whether the specific app is even approved for use or not, what permissions have actually been granted, and which users or groups have been assigned access.
- Publisher:
readify.onmicrosoft.com
- App Registration for ‘MegaAwesome App’
- Defined by development team
- Describes name, logo, endpoints, required permissions
- App Registration for ‘MegaAwesome App’
- Subscriber:
contoso.onmicrosoft.com
- Enterprise App for ‘MegaAwesome App by Readify’
- Acknowledges that Contoso uses the app
- Controlled by Contoso’s IT admins
- Grants permission for the app to access specific Contoso resources
- Grants permission for specific Contoso users/groups to access the app
- Defines access requirements for how Contoso users access the app (i.e. Conditional Access rules around MFA, device registration, and MDM compliance)
- Might override the app name or logo, to rebrand how it displays in navigation experiences like https://myapps.microsoft.com.
- Enterprise App for ‘MegaAwesome App by Readify’
If we take the multi-tenant SaaS scenario away, and just focus on an internal app in our own org, all we do is put both entries in the same tenant:
- Org:
readify.onmicrosoft.com
- App Registration for ‘MegaAwesome App’
- Defined by development team
- Enterprise App for ‘MegaAwesome App by Readify’
- Controlled by Readify’s IT admins (not dev team)
- App Registration for ‘MegaAwesome App’
The App Registration and the Enterprise App then represent the internal split between the dev team and the IT/security team who own the directory.
Consent
This is (mostly) how an enterprise app instance gets created.
The first time an app is used by a subscriber tenant, the enterprise app entry is created. Some form of consent is required before the app actually gets any permissions to that subscriber tenant though.
Depending on the permissions requested in the app registration or login flow, consent might come from the end user, or might require a tenant admin.
In an interactive / web-based login flow, the user will see the consent prompt after the sign-in screen, but before they’re redirected back to the app.
Our Problem
In the scenario that both of my co-workers were hitting, they had:
- Created an app registration
- Grabbed the app’s client ID and secret
- Tried to make an API call using those values
- Failed with a
401 Unauthorised
response
Because they weren’t redirecting a user off to the login endpoint, there was no user-interactive login flow, and thus no opportunity for the enterprise app entry to be created or for consent to be provided.
Basic Solution
You can jump straight to the consent prompt via this super-easy to remember, magical URL:
https://login.microsoftonline.com/ {TenantDomain} /oauth2/authorize ?client_id={AadClientId} &response_type=code &redirect_uri=https://readify.net &nonce=doesntmatter &resource={ResourceUri} &prompt=admin_consent
You fill in the values for your tenant, app client ID, and requested resource ID, then just visit this URL in a browser once. The redirect URI and nonce don’t matter as it’s only yourself being redirect there after the consent has been granted.
For example:
https://login.microsoftonline.com/ readify.onmicrosoft.com /oauth2/authorize ?client_id=ab4682b39bc... &response_type=code &redirect_uri=https://readify.net &nonce=doesntmatter &resource=https://graph.microsoft.com &prompt=admin_consent
Better Solution
Requiring a user to visit a one-time magical URL to setup an app is prone to failure. Somebody will inevitably want to deploy the app/script to another environment, or change a permission, and then wonder why everything is broken even though the app registrations are exactly the same.
In scripts that rely on app-based authentication, I like to include a self-test for each resource. This self-test does a basic read-only API call to assert that the required permissions are there, then provides a useful error if they aren’t, including a pre-built consent URL. Anybody running the script or reviewing logs can browse straight to the link without needing to understand the full details of what we’ve just been through earlier in this post.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Get-AppConsentUri($resource) | |
{ | |
"https://login.microsoftonline.com/$TenantDomain/oauth2/authorize?client_id=$AadClientId&response_type=code&redirect_uri=https://readify.net&nonce=foo&resource=$resource&prompt=admin_consent" | |
} | |
function Test-GraphAccess() | |
{ | |
try | |
{ | |
Invoke-RestMethod ` | |
–Headers $graphHeaders ` | |
–Method Get ` | |
–Uri 'https://graph.microsoft.com/v1.0/users/?$top=1' | | |
Out-Null | |
Write-Verbose "API access seems to work; could retrieve basic user listing" | |
} | |
catch [Exception] { | |
throw "Failed to read basic directory data. Ensure: | |
1) app registration includes Microsoft Graph API, Read directory data permission | |
2) admin consent has been granted via $(Get-AppConsentUri https://graph.microsoft.com)" | |
} | |
} | |
Test-GraphAccess |
Preferred Solutions
Where possible, act on behalf of a user rather than using the generic app secret to make API calls. This makes for an easier consent flow in most cases, and gives a better audit log of who’s done what rather than just which app did something.
Further, try to avoid actually storing the app client ID and secret anywhere. They become another magical set of credentials that aren’t attributed to any particular user, and that don’t get rotated with any real frequency. To bootstrap them into your app, rather than storing them in config, look at solutions like Managed Service Identity. This lets AAD manage the service principal and inject it into your app’s configuration context at runtime.
Other Resources
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-application-objects
You must be logged in to post a comment.