Phase 09 — RESTful APIs, JSON & Secure AuthN/AuthZ
Difficulty: ⭐⭐⭐☆☆ (the protocol) → ⭐⭐⭐⭐⭐ (the judgment about status codes, the validation-then-authorization order, and where the gateway sits) Estimated Time: 1 week (12–18 hours) Prerequisites: Phase 03 (OAuth2 / OIDC / JWT — this phase consumes the token that phase taught you to mint and validate) and Phase 00 (control plane vs data plane, identity is the perimeter). Comfortable reading JSON and HTTP. No live API Management instance is required for the lab.
Why This Phase Exists
The JD asks for one thing in plain language: "Develop and integrate RESTful APIs using
JSON and implement secure authentication and authorization mechanisms (OAuth2, OpenID
Connect)." That sentence hides the entire surface area of how the modern web talks to
itself — and the part that separates a senior from a principal is not "can you write a
controller," it is can you reason about the edge: the gateway that every request crosses
before it reaches your code, the contract that says which method is safe to retry, and the
exact moment a request becomes a 401 rather than a 403 rather than a 429.
Phase 03 built the token. This phase builds the thing that checks the token on every
request and decides what happens next — Azure API Management, modeled as the machine it
actually is: a policy pipeline (inbound → backend → outbound → on-error) that matches
the route, validates the JWT, authorizes the scope, enforces the rate limit, and only then
lets the call through. Almost everyone who has shipped an API can describe this vaguely.
Very few can tell you, without hand-waving, why PUT is idempotent and POST is not
(and why that decides whether a client may retry on a timeout), why a missing scope is
403 and a missing token is 401 (and why an SDK that confuses them causes a retry
storm), or how a token bucket computes the Retry-After you owe a throttled client.
Those are not trivia. "We returned 401 on an expired token and the client re-tried the
same expired token forever" is a real outage. "Our retry-on-5xx logic double-charged
customers because POST /charge isn't idempotent" is a real incident. This phase makes you
build the gateway so REST stops being a vibe and becomes a sequence of decisions you can
defend line by line.
What "Principal-Level" Means Here
A senior engineer wires an API behind APIM and it works. A principal understands the edge well enough to:
- Use the right method for the right semantics —
GET(safe, cacheable),PUTandDELETE(idempotent: same call twice = same state),POST(not idempotent: creates),PATCH(partial, not guaranteed idempotent) — and reason from idempotency to "may the client retry this on a timeout?", which is the whole reason the distinction exists. - Return the correct status code under pressure —
2xxsuccess,4xxyou (the client) made a mistake,5xxwe (the server) did; and inside4xxthe trio that gets confused:401(not authenticated — who are you?),403(authenticated, not authorized — not allowed),429(authenticated, allowed, but too fast). The wrong code makes the client do the wrong thing. - Place the gateway correctly and configure its pipeline — products / APIs / operations
in APIM; the
inboundpolicies that authenticate (validate-jwt), authorize (claim checks), throttle (rate-limit-by-key), and transform; thebackendcall; theoutboundreshaping; andon-error— and explain why each policy lives in the section it does. - Separate authentication from authorization at the edge —
validate-jwtproves the token is real, current, and for this API (signature /iss/aud/exp); the scope check (scpdelegated vsrolesapp-only) decides whether this caller may invoke this operation. AuthN is401-or-pass; AuthZ is403-or-pass. Conflating them is the most common edge bug. - Do the rate-limit arithmetic — a token bucket with a
capacity(burst) and arefill_per_sec(sustained rate); on empty, a429with aRetry-Afterthat says exactly how long until one token accrues — and the difference between a rate limit (smooth the burst) and a quota (cap the long-window total for fairness/billing).
Concepts
- REST as resource orientation. A REST API exposes resources (nouns:
/users/{id},/orders) and acts on them with a small fixed set of methods (verbs), rather than RPC-style endpoints (/createUser,/getUserById). The URL names what; the method names the operation; the body and status carry the result. This is whyGET /users/42andDELETE /users/42are the same resource, two operations. - The HTTP methods and their semantics. Safe = no observable state change
(
GET,HEAD,OPTIONS). Idempotent = doing it N times leaves the same state as doing it once (GET,PUT,DELETE,HEAD— and crucially notPOST).PATCHis partial update and is not guaranteed idempotent. The payoff: a client may safely retry an idempotent request after a timeout (it might already have succeeded) but must not blindly retry aPOST(it might create a second resource) — idempotency is the property that makes the network's "did it happen?" ambiguity survivable. - Status codes that mean something.
2xxsuccess (200 OK,201 Created,204 No Content);3xxredirect;4xxclient error (400malformed,401unauthenticated,403unauthorized,404not found,409conflict,415/406content negotiation,422semantic,429too many requests);5xxserver error (500,502,503,504). The split matters for retries: a client may retry5xx(our fault, transient) and429(with backoff) but must not retry4xxlike400/401/403unchanged — the request itself is wrong. 401vs403vs429— the three that decide client behavior.401 Unauthorizedis misnamed: it means un-authenticated — no token, or an invalid/expired one; the client should re-authenticate.403 Forbiddenmeans authenticated but not authorized — a valid token without the required scope; re-authenticating won't help, the client needs more scope/role.429 Too Many Requestsmeans authenticated and authorized but over budget — the client should back off forRetry-Afterseconds. Three different fixes; the gateway must return the code that triggers the right one.- Pagination and content negotiation. Large collections are paged — offset/limit
(simple, drifts under writes) or cursor/keyset (stable, the production default) — with
a
nextlink/cursor and often a total. Content negotiation lets the client ask for a representation (Accept: application/json) and declare what it sent (Content-Type); a mismatch is406/415. JSON is the lingua franca; the contract is the schema. - Azure API Management as the gateway. APIM sits in front of backends and exposes
products (a bundle of APIs with a subscription and policies) made of APIs made of
operations (method + URL template). Its core is the policy pipeline: every request
flows
inbound(where you authenticate, authorize, rate-limit, transform, route) →backend(the call to the origin) →outbound(reshape the response) →on-error(catch failures) — a programmable choke point that turns cross-cutting concerns into declarative policy instead of code in every service. - AuthN vs AuthZ at the edge.
validate-jwtauthenticates: the signature proves the token wasn't forged,issproves Entra issued it,audproves it's for this API (skip this and you're a confused deputy),exp/nbfprove it's current. The scope check authorizes:scp(space-delimited delegated scopes — a user is present) orroles(app roles — a daemon, client-credentials) must contain what the operation requires. AuthN failure →401; AuthZ failure →403. Fail closed: anything you can't prove, deny. - Token-bucket rate limiting. A bucket holds up to
capacitytokens and refills atrefill_per_sec; each request spends one; an empty bucket yields429+ aRetry-Afterof(1 − tokens) / refill_per_sec.capacityis the burst you tolerate;refill_per_secis the sustained rate you guarantee. A quota is the same idea over a long window (calls/day) for fairness and billing rather than burst smoothing.
Labs
Lab 01 — API Gateway: Routing, Scope Authz & Rate Limiting (flagship, implemented)
| Field | Value |
|---|---|
| Goal | Build the engine APIM runs on every request: a Route(method, path_template, required_scopes) matcher that extracts path params and prefers the most-specific template; an authorize(claims, required_scopes) that returns 401 (no/invalid token) / 403 (valid, insufficient scope) / 200, failing closed; a continuous-refill TokenBucket (and per-client RateLimiter) returning (allowed, retry_after); and a handle(...) pipeline that runs match (404) → authorize (401/403) → rate-limit (429 + Retry-After) → backend (200) in that order |
| Concepts | Path-template matching + param extraction + most-specific-wins; method dispatch; the 401/403/429 distinction; fail-closed scope containment (scp); token-bucket math + Retry-After; per-client bulkheading; the pipeline order as the security contract |
| Steps | 1. Route + match_route (params, most-specific, 404); 2. authorize (the three statuses, fail closed); 3. TokenBucket.allow (continuous refill, retry_after); 4. RateLimiter (per-client buckets); 5. handle (the four-stage pipeline, order pinned by tests) |
| How to Test | pytest test_lab.py -v — 29 tests: param extraction, most-specific-wins, method dispatch, 404; 401 vs 403 vs 200 for the scope cases; bucket allows to capacity then 429 with correct retry_after, refills after injected elapsed time, caps at capacity; and the pipeline ordering (404 before 401, 401 before 429), per-client isolation, determinism |
| Talking Points | "Walk me through what a gateway does to a request, in order." / "401 vs 403 — when does each fire and why does it matter operationally?" / "Implement a token bucket; what should Retry-After say?" / "Why route before auth, and auth before rate-limit?" |
| Resume bullet | Built an API-gateway engine (method + path-template routing with most-specific match and param extraction, OAuth2 scope authorization with the 401/403 fail-closed distinction, and a continuous-refill token-bucket rate limiter with Retry-After) modeling the Azure API Management inbound policy pipeline |
→ Lab folder: lab-01-api-gateway/
Integrated-Scenario Suggestions (carried through the whole track)
The phases compound. Keep these in mind — each plugs the gateway into a larger system:
- SPA → APIM → API → downstream API. A browser app got an access token via
auth-code + PKCE (Phase 03); APIM
validate-jwtchecksaud= your API andscpcarries the user's scopes; an insufficient scope is a403, an expired token a401. This is Phase 03'svalidate_jwtrunning as a policy on every request. (→ P03) - Daemon → APIM → API. A nightly job uses client-credentials; its token carries
roles, notscp, so the operation authorizes on app roles. The grant choice (P03) determines which claim the gateway authorizes on. (→ P03, P12) - Rate limiting as fairness and as defense.
rate-limit-by-keyper subscription smooths bursts and bulkheads tenants; aquota-by-keycaps the monthly total for billing; a flood gets429 + Retry-After, and a circuit breaker downstream (P14) sheds load when the backend is sick. The gateway is the first place reliability is enforced. (→ P14) - Managed Identity behind the gateway. APIM calls a backend that authenticates with a Managed Identity (P12) — no secret on the wire — and the backend reads Key Vault for anything else. The gateway authenticates the caller; MSI authenticates the backend. (→ P12)
- Observability of the edge. Every
401/403/429/5xxis a signal; APIM emits to Application Insights and Log Analytics, and a KQL query (P13) over the gateway logs is how you find "tenant X is getting 90%403s — they're missing a scope." (→ P13)
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
- The gateway is a pipeline, and the order is the policy. Match (
404) before auth (no operation to authorize), auth (401/403) before rate-limit (429) (don't let probing cost rate budget), then the backend (200). Reorder it and you have a security or a fairness bug, not just a style choice. 401≠403≠429, and each triggers a different client action.401re-authenticate,403get more scope,429back off. The whole reason status codes are standardized is so a generic client does the right thing without reading your docs — pick the wrong one and you've broken that contract.- Idempotency decides retryability.
PUT/DELETE/GETare idempotent and safe to retry on a timeout;POSTis not. Designing your write as idempotent (or giving it an idempotency key) is what makes the network's "did it happen?" ambiguity survivable. - Authenticate, then authorize, and fail closed. Prove the token is real, current, and
for you (
validate-jwt); then check the scope. Anything you can't prove denies. Skipaudand you're a confused deputy; conflate401and403and clients retry forever. - Rate-limit math is arithmetic, not magic.
capacity= burst,refill= sustained,Retry-After = (1 − tokens) / refill. Quota is the same idea over a long window. Put the numbers in the design review, not in the post-incident review.
Deliverables Checklist
-
Lab 01 implemented; all 29 tests pass against
solution.pyand yourlab.py - You can recite the gateway pipeline order and say what each stage's failure code is
-
You can state the
401/403/429rule and the client action each should trigger - You can list which HTTP methods are idempotent and explain why that decides retryability
-
You can implement a token bucket and compute the
Retry-Afterit owes a client -
You can explain
scpvsrolesauthorization and which grant produces each (ties to P03) -
You can place
validate-jwt,rate-limit-by-key, and routing in the right APIM pipeline section and say why each belongs there