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 itsaud, and tell a delegated call from an app-only call byscp-vs-roles. - Recite and defend the validation order — structure/
alg→ signature →iss→aud→nbf/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, thealg:nonebypass, the timing side-channel a non-constant-time compare leaks). - Explain PKCE from first principles —
code_challenge = BASE64URL(SHA256(verifier)), why the one-way hash means an attacker who intercepts the authcodestill 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 & discovery —
https://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 theissuer, thejwks_uri, and every endpoint so SDKs configure themselves. JWKS is the issuer's published set of public signing keys; the token header'skidselects 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
audis the API, and it carriesscp/roles; treat it as opaque and never decode it as a client. The ID token (OIDC) authenticates the user to the client — itsaudis the client, and it carriessub/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 rawcode_verifierat/token; the server re-hashes and compares. An attacker who intercepts thecodecan'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 →iss→aud→nbf/exp(with clock skew) → authorization claim.scp(space-delimited) is delegated authority (a user delegated it);rolesis app authority (the app itself was granted it). Skipaudand 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)
| Field | Value |
|---|---|
| Goal | Build 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 → iss → aud → nbf/exp → scp/roles in order, failing closed and naming the failed check |
| Concepts | base64url (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 |
| Steps | 1. 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 Test | pytest 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 bullet | Built 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:
- SPA → API → downstream API — a browser app uses auth-code+PKCE to call your API
(access token,
aud= your API,scpcarries 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) - 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) - API gateway authorization — API Management (or your API) validates every inbound JWT —
kid→JWKS→signature→iss→aud→exp→scp/roles— then routes; an invalidaudis a401, a missing scope is a403. This lab'svalidate_jwtis that policy. (→ P09) - 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)
- 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
- HITCHHIKERS-GUIDE.md — the 30-minute orientation; read first
- WARMUP.md — the full primer; read slowly
- BROTHER-TALK.md — the candid version
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.
audtells access token from ID token;scp-vs-rolestells 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
audalways (or you're a confused deputy); pin thealg(or you acceptalg: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.pyand yourlab.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) andscpvsroles(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