AI WisdomArchitecture & guides β†—
HT
How Things Work

Microsoft Entra ID

OAuth 2.0 and OIDC identity platform β€” app registrations, managed identities, and the token validation steps your API must not skip.

How It Works

Entra ID (formerly Azure Active Directory) is Microsoft's identity platform implementing OAuth 2.0 and OpenID Connect. It handles authentication for web apps and APIs, service-to-service authorization, and credential-free access via managed identities. The complexity lies in understanding which flow to use (auth code for users, client credentials for services), what audience to request tokens for, and how managed identity interacts with VNET routing.

1
App Registrations vs Enterprise Applications

Every application using Entra ID gets two objects. The App Registration is the global definition β€” it lives in your tenant and defines the app's identity, redirect URIs, API permissions, and exposed scopes. The Enterprise Application (Service Principal) is the local instantiation in a specific tenant β€” it holds consent grants, role assignments, and sign-in activity. When another tenant uses your app (multi-tenant), they get their own Enterprise Application but reference your App Registration.

2
OAuth 2.0 Authorization Code Flow β€” The Web App Pattern

For web apps with a user sign-in: the app redirects the user to login.microsoftonline.com with client_id, redirect_uri, scope, and a state parameter. Entra ID authenticates the user and returns an authorization code. The app exchanges the code for an access token and refresh token using its client secret. The access token (JWT) carries the user's claims. PKCE (Proof Key for Code Exchange) replaces client secrets for public clients (SPAs, mobile).

3
Client Credentials Flow β€” Service-to-Service

For background services or APIs calling other APIs without a user context: the service authenticates directly with its client ID and client secret (or certificate). No user consent needed β€” an admin consents to the app permissions at the tenant level. The resulting token has application permissions (roles), not delegated permissions (scopes). This is the pattern for microservices, Azure Functions, and WebJobs calling other APIs.

4
Managed Identity β€” Credentials Azure Manages For You

System-assigned managed identity creates an identity tied to a specific Azure resource's lifecycle. User-assigned managed identity is standalone and can be assigned to multiple resources. Either way, Azure manages the credential rotation β€” no secrets in code, no certificate expiry surprises. DefaultAzureCredential in the Azure SDK tries managed identity first, then Visual Studio credentials in dev. Works for Key Vault, Storage, Service Bus, SQL Server (with Azure AD auth enabled).

5
Token Validation β€” What Your API Must Check

When your API receives a Bearer token, it must validate: the signature (using Entra ID's public keys from the OIDC metadata endpoint), the issuer (iss claim must match your tenant), the audience (aud claim must match your API's client ID), and expiry (exp claim). Microsoft.Identity.Web handles all of this. What it doesn't validate automatically: the scp (scope) or roles claims β€” use [RequiredScope] or [Authorize(Roles = "...")] for that.

Key Concepts

πŸ“‹App Registration

Global definition of an application in your Entra ID tenant. Defines client ID, redirect URIs, API permissions requested, scopes exposed, and token configuration. One app registration; can have multiple Enterprise Applications (one per tenant it's used in).

πŸ€–Managed Identity

An identity for Azure resources managed by Azure β€” no credentials to store or rotate. System-assigned: tied to a specific resource, deleted with it. User-assigned: standalone, assignable to multiple resources. Use DefaultAzureCredential to consume it in code.

πŸ”‘OAuth 2.0 Scope

Permission unit requested by clients. Delegated scopes (User.Read, orders.read) represent actions on behalf of a user. Application permissions (Mail.ReadAll) grant access without a user. Your API defines its own scopes in its App Registration.

🎟️Access Token

Short-lived JWT (typically 1 hour) proving identity and permissions. Contains claims: oid (user/app ID), scp (delegated scopes), roles (app roles), aud (intended audience), exp (expiry). Validate aud against your API's client ID to prevent token substitution attacks.

πŸ›‘οΈConditional Access

Policies that evaluate sign-in risk, device compliance, and location to grant or deny access and require MFA. Evaluated at token issuance. If a user's device becomes non-compliant after token issuance, the policy doesn't retroactively revoke the existing token.

πŸ”PKCE

Proof Key for Code Exchange β€” replaces client secrets for public clients (SPAs, native apps) in the auth code flow. The app creates a code_verifier, hashes it to code_challenge, sends the challenge in the auth request, and proves it by sending the verifier in the token request. Prevents auth code interception attacks.

API Protection, Managed Identity & Token Acquisition
tsx
1// ASP.NET Core β€” protect API with Entra ID (Bearer token validation)
2builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
3 .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
4
5// appsettings.json
6{
7 "AzureAd": {
8 "Instance": "https://login.microsoftonline.com/",
9 "TenantId": "your-tenant-id",
10 "ClientId": "your-api-app-id", // The API's own app registration
11 "Audience": "api://your-api-app-id" // Must match 'aud' claim in token
12 }
13}
14
15// Controller β€” require scope from the token
16[Authorize]
17[RequiredScope("orders.read")]
18public class OrdersController : ControllerBase { }
19
20// Managed Identity β€” call another service without credentials in code
21// No client secret, no certificate β€” Azure manages the identity
22var credential = new DefaultAzureCredential(); // picks up managed identity automatically
23var client = new SecretClient(
24 new Uri("https://my-vault.vault.azure.net/"),
25 credential);
26var secret = await client.GetSecretAsync("db-password");
27
28// App registration β€” expose an API scope
29// In your API's app registration, define a scope:
30// "orders.read" with admin consent required
31// Client apps request this scope when acquiring tokens
32
33// Client app acquires token for the API
34var app = ConfidentialClientApplicationBuilder
35 .Create(clientId)
36 .WithClientSecret(clientSecret)
37 .WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}"))
38 .Build();
39
40var result = await app.AcquireTokenForClient(
41 new[] { "api://your-api-app-id/.default" }) // .default requests all configured scopes
42 .ExecuteAsync();
43
44httpClient.DefaultRequestHeaders.Authorization =
45 new AuthenticationHeaderValue("Bearer", result.AccessToken);
πŸ’‘
Why This Matters

Entra ID eliminates the need to build auth infrastructure. Managed identity removes secrets from code entirely β€” no credential rotation, no secrets in environment variables, no certificate expiry incidents. For internal services, it's the difference between secure-by-default and secure-if-you-remember-to-rotate-credentials.

Common Pitfalls

⚠Access tokens are valid for 1 hour by default and cannot be revoked mid-lifetime (Continuous Access Evaluation extends this with near-real-time revocation for compliant clients). Conditional Access policy changes take effect at next token acquisition, not immediately.
⚠The 'aud' claim in a token identifies who the token is FOR. Your API must validate that aud matches its own client ID. A token acquired for ServiceA cannot be used against your API β€” this is the intended behavior, not a bug.
⚠Managed identity requires that the hosting environment can reach the IMDS endpoint (169.254.169.254). VNET routing rules or Azure Firewall can block this traffic. Test managed identity in your actual network topology.
⚠App permissions (roles) require admin consent and are granted at the tenant level. Delegated permissions (scopes) require user consent or admin pre-consent. Confusing the two is the most common cause of 'permission denied' errors that work for admins but not regular users.
⚠Multi-tenant apps: tokens issued from a different tenant will have a different issuer (iss) claim. Configure your token validation to accept multiple issuers or use the /common endpoint with additional issuer validation logic.
Real-World Use Cases

1API Returns 401 Despite Valid Token From Other Service

Scenario

ServiceA calls your API with a valid token acquired from Entra ID. The API returns 401 Unauthorized. The token validates fine in jwt.ms, the scopes look correct, and ServiceA can call other APIs successfully with the same flow.

Problem

The token's 'aud' (audience) claim contains ServiceA's client ID, not your API's client ID. ServiceA was requesting a token for itself ('/.default' against its own app ID) rather than for your API. The token is valid β€” just not for your API. Your API's JWT validation correctly rejects it because the audience doesn't match.

Solution

ServiceA must request the token with your API's scope as the resource: new[] { 'api://your-api-client-id/.default' }. This produces a token with aud set to your API's client ID. Also verify your API's AzureAd:ClientId setting matches the audience the token is issued for. Common mistake: copying settings from another service's appsettings without updating the ClientId.

πŸ’‘

Takeaway: JWT audience validation prevents token substitution β€” a valid token for ServiceA cannot be used against your API. When a service-to-service call returns 401, check the 'aud' claim in the token at jwt.ms. It must match your API's client ID, not the caller's.

2Managed Identity Works Locally But Fails in Production

Scenario

A .NET app uses DefaultAzureCredential to access Key Vault. Works perfectly in development (developers have logged in via 'az login'). In production on App Service with managed identity enabled, it fails with 'ManagedIdentityCredential authentication failed: IMDS endpoint not responding within 1000ms'.

Problem

The App Service was deployed to a consumption plan with outbound VNET restrictions. The IMDS (Instance Metadata Service) endpoint at 169.254.169.254 is a link-local address that traffic is routed through β€” not the internet β€” but certain VNET configurations (specifically UDR rules that route 0.0.0.0/0 through a firewall) intercept this traffic and block it. DefaultAzureCredential's managed identity credential times out after 1 second by default.

Solution

Add an explicit UDR exception for 169.254.169.254/32 β†’ Virtual Network to bypass the firewall for IMDS traffic. Alternatively, configure the DefaultAzureCredential to exclude irrelevant credential sources and increase the IMDS timeout: new ManagedIdentityCredential(new ManagedIdentityCredentialOptions { Transport = new HttpClientTransport(new HttpClient { Timeout = TimeSpan.FromSeconds(5) }) }). Add a startup health check that validates Key Vault access.

πŸ’‘

Takeaway: IMDS (169.254.169.254) is how managed identity tokens are obtained in Azure. VNET routing rules or firewalls can intercept this traffic. Always test managed identity in your actual network topology, not just with az login credentials locally. Add Key Vault access to your app's startup health check.