Entra ID app registrations vs enterprise apps: where consent lives
For developers wiring up Microsoft identity, the confusing part is not OAuth itself; it is knowing which Entra ID object to inspect when sign-in or API consent fails. This guide explains the split between app registrations and enterprise applications, shows where delegated and application consent actually lands, and gives you concrete commands and diagnostics to fix the common misconfigurations.
TL;DR — In Entra ID, the app registration is the application definition/template, while the enterprise application is the tenant-local service principal instance that users, groups, assignments, SSO settings, and most effective consent decisions attach to. If you are debugging "needs admin approval", missing roles, or "user assignment required", look at the enterprise application (service principal) in the tenant where the sign-in happens; if you are changing redirect URIs, exposed scopes, app roles, or supported account types, change the app registration. Reading time: ~7 min
What it is and where it sits
The practical model is this:
- App registration = the global definition of an application object in its home tenant.
- Enterprise application = the service principal created in a tenant to represent that app there.
- Consent lives on the service principal side, because consent is tenant-specific and often user- or admin-specific to the tenant where access is granted.
If you build line-of-business apps, you usually create the app registration in your tenant first. When that app is used in the same tenant, Entra also has a corresponding service principal. When a multi-tenant app is used by another tenant, that other tenant gets its own enterprise application/service principal for your app.
What this replaces conceptually: if you came from older Azure AD docs, the underlying objects are still application and servicePrincipal in Microsoft Graph. The portal labels are just friendlier names. When docs, scripts, and errors disagree, Graph object names usually win.
Architecture context in a normal OAuth/OIDC flow:
Browser / SPA / mobile app
|
| 1. authorize request
v
Entra ID authorization endpoint
|
| 2. checks app definition (redirect URIs, scopes exposed, audience)
| 3. checks tenant-local service principal (assignment, consent, CA, SSO settings)
v
User signs in / admin approves
|
| 4. token issued with roles/scopes based on consent and assignments
v
Your app / API
|
| 5. API validates token against app ID URI / audience / roles / scopes
v
Protected data
What talks to what:
- Your client app talks to the
/authorizeand/tokenendpoints. - Entra evaluates the application object for protocol config: redirect URIs, implicit/hybrid flags if any legacy app still needs them, exposed scopes, app roles, supported account types.
- Entra evaluates the service principal in the current tenant for local policy: user assignment required, who has consented, app role assignments, SAML/SSO settings, provisioning, conditional access interaction.
The key mental model: definition on the app registration, instantiation and tenant-local access on the enterprise app.
How it actually works
Walk one realistic example: a multi-tenant web app calls a custom API, and a customer tenant admin grants consent.
Step 1: You define the app and API in the home tenant
You create an app registration for the web app and another for the API.
On the API app registration, you configure:
- an Application ID URI, such as
api://11111111-2222-3333-4444-555555555555 - delegated scopes, such as
access_as_user - app roles, if daemon/service access is needed
On the web app registration, you configure:
- redirect URIs
- required API permissions pointing to the API scope
At this point, you changed the application objects only.
Step 2: A customer tenant first uses the app
A user from contoso.com hits your sign-in URL with common or organizations authority. Entra sees this is a multi-tenant app and, on first successful interaction/consent path, creates a service principal for your app in Contoso's tenant. The same happens for the API if needed.
This is why your home tenant app registration is not enough to debug customer-tenant failures: the customer tenant now has its own enterprise application objects.
Step 3: Consent is recorded tenant-locally
Suppose your app requests delegated permission to your API scope and also User.Read from Microsoft Graph.
When the Contoso admin approves, the durable effect is not "stored on the app registration" in your home tenant. It is represented by tenant-local objects tied to the service principal relationships in Contoso. In Graph terms, the important records are typically OAuth2 permission grants and app role assignments associated with service principals.
That is why these symptoms are fixed in the customer tenant's enterprise app:
- users get
AADSTS65001or "needs admin approval" - app role not appearing in token
- assignment required blocks sign-in
- SAML/SSO settings differ by tenant
Typical error shape during missing consent:
AADSTS65001: The user or administrator has not consented to use the application with ID 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' named 'Acme Web'. Send an interactive authorization request for this user and resource.
Trace ID: 2d6f...
Correlation ID: 9a2b...
Timestamp: 2026-08-05 10:14:31Z
Step 4: Token issuance uses both objects
When Contoso user signs in again:
- Entra checks the app registration for redirect URI and audience validity.
- Entra checks the Contoso enterprise app for whether the user is allowed, whether admin consent exists, and whether app roles are assigned.
- The resulting token includes
scpfor delegated scopes orrolesfor app roles, depending on flow.
If redirect URI is wrong, changing enterprise app settings will not help. If consent/assignment is wrong, changing app registration redirect URIs will not help.
Step 5: Diagnose with Graph/CLI, not portal guessing
Find the app registration object and the service principal separately.
az ad app list --filter "appId eq 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'" --query "[].{displayName:displayName,appId:appId,id:id}" -o table
az ad sp list --filter "appId eq 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'" --query "[].{displayName:displayName,appId:appId,id:id,servicePrincipalType:servicePrincipalType}" -o table
Typical output shape:
DisplayName AppId Id
------------- ------------------------------------ ------------------------------------
Acme Web aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee 0f0f0f0f-1111-2222-3333-444444444444
DisplayName AppId Id ServicePrincipalType
------------- ------------------------------------ ------------------------------------ --------------------
Acme Web aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee 99999999-8888-7777-6666-555555555555 Application
Different IDs are expected. Same appId, different object IDs. The app registration id is the application object ID; the enterprise app id is the service principal object ID.
When to use it (and when not to)
| Scenario | Recommendation |
|---|---|
| You need to add a redirect URI, logout URL, exposed scope, or app role | Change the app registration |
| Users in a tenant cannot sign in because of assignment, consent, or SSO policy | Inspect the enterprise application in that tenant |
| You are building a multi-tenant SaaS and customers self-consent/admin-consent | Expect a service principal per customer tenant; operationalize around enterprise apps |
| You need to know whether admin consent has been granted for an API in a tenant | Query service principal grants/assignments in that tenant |
| You only have a single-tenant internal app and no per-tenant customer onboarding | You still have both objects, but you will mostly touch app registration plus the local enterprise app for assignments |
You are trying to fix a bad redirect_uri error by editing enterprise app settings | Don’t; fix the app registration |
| You are trying to understand why roles are absent from a token for one tenant only | Check enterprise app role assignments in that tenant |
You probably do not need to think deeply about enterprise applications if all of these are true:
- single-tenant app
- no user/group assignment requirements
- no customer tenants
- no SAML/provisioning/SSO customization
- no support burden around admin consent
But the moment you support external tenants, enterprise apps stop being optional knowledge.
Trade-offs
-
Benefit: clean separation of definition vs tenant-local instance
Cost: two objects to inspect, two object IDs, and many support tickets caused by editing the wrong one. -
Benefit: each customer tenant can have its own consent, assignments, and SSO settings
Cost: operational sprawl. A multi-tenant SaaS may have thousands of service principals across customer tenants that you do not directly control. -
Benefit: admin consent is tenant-scoped, which is correct for governance
Cost: onboarding friction. Enterprise customers often require admin approval before any user can proceed. -
Benefit: app roles and assignment can limit access tightly
Cost: missing assignments look like broken auth. Tokens simply omitroles, and your app may fail with generic 403s unless you log claims clearly. -
Benefit: protocol config stays centralized on the app registration
Cost: lock-in to Entra object model if you automate heavily with Graph-specific concepts like service principals, OAuth2 permission grants, and app role assignments.
Latency and money are usually not the issue here; human debugging time is. The expensive failure mode is support engineers chasing the app registration when the enterprise app in the customer tenant is the real problem.
In practice
Example 1: Inspect app registration vs enterprise app with Azure CLI and Graph
APPID="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
echo "== Application object =="
az ad app list \
--filter "appId eq '$APPID'" \
--query "[].{appObjectId:id,appId:appId,displayName:displayName,signInAudience:signInAudience}" \
-o json
echo "== Service principal object =="
az ad sp list \
--filter "appId eq '$APPID'" \
--query "[].{spObjectId:id,appId:appId,displayName:displayName,appOwnerOrganizationId:appOwnerOrganizationId}" \
-o json
This shows the two different objects for the same appId. Gotcha: in cross-tenant support, run this against the customer tenant context, not just your home tenant, or you will inspect the wrong service principal.
If you need consent-related data, use Graph against the tenant where sign-in fails:
SP_ID="99999999-8888-7777-6666-555555555555"
az rest --method GET \
--url "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_ID/appRoleAssignedTo"
az rest --method GET \
--url "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?$filter=clientId eq '$SP_ID'"
appRoleAssignedTo helps with application permissions and role assignments; oauth2PermissionGrants helps with delegated consent. Gotcha: Graph permissions for your admin account/tooling must allow reading these objects, or you will get Authorization_RequestDenied instead of useful data.
Example 2: Diagnose redirect mismatch vs consent failure from raw HTTP
TENANT="common"
CLIENT_ID="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
REDIRECT_URI="https%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback"
SCOPE="openid%20profile%20offline_access%20api%3A%2F%2F11111111-2222-3333-4444-555555555555%2Faccess_as_user"
curl -i "https://login.microsoftonline.com/$TENANT/oauth2/v2.0/authorize?client_id=$CLIENT_ID&response_type=code&redirect_uri=$REDIRECT_URI&response_mode=query&scope=$SCOPE&state=debug123"
If the redirect URI is not registered on the app registration, you will typically see an HTML error page and a message like this in the body:
AADSTS50011: The redirect URI 'https://localhost:3000/auth/callback' specified in the request does not match the redirect URIs configured for the application 'Acme Web'.
That is an app registration fix: go to your Entra admin UI for the app registration and add the exact URI, including scheme, host, port, and path.
If redirect is correct but consent is missing, the browser flow reaches sign-in and then fails with a consent/admin approval error instead. That is usually an enterprise app/tenant-consent fix, not a redirect fix.
Example 3: Token claims check in an API
{
"expectedAudiences": [
"api://11111111-2222-3333-4444-555555555555",
"11111111-2222-3333-4444-555555555555"
],
"requireScope": "access_as_user",
"requireRole": "Api.Read.All"
}
This is the validation policy your API effectively enforces. Gotcha: delegated flows produce scp; app-only flows produce roles. If you require roles for a user token, you will reject valid delegated access and misdiagnose it as consent failure.
⚠️ Changing app roles or removing exposed scopes on a production app registration can break existing clients immediately. Tokens may stop containing expected claims, and consented permissions may no longer map cleanly. Stage these changes in a non-production app registration first.
Further reading
- Microsoft Graph
applicationresource type - Microsoft Graph
servicePrincipalresource type - Microsoft Graph
oAuth2PermissionGrantresource type - Microsoft identity platform: Permissions and consent
- OpenID Connect Core 1.0
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI