Skip to content

8.4.9 — Microsoft Entra ID in Practice

An application works in development. In production, login fails:

AADSTS50011: The redirect URI specified in the request does not match
the redirect URIs configured for the application.

Someone adds the production URL. Now login succeeds and the API returns 401. The token looks valid. Nobody can say why it is rejected.

Both problems are ordinary once you know the model, and the model has one idea that everything else hangs off: an application registration and a service principal are different objects, and permissions live on the second one.

1. Tenants, registrations and service principals

A tenant is one instance of the directory — one organisation's users, groups and applications. It has a GUID (tenantId) and one or more domains. Every request to Entra names a tenant, either explicitly or as common / organizations.

An app registration is the definition of an application. Its name, redirect URIs, credentials, exposed API and requested permissions. It lives in exactly one tenant — the home tenant — and it is the blueprint.

A service principal is the instance of that application inside a tenant. It is what actually gets permissions, role assignments and consent. In the portal these appear under Enterprise applications.

Read the relationship this way: a registration is the class; a service principal is the object. In your own tenant both exist and it is easy to conflate them. For a multi-tenant application there is one registration in your tenant and one service principal in every customer's tenant, created when an administrator consents.

That single fact answers a family of confusing errors. AADSTS700016: Application with identifier X was not found in the directory means the service principal does not exist in that tenant yet — nobody has consented — not that the registration is wrong.

2. Registering an application

Redirect URIs must match exactly, string for string, including scheme, port and trailing slash — the same rule as Chapter 8.4.3. They are grouped by platform (web, single-page application, mobile), and the platform type changes the behaviour: a URI registered under "Web" expects a client secret and will not issue tokens to browser-based code with PKCE, while one under "Single-page application" enables the CORS-based token endpoint. Registering a SPA's URI under Web produces AADSTS9002326, which reads as a CORS error and is actually a platform-type error.

Credentials: a client secret or a certificate.

Client secrets expire — maximum 24 months, and the expiry is an outage. It is one of the most common preventable production incidents in Azure estates: the secret expires at a date nobody recorded, and every service using it fails at once with AADSTS7000215: Invalid client secret provided.

Prefer a certificate, and prefer a managed identity over both wherever the code runs in Azure (section 4). If you must use a secret: store it in Key Vault (Chapter 8.6.1), record the expiry, alert 60 days ahead, and support two active credentials so rotation overlaps.

3. Permissions: delegated versus application

This is the distinction that decides whether your service can read every user's mailbox or only the signed-in user's.

Delegated permissions — the application acts as the signed-in user. The effective access is the intersection of what the application was granted and what the user is allowed. Mail.Read delegated means "read the signed-in user's mail".

Application permissions — the application acts as itself, with no user. Mail.Read as an application permission means read every mailbox in the organisation. These always require administrator consent, and they are where over-permissioning becomes dangerous.

The rule: use delegated permissions unless there is genuinely no user. Background jobs, daemons and service-to-service calls need application permissions; anything triggered by a person should act as that person, so the directory's own access control still applies.

Consent is the user or administrator agreeing. Personal-scope permissions can be consented by the user; anything organisation-wide requires an administrator. AADSTS65001: The user or administrator has not consented means exactly that, and the fix is an administrator visiting the consent URL — not a code change.

The .default scope is Entra-specific and worth knowing: requesting https://graph.microsoft.com/.default means "give me a token with everything already consented for this application", rather than naming individual scopes. It is required for the client credentials flow and is the usual answer when a client-credentials token request rejects your scope list.

4. Managed identities

The best credential is one that does not exist. A managed identity gives an Azure resource — a virtual machine, an app service, a container, a function — an identity in Entra with no secret anywhere in your code or configuration.

The platform exposes a local endpoint that only code on that resource can reach, and the SDK fetches a token from it. There is nothing to leak, nothing to rotate, and nothing to expire.

System-assigned — tied to one resource, created and deleted with it. Simple, and the identity disappears if the resource is recreated, which loses its role assignments.

User-assigned — a standalone identity attached to several resources, surviving their lifecycle. This is usually the better choice for anything with a deployment pipeline that recreates resources, because the role assignments persist.

ts
import { DefaultAzureCredential } from '@azure/identity';

const credential = new DefaultAzureCredential();      // (1)
const token = await credential.getToken('https://graph.microsoft.com/.default');  // (2)

(1) DefaultAzureCredential tries several sources in order — environment variables, a managed identity, then your local developer sign-in. That is what lets the same code run locally and in Azure with no branching. (2) In Azure this reaches the platform's local token endpoint; locally it uses your signed-in developer identity.

If your code runs in Azure and still holds a client secret, that is a finding, not a style preference.

5. Tokens, and the 401 that has no explanation

Return to the opening. The token was issued, and the API rejects it. Four causes account for nearly all of these.

Wrong audience. The token's aud is whichever resource you requested a token for. If you asked for a Graph token and sent it to your own API, your API is correct to reject it. Request a token for your API's own application id URI, such as api://<app-id>/access_as_user.

Wrong endpoint version. Entra has a v1.0 and a v2.0 endpoint with different token formats and aud conventions. A client using v2.0 against an API expecting v1.0 tokens fails; the registration's accessTokenAcceptedVersion manifest setting controls what is issued. Mixing them is the classic "valid token, rejected by API".

Missing scope or role. The API validates that the token carries the expected scp (delegated) or roles (application) value, and the client requested neither.

Validation misconfigured. The API's issuer or audience configuration does not match the tenant or the application id URI. Decode the token and compare aud, iss, scp and roles against what the API expects — that comparison resolves it in a minute and guessing does not.

The group overage claim is the trap that breaks authorisation silently. If a user belongs to more than about 200 groups, Entra omits the groups claim entirely and instead includes _claim_names and _claim_sources pointing at a Graph endpoint. Code that reads groups finds nothing and concludes the user has no roles. Handle the overage by calling Graph for group membership, or — better — use app roles instead of raw groups, which do not overflow and express your application's vocabulary rather than the directory's.

6. Conditional access

Conditional access evaluates every sign-in against policies: if these signals, then require these controls.

Signals — user or group, application, device compliance, location or IP, sign-in risk, client type. Controls — block, require MFA, require a compliant device, require an approved app, limit the session.

Common policies: MFA for administrators, block legacy authentication protocols that cannot do MFA, require a compliant device for sensitive applications, block sign-in from countries where you have no staff.

Two things engineers must know.

It manifests as errors your application must handle. AADSTS50076 means MFA is required; AADSTS53003 means access was blocked by policy. For a web application, respond by redirecting into an interactive login rather than showing a generic failure — MSAL raises this as an interaction-required error and the correct handling is to prompt.

Continuous access evaluation pushes critical events — a disabled account, a password change, a risky sign-in — to resource providers so a token can be rejected before it expires. This is Entra's answer to Chapter 8.4.2's revocation problem, and it is the closest thing to real-time revocation for stateless tokens.

And the break-glass account from Chapter 8.4.6 applies here directly: a conditional access policy that requires a compliant device, applied to all users including administrators, has locked organisations out of their own tenant. Exclude at least two emergency accounts from every policy, monitor them, and use long stored credentials.

7. Groups, guests and customers

Security groups carry permissions; Microsoft 365 groups come with a mailbox and collaboration features. Use security groups for authorisation.

Dynamic groups compute membership from a rule — user.department -eq "Finance" — so joiners and leavers are handled by attributes rather than by a person remembering.

B2B (external collaboration) invites a guest user from another organisation into your tenant. They authenticate at their own tenant and appear as a guest in yours. This is the right mechanism for partners and contractors, and guests must be reviewed — a stale guest is a stale account with someone else's lifecycle.

B2C / Microsoft Entra External ID is a separate product for customer identity: your consumers sign up with an email, Google, Facebook or Apple, in a directory that is not your employee directory. Do not put customers in your corporate tenant — different lifecycle, different scale, different security posture.

8. Single-page applications, and Easy Auth

MSAL is the client library, and the shape to use is authorization code with PKCE (Chapter 8.4.3) — implicit is gone.

Three practical points:

Acquire tokens silently and fall back to interactive. acquireTokenSilent first; on an interaction-required error, redirect or open a popup. That single pattern removes most login-loop bugs.

Attach tokens in an interceptor, per API. In Angular, an HTTP interceptor maps each API URL to the scopes it needs and attaches the right token. Do not attach one token to every outbound request — sending your API token to a third-party host leaks a credential.

Content Security Policy must allow the login domains. Silent renewal historically used a hidden iframe, so frame-src https://login.microsoftonline.com was required, alongside connect-src for the token endpoint. Silent renewal failing while normal login works is almost always CSP or third-party cookie behaviour, and Chapter 5.6.3's cookie changes are why the ecosystem moved to refresh tokens in the browser instead.

Easy Auth (App Service authentication) is the zero-code option: enable it on the platform and every request is authenticated before your code runs, with the identity handed over in an X-MS-CLIENT-PRINCIPAL header. It is genuinely useful for internal tools. Its limits are worth knowing: authorisation beyond "is signed in" is still yours, the header must be trusted only because the platform sets it (Chapter 9.9.2's proxy-trust reasoning), and local development needs a different path.

9. Troubleshooting

The sign-in logs are the answer to most questions. Every attempt is recorded with the application, the conditional access policies evaluated and their results, and a failure reason. Ask for the correlation id from the error page and look it up.

CodeMeaning
AADSTS50011Redirect URI mismatch — exact string, including trailing slash
AADSTS65001Consent not granted — an administrator must consent
AADSTS700016Application not found in this tenant — no service principal, so nobody consented
AADSTS7000215Invalid client secret — usually expired
AADSTS50076MFA required by conditional access — respond interactively
AADSTS53003Blocked by a conditional access policy
AADSTS900023Tenant not found — a typo in the tenant id or domain
AADSTS9002326Cross-origin token redemption — the URI is registered as Web, not SPA

And the general method: decode the token. Compare aud, iss, scp, roles and tid against exactly what the API expects. Most Entra problems are a mismatch between what was requested and what is validated, and the token says which.

What the interviewer will push on

"What is the difference between an app registration and an enterprise application?" The registration is the definition and lives in the home tenant; the service principal is the instance in each tenant and is what holds permissions and consent. Then apply it: AADSTS700016 means the service principal does not exist there, so nobody has consented — not that the registration is wrong.

"Delegated or application permissions?" Delegated means acting as the signed-in user, with access limited to the intersection of the grant and the user's own rights. Application means acting as itself — Mail.Read then reads every mailbox. Use delegated unless there is genuinely no user, so the directory's access control still applies.

"How would you avoid storing a client secret?" A managed identity, preferably user-assigned so role assignments survive resource recreation. There is no secret to leak, rotate or expire. Then say the strong version: code running in Azure that still holds a client secret is a finding.

"A user gets a token and the API returns 401. How do you debug it?" Decode the token and compare aud, iss, scp/roles and tid with what the API validates. The usual causes are requesting a token for the wrong resource, mixing v1.0 and v2.0 endpoints, or a missing scope. Naming the endpoint-version mismatch is the tell.

"What is the group overage claim?" Past roughly 200 groups, Entra omits groups and sends _claim_names/_claim_sources instead, so code reading groups sees none and grants nothing. Handle it by calling Graph — or avoid it entirely by using app roles, which do not overflow and express your application's vocabulary.

"What does conditional access mean for an application developer?" New error paths: MFA required, blocked by policy. Handle interaction-required by prompting rather than failing. Then mention continuous access evaluation as Entra's answer to token revocation, and the break-glass exclusion that stops a device-compliance policy locking the organisation out of its own tenant.

One thing to volunteer: point out that client secret expiry is one of the most common preventable Azure outages — a maximum 24-month life, an expiry date nobody recorded, and every dependent service failing at once. Certificates or managed identities remove it; if a secret is unavoidable, keep two active so rotation overlaps and alert 60 days ahead.

Recall

  • An app registration is the definition (home tenant only); a service principal is the instance per tenant and is what holds permissions and consent. AADSTS700016 = no service principal there yet.
  • Redirect URIs match exactly and the platform type matters — a SPA URI registered as "Web" fails as a CORS-looking error.
  • Delegated = act as the signed-in user (intersection of grant and user rights). Application = act as itselfMail.Read then covers every mailbox, and always needs admin consent. Default to delegated. .default is required for client credentials.
  • Managed identities remove the secret entirely. Prefer user-assigned so role assignments survive resource recreation. DefaultAzureCredential makes local and cloud code identical.
  • Client secrets expire at 24 months and the expiry is an outage. Use certificates or managed identities; if not, keep two active and alert 60 days ahead.
  • A token rejected by the API is almost always wrong aud, v1.0 vs v2.0 endpoint mismatch, or a missing scp/roles. Decode it and compare against what the API validates.
  • The group overage claim: past ~200 groups, groups is omitted in favour of _claim_names/_claim_sources, so authorisation silently grants nothing. Use app roles instead.
  • Conditional access appears as AADSTS50076/53003respond interactively, not with a generic failure. Continuous access evaluation is the near-real-time revocation answer. Exclude two monitored break-glass accounts from every policy.

Self-test: Which object holds consent, and which lives only in the home tenant? · What does Mail.Read as an application permission actually grant? · What replaces a client secret for code running in Azure? · Name three causes of "valid token, 401 from the API" · What silently breaks authorisation for a user in 250 groups? · Why must some accounts be excluded from every conditional access policy?

Next: 8.4.10 closes the identity folder with the question every one of these protocols hands back to you — now that you know who the user is, how do you decide what they may do?