Phase 03 — Microsoft Entra ID: OAuth2 / OIDC / JWT

Difficulty: ⭐⭐⭐☆☆ (the protocol) → ⭐⭐⭐⭐⭐ (the judgment about grants, claims, and the validation order) Estimated Time: 1 week (12–18 hours) Prerequisites: Phase 00 (control plane vs data plane, identity is the perimeter) and Phase 01 (the ARM request pipeline — this phase builds the authenticate gate that pipeline starts with). Comfortable reading JSON. No live Entra tenant required for the lab.


Why This Phase Exists

Phase 01 taught the single most important sentence about the Azure control plane: every write is one idempotent PUT through ARM, and ARM's first gate is authenticate the caller's Entra token. This phase opens that gate and builds the machine inside it — because identity is the perimeter, and in a cloud with no network edge to hide behind, the token is the security boundary. Get the token wrong and nothing else you built matters.

Here is the thing almost nobody can actually do in an interview, even people who've wired up "Login with Microsoft" a dozen times: draw the authorization-code + PKCE flow from memory, say exactly what PKCE defends against, and then list — in order — every check a resource API must run on the JWT it receives. They can name OAuth2. They cannot tell you why a public SPA must use PKCE but a daemon must not, why the access token's aud is your API but the ID token's aud is the client, what the difference between scp and roles signals about who is calling, or which single skipped check turns your service into a confused deputy that honors a token minted for somebody else.

Those are not academic distinctions. "We validated the signature but not the aud" is a real breach shape: service A accepts a perfectly-signed, perfectly-current token that Entra issued for service B, and now an attacker with legitimate access to B is acting as A. "alg: none" is a real CVE family. "The clock skew rejected every token for ten minutes" is a real outage. This phase makes you build the token engine — the base64url codec, the PKCE proof, the authorization server's two grants, and above all the validator whose check order is the security — so that OAuth2 stops being three letters you nod at and becomes a sequence of if statements you can defend line by line. This is the keystone for P04 (RBAC, which runs after this gate), P09 (the API gateway that runs this validation on every request), and P12 (Managed Identity, which is just this flow with the secret removed).

What "Principal-Level" Means Here

A senior engineer wires an app to Entra with an SDK and it works. A principal understands the protocol well enough to:

  • Pick the right grant on sight — authorization-code+PKCE for a user in a browser/native app, client-credentials for a daemon with no user, on-behalf-of for a middle tier passing the user's identity downstream, device-code for a screen with no keyboard — and explain why each alternative is wrong for the case.
  • Read a JWT like a sentence — name what every claim means (iss, aud, exp, nbf, scp, roles, appid/azp, oid/tid), tell an access token from an ID token by its aud, and tell a delegated call from an app-only call by scp-vs-roles.
  • Recite and defend the validation order — structure/alg → signature → issaudnbf/exp (with skew) → scope/role — and explain why each step is where it is and what breaks if you reorder or skip one (the confused deputy, the alg:none bypass, the timing side-channel a non-constant-time compare leaks).
  • Explain PKCE from first principlescode_challenge = BASE64URL(SHA256(verifier)), why the one-way hash means an attacker who intercepts the auth code still can't redeem it, and why this replaced the implicit flow entirely for public clients.
  • Reason about Conditional Access as a policy engine over signals (user, device, location, risk) that gates token issuance — the layer that turns "valid credentials" into "valid credentials, from a compliant device, in an expected place, with MFA."

Concepts

  • The Entra ID model — a tenant is an isolated directory (a GUID); it holds users and groups; an app registration is the global definition of an application (its client id, redirect URIs, secrets/certs, exposed scopes, app roles), and a service principal is that app's local identity in a tenant (what role assignments and consent attach to). One app registration → one service principal per tenant it's used in. Get this app-registration-vs-service-principal distinction cold; it confuses everyone.
  • The v2.0 endpoints & discoveryhttps://login.microsoftonline.com/{tenant}/oauth2/v2.0/{authorize,token}, and the self-describing OpenID Connect discovery document at …/{tenant}/v2.0/.well-known/openid-configuration, which publishes the issuer, the jwks_uri, and every endpoint so SDKs configure themselves. JWKS is the issuer's published set of public signing keys; the token header's kid selects which one.
  • OAuth2 grants — and exactly when each is correct. Authorization code (+ PKCE) for an interactive user (the only right answer for SPAs and native/mobile apps). Client credentials for a daemon/service with no user (app-only, authenticates with its own secret or cert). On-behalf-of (OBO) for a middle-tier API that must call a downstream API as the original user. Device code for input- constrained devices (TVs, CLIs, IoT). Choosing wrong is a design smell an interviewer pounces on.
  • Access token vs ID token vs refresh token. The access token authorizes a call to a resource — its aud is the API, and it carries scp/roles; treat it as opaque and never decode it as a client. The ID token (OIDC) authenticates the user to the client — its aud is the client, and it carries sub/name/preferred_username. The refresh token buys new access tokens without re-prompting; it never leaves the client.
  • OIDC: authentication vs authorization. OAuth2 is authorization ("what may this caller do"); OIDC layers authentication on top ("who is this user") via the ID token and the userinfo/discovery machinery. Authentication answers who; authorization answers what. A principal never conflates them — and the lab's validator separates them (signature+iss+ aud+time = authentication of the token; scp/roles = authorization).
  • PKCE (RFC 7636). A public client can't keep a secret, so it can't prove at the token endpoint that it started the login. PKCE fixes it without a secret: send code_challenge = BASE64URL(SHA256(code_verifier)) (method S256) to /authorize, then present the raw code_verifier at /token; the server re-hashes and compares. An attacker who intercepts the code can't redeem it — they never saw the verifier.
  • JWT structure & the validation ORDER. base64url(header).base64url(payload).base64url(signature). Validate, failing closed, in this order: kid → JWKS key → signature → issaudnbf/exp (with clock skew) → authorization claim. scp (space-delimited) is delegated authority (a user delegated it); roles is app authority (the app itself was granted it). Skip aud and you've built the confused deputy.
  • Conditional Access (framing). A policy engine: signals (user/group, device compliance, location/IP, sign-in risk, app) → a decision (grant / block / require MFA / require a compliant device). It gates token issuance at /authorize — the modern "Zero Trust" control plane on top of OAuth2.

Labs

Lab 01 — OAuth2 / OIDC / JWT Engine (flagship, implemented)

FieldValue
GoalBuild a miniature Entra ID token engine: the base64url codec, the PKCE S256 challenge, an authorization server running the auth-code+PKCE and client-credentials grants offline, HS256 JWT signing, and a JWT validator that runs signature → issaudnbf/expscp/roles in order, failing closed and naming the failed check
Conceptsbase64url (no padding); PKCE one-way proof; access vs ID token (by aud); scp vs roles; the validation order; the confused deputy; alg:none; constant-time compare; clock-skew leeway; single-use codes
Steps1. b64url_encode/decode; 2. pkce_challenge (S256 + plain); 3. make_jwt/decode_jwt_unverified (HS256); 4. validate_jwt — the seven ordered checks; 5. AuthorizationServer.authorize (mint a PKCE-bound one-time code); 6. token_authorization_code (verifier must hash to the challenge → access + ID tokens); 7. token_client_credentials (secret check → app token with roles)
How to Testpytest test_lab.py -v — 32 tests: PKCE S256 known-answer vector + plain, sign/verify round-trip + determinism, the validator happy path, and a named negative test for every failure (bad signature, tampered payload, alg:none, wrong iss, wrong aud, expired, not-yet-valid, missing scope, missing role), leeway boundaries, the full auth-code+PKCE flow with wrong-verifier rejection and single-use, and client-credentials with a wrong-secret rejection
Talking Points"Draw auth-code + PKCE and tell me what PKCE defends against." / "What does a resource API check on a JWT, in what order, and why that order?" / "scp vs roles — which grant produces each?" / "What's the confused deputy and which check prevents it?"
Resume bulletBuilt an OAuth2/OIDC authorization-server and JWT-validation engine (authorization-code + PKCE and client-credentials grants, constant-time signature verification, ordered iss/aud/exp/nbf and scp/roles claim checks with clock skew) modeling Microsoft Entra ID

→ Lab folder: lab-01-oauth-oidc-engine/

Integrated-Scenario Suggestions (carried through the whole track)

The phases compound. Keep these in mind as you build — each plugs the token engine into a larger system:

  1. SPA → API → downstream API — a browser app uses auth-code+PKCE to call your API (access token, aud = your API, scp carries the user's scopes); your API uses on-behalf-of to call Microsoft Graph as the user. Three audiences, two grants, one user identity preserved end-to-end. (→ P09)
  2. Daemon ingestion job — a nightly service uses client-credentials (no user) to write to a storage account; its token carries roles, and the storage RP authorizes via RBAC on the service principal. The grant choice is the identity model. (→ P04, P10)
  3. API gateway authorization — API Management (or your API) validates every inbound JWT — kid→JWKS→signature→issaudexpscp/roles — then routes; an invalid aud is a 401, a missing scope is a 403. This lab's validate_jwt is that policy. (→ P09)
  4. CI/CD without stored secrets — a pipeline presents its OIDC token to Entra and gets an Azure access token via workload-identity federation (matching issuer/subject/ audience) — the same JWT validation, applied to a federated credential, with nothing to leak. (→ P08)
  5. Managed Identity — a Function asks IMDS for a token and gets an Entra access token with no secret at all; downstream APIs validate it exactly like any other Entra JWT. This phase is the protocol; P12 removes the credential. (→ P12)

Guides in This Phase

Key Takeaways

  • Identity is the perimeter, and the JWT is the boundary. In a cloud with no network edge, the token is what stands between your service and the world — so the validator that checks it is the most security-critical code you'll write.
  • The grant encodes the identity model. Auth-code+PKCE = a user is present; client-creds = an app acting as itself; OBO = a middle tier acting as the user; device-code = no browser. Choosing the grant is deciding who is calling and how they prove it.
  • aud tells access token from ID token; scp-vs-roles tells delegated from app-only. Two distinctions that, once internalized, make every token legible at a glance.
  • The validation order is the security. Verify the signature before you read any claim (it's attacker JSON until then); check aud always (or you're a confused deputy); pin the alg (or you accept alg:none); compare in constant time; allow clock skew. Order is not style — reorder it and you have a vulnerability.
  • PKCE made public clients safe without secrets, which is the same idea that makes Managed Identity (P12) and OIDC federation (P08) the modern, secretless default: prove possession, don't store a password.

Deliverables Checklist

  • Lab 01 implemented; all 32 tests pass against solution.py and your lab.py
  • You can draw the authorization-code + PKCE flow from memory and say what PKCE defends against
  • You can list the JWT validation order and explain why each step is where it is
  • You can explain access token vs ID token (by aud) and scp vs roles (by grant)
  • You can explain the confused-deputy attack and the single check that prevents it
  • You can pick the correct grant for a SPA, a daemon, a middle tier, and a CLI — and say why the others are wrong
  • You can explain app registration vs service principal in one breath