Warmup Guide — RESTful APIs, JSON & Secure AuthN/AuthZ
Zero-to-principal primer for Phase 09: REST as resource orientation, the HTTP methods and the idempotency that decides retryability, the status codes that mean something (and the
401/403/429trio everyone gets backwards), pagination and content negotiation, Azure API Management and its policy pipeline, authentication-vs-authorization at the edge (validate-jwtthen scope), and the token-bucket math behind a rate limiter. Every concept goes from what it is to why it exists to the mechanism under the hood (diagrams, tables, code, math) to production significance to the misconceptions that get people paged.
Table of Contents
- Chapter 1: REST — Resources, Not Procedures
- Chapter 2: HTTP Methods, Safety, and Idempotency
- Chapter 3: Status Codes That Mean Something
- Chapter 4: JSON, Pagination, and Content Negotiation
- Chapter 5: Azure API Management — The Gateway
- Chapter 6: The Policy Pipeline — Inbound, Backend, Outbound, On-Error
- Chapter 7: Authentication vs Authorization at the Edge
- Chapter 8:
401vs403vs429— The Three That Decide Client Behavior - Chapter 9: Token-Bucket Rate Limiting — The Math
- Chapter 10: Route Matching — Templates, Params, and Specificity
- Chapter 11: The Pipeline Order Is the Policy
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: REST — Resources, Not Procedures
From zero. Before REST, web APIs were RPC — you called a procedure by name over
HTTP: POST /createUser, POST /getUserById, POST /deleteUser. Every endpoint was a new
verb, every team invented its own, and nothing was uniform. REST (Representational State
Transfer, Roy Fielding's 2000 dissertation) inverts this: you model the world as
resources — nouns with stable URLs — and act on them with a small fixed set of
methods. /users/{id} is a resource; GET reads it, PUT replaces it, DELETE removes
it, POST /users creates one. The URL says what; the method says the operation; the
status code and body say the result.
Why it exists. Uniformity is the whole point. Because every REST API uses the same nine
methods and the same status-code families, a client, a cache, a proxy, and a gateway can all
reason about any REST API without reading its docs: a cache knows GET is cacheable; a
client knows it may retry a PUT; a gateway knows 429 means throttle. RPC throws all of
that away — the verb POST /doTheThing is opaque to every layer but the one that implemented
it. REST trades a little expressiveness for an enormous amount of shared, machine-readable
semantics, and that trade is why it won.
Mechanism. A REST request is METHOD URL HEADERS [BODY]; a response is STATUS HEADERS [BODY]. The contract lives in three places: the URL template (/users/{id} — the
resource shape), the method (the operation's semantics), and the media type (Content- Type: application/json — the representation). A well-designed API is "obvious": you can guess
GET /orders/99/items/3 without asking.
Misconception. "REST means JSON over HTTP." No — REST is the constraints (resources,
uniform methods, statelessness, cacheability, HATEOAS); JSON is just the most common
representation. You can do REST with XML, and you can do non-REST (RPC) with JSON — which is
what most "REST APIs" that have POST /api/doStuff endpoints actually are.
Chapter 2: HTTP Methods, Safety, and Idempotency
From zero. The methods are not interchangeable; each carries a promise about what it does to server state. Two properties matter more than any other:
- Safe — the method has no observable effect on server state.
GET,HEAD,OPTIONSare safe: a crawler can fetch them freely. (AGETthat deletes something is a famous bug class — search engines have wiped admin panels by following "delete" links that wereGETs.) - Idempotent — performing it N times leaves the resource in the same state as performing it once. This is the property that decides retryability.
| Method | Safe | Idempotent | Meaning | May a client retry on timeout? |
|---|---|---|---|---|
GET | ✅ | ✅ | read a resource | yes |
HEAD | ✅ | ✅ | read headers only | yes |
PUT | ❌ | ✅ | replace a resource wholesale | yes — second PUT = same state |
DELETE | ❌ | ✅ | remove a resource | yes — already-gone stays gone |
POST | ❌ | ❌ | create / process (non-idempotent) | no — may create a duplicate |
PATCH | ❌ | ❌ (not guaranteed) | partial update | only if you made it idempotent |
Why idempotency is the whole game. Networks are unreliable: a client sends a write, the
server processes it, the response is lost. Did it happen? The client cannot tell. If the
write was idempotent, the client simply retries — worst case it re-applies the same state,
no harm. If it was a POST /charge, retrying might charge the customer twice. So
idempotency is not academic: it is the property that makes the network's fundamental
ambiguity ("did my request succeed?") survivable. Principals design writes to be idempotent
— a PUT to a known id, or a POST with an idempotency key the server dedups on — so
clients can retry fearlessly. (Azure's whole control plane is built on this: every ARM write
is an idempotent PUT, Phase 01.)
PATCH is the trap. PATCH is partial update, and the RFC does not guarantee it's
idempotent. PATCH {"balance": "+10"} (relative) applied twice adds 20; PATCH {"name": "Ann"} (absolute) applied twice is fine. If you offer PATCH, you decide and document which
it is — and most clients should not blindly retry it.
Chapter 3: Status Codes That Mean Something
From zero. The status code is a three-digit machine-readable verdict, grouped by first digit:
1xx informational (rare; 100 Continue, 101 Switching Protocols)
2xx success 200 OK · 201 Created · 202 Accepted · 204 No Content
3xx redirection 301 Moved · 304 Not Modified (caching)
4xx CLIENT error YOU sent something wrong — do not retry unchanged
5xx SERVER error WE failed — usually transient, retry with backoff
The split that decides retries. The 4xx/5xx boundary is the single most important
distinction: a 4xx says the request itself is wrong (a bad body, a missing token, an
unknown route) — retrying the same request gets the same error, so a smart client fixes
the request or gives up. A 5xx says the server failed (a crash, a dependency timeout) —
which is usually transient, so a smart client retries with exponential backoff (Phase
14). Getting this wrong is expensive in both directions: returning 500 for a malformed body
makes clients hammer you with a request that can never succeed; returning 400 for a
transient backend hiccup makes them give up on a request that would have worked.
The 4xx codes you'll actually return:
| Code | Name | When |
|---|---|---|
400 | Bad Request | malformed syntax, unparseable JSON, missing required field |
401 | Unauthorized | un-authenticated — no/invalid/expired token (see Ch. 8) |
403 | Forbidden | authenticated but not authorized — wrong scope/role |
404 | Not Found | no such resource / route |
405 | Method Not Allowed | route exists, but not for this method |
406 / 415 | Not Acceptable / Unsupported Media Type | content-negotiation mismatch |
409 | Conflict | the write conflicts with current state (e.g. optimistic-concurrency) |
422 | Unprocessable Entity | syntactically valid but semantically wrong |
429 | Too Many Requests | rate/quota exceeded — back off (see Ch. 8–9) |
Misconception. "200 with {"error": ...} is fine." No — burying errors in a 200
body breaks every layer that reads the status: monitoring thinks you're healthy, the gateway
won't retry, the cache caches the error. The status code is the protocol-level truth; use
it.
Chapter 4: JSON, Pagination, and Content Negotiation
JSON is the lingua franca: a Content-Type: application/json body whose contract is a
schema (OpenAPI / JSON Schema). The principal discipline is to treat that schema as a
versioned contract — additive changes (new optional field) are safe; removing a field or
tightening a type is a breaking change that needs a new version (/v2/...) or a
deprecation window, exactly like the schema-compatibility rules in a data platform.
Pagination. You never return a million rows in one response. Two strategies:
- Offset / limit —
GET /users?offset=40&limit=20. Simple, but drifts: if rows are inserted/deleted between pages, you skip or repeat items, and deep offsets are slow (the DB scans and discardsoffsetrows). - Cursor / keyset —
GET /users?after=<opaque_cursor>&limit=20, where the cursor encodes "the last key I saw." Stable under concurrent writes and O(1) to resume — the production default. The response carries anextcursor (often as a link), and the client follows it until it's absent.
Content negotiation. The client says what representation it wants (Accept: application/json) and what it sent (Content-Type: application/json); the server picks the
best it can produce. A Content-Type the server can't parse is 415 Unsupported Media
Type; an Accept it can't satisfy is 406 Not Acceptable. This is how one URL can
serve JSON to an app and CSV to a spreadsheet.
Chapter 5: Azure API Management — The Gateway
From zero. An API gateway is a reverse proxy that sits in front of one or more backend services and is the single front door every client request passes through. Azure API Management (APIM) is Azure's managed gateway. Why have one at all? Because there are cross-cutting concerns — authentication, authorization, rate limiting, caching, transformation, logging, versioning — that every backend needs, and you do not want to re-implement them (subtly differently, with subtly different bugs) in every microservice. The gateway centralizes them: write the policy once, at the edge, and every backend inherits it.
The APIM object model (learn these nouns; interviewers use them):
Product ── a bundle of APIs with a subscription (key) and product-level policies
└─ API ── a set of operations sharing a base path and backend (e.g. the "Users API")
└─ Operation ── one method + URL template (e.g. GET /users/{id})
└─ Policy ── the inbound/backend/outbound/on-error rules that run for it
A subscription (and its subscription key) is how a consumer is identified and metered; a product decides what's bundled and gated. Policies can be attached at every level (global → product → API → operation) and they compose — operation policies run inside API policies inside product policies, like nested middleware.
Why it exists / production significance. The gateway is where you enforce the things that must be uniform and non-bypassable: no backend should ever see an unauthenticated request, no tenant should be able to flood another, and no client should depend on a backend's internal URL. Centralizing that at APIM means a security fix (e.g. tightening JWT validation) ships once and protects everything — and a backend that someone forgot to secure is still protected because traffic can't route around the gateway.
Misconception. "The gateway is just a proxy." It's a programmable proxy: the policy pipeline (next chapter) is a small language for rewriting, authenticating, throttling, and routing every request and response — far more than forwarding bytes.
Chapter 6: The Policy Pipeline — Inbound, Backend, Outbound, On-Error
Mechanism. Every request through APIM runs a four-section pipeline, and which section a policy lives in is part of its meaning:
┌──────────────────────────────────────────────┐
request ──▶│ INBOUND authenticate (validate-jwt), │
│ authorize (claim checks), │
│ rate-limit (by-key), transform, │
│ cache-lookup, route/rewrite │
└───────────────────────┬──────────────────────┘
│ (if not short-circuited)
┌───────────────────────▼──────────────────────┐
backend ──▶│ BACKEND the call to the origin server │
│ (set backend, mTLS, MSI, timeout) │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────▼──────────────────────┐
response ◀──│ OUTBOUND reshape response, set headers, │
│ cache-store, strip internal fields │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
error ◀──│ ON-ERROR catch anything thrown above; │
│ shape the error response │
└──────────────────────────────────────────────┘
In real APIM this is XML:
<policies>
<inbound>
<base />
<validate-jwt header-name="Authorization" failed-validation-httpcode="401">
<openid-config url="https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration" />
<audiences><audience>api://my-api</audience></audiences>
<issuers><issuer>https://login.microsoftonline.com/{tid}/v2.0</issuer></issuers>
<required-claims>
<claim name="scp" match="all"><value>users.read</value></claim>
</required-claims>
</validate-jwt>
<rate-limit-by-key calls="10" renewal-period="1" counter-key="@(context.Subscription.Key)" />
</inbound>
<backend><forward-request /></backend>
<outbound><base /></outbound>
<on-error><base /></on-error>
</policies>
The key insight: inbound policies run in document order, and an inbound policy can
short-circuit the whole pipeline (a failed validate-jwt returns 401 and the backend is
never called). That ordering is the gateway's security and fairness logic — which is
exactly what the lab's handle function models in Python.
Chapter 7: Authentication vs Authorization at the Edge
From zero — the two questions. Authentication asks "who are you?"; authorization asks "are you allowed to do this?" They are different checks, run in order, and they fail with different status codes. Conflating them is the most common edge bug.
AuthN — validate-jwt (Phase 03's validator, run as a policy). The gateway proves the
bearer token is real and meant for it, in order:
- Signature — recompute/verify with the issuer's public key (selected by the token's
kidfrom the issuer's JWKS). Until this passes, the payload is attacker-controlled JSON — read nothing from it. iss— the token was issued by your Entra tenant, not some other issuer.aud— the token's audience is this API (api://my-api). Skip this and you're a confused deputy: you'd accept a perfectly-signed token Entra minted for a different service.exp/nbf— the token is currently valid (within clock-skew leeway).
Any failure here is 401 — the token did not authenticate. Pass, and you know who the
caller is.
AuthZ — the scope/role check. Now decide if this caller may invoke this operation:
scp(space-delimited) — delegated scopes: a user is present and consented (e.g."users.read orders.write"). Produced by the auth-code + PKCE grant.roles— app roles: the application itself was granted them (no user). Produced by the client-credentials grant.
The operation requires a set of scopes; authorization passes iff required ⊆ granted. A
failure here is 403 — authenticated, but not authorized.
┌─────────────┐ no/invalid token ┌──────┐
token ─▶│ validate-jwt│────────────────────▶│ 401 │
└──────┬──────┘ └──────┘
│ valid (we know who)
┌──────▼──────┐ scp ⊉ required ┌──────┐
│ scope check │────────────────────▶│ 403 │
└──────┬──────┘ └──────┘
│ scp ⊇ required
┌──▼──┐
│ ok │ → rate limit → backend
└─────┘
Fail closed. The default at every step is deny. No token, malformed token, can't reach
JWKS, missing the claim you authorize on — all deny. "Allow on doubt" is how breaches happen.
The lab's authorize encodes exactly this: not-a-dict / not-valid / no scp → 401;
scp missing a required scope → 403; otherwise 200.
Chapter 8: 401 vs 403 vs 429 — The Three That Decide Client Behavior
This is the chapter interviewers live in, because the three codes look similar and trigger three different client behaviors — and a client that gets the code wrong does the wrong thing, sometimes catastrophically.
| Code | Literal meaning | Real meaning | Correct client action |
|---|---|---|---|
401 | Unauthorized | un-authenticated — no/invalid/expired token | re-authenticate (get a fresh token) |
403 | Forbidden | authenticated, not authorized — valid token, wrong scope | acquire more scope/role (re-auth won't help) |
429 | Too Many Requests | authenticated + authorized, over budget | back off for Retry-After seconds, then retry |
Why the distinction is load-bearing:
- Return
403when you meant401(e.g. on an expired token) and a smart client won't refresh its token — it'll assume it lacks permission and give up, or escalate to a human. Return401when you meant403and the client will refresh its token and retry the same forbidden call forever — a refresh storm against Entra and a hot loop against you. This exact bug has caused production incidents at large shops. - Return a plain
403/401when you meant429and the client won't back off — it treats throttling as a permanent failure.429with aRetry-Afteris a cooperative signal: "you're fine, just slow down for N seconds." A well-behaved client honors it and the system self-stabilizes; a429withoutRetry-Afterleaves the client guessing (usually retrying too fast and making it worse).
Mnemonic. 401 = "I don't know you." 403 = "I know you, the answer's no." 429 =
"I know you, you're allowed, but not this fast." Three different fixes; the gateway's job is
to return the one that triggers the right fix.
Chapter 9: Token-Bucket Rate Limiting — The Math
From zero — why limit at all. A backend has finite capacity. Without a limiter, one
buggy client (or one noisy tenant, or one attacker) can consume it all and starve everyone
else — the noisy-neighbor problem. A rate limiter caps how fast each caller may go, so a
flood from one client gets 429'd while everyone else sails through. It is simultaneously a
fairness mechanism and a defense mechanism.
The token bucket — the canonical algorithm. Picture a bucket that holds up to
\( C \) tokens (the capacity, i.e. the burst you tolerate) and is refilled at a steady
\( r \) tokens per second (the sustained rate you guarantee). Each request must take one
token; if the bucket is empty, the request is denied (429).
The bucket is described by two numbers and updated lazily — there's no background timer; you compute the refill on each request from the elapsed time. Let \( t_{\text{last}} \) be the last time we touched the bucket and \( n \) the token count then. At a new time \( t \):
$$ n' \;=\; \min\!\bigl(C,\; n + (t - t_{\text{last}})\cdot r\bigr) $$
The \( \min \) with \( C \) is the cap: you cannot bank unused rate beyond the burst size — idle for an hour and you still only get \( C \) tokens, not 3600r. Then:
$$ \text{allow} \;=\; \begin{cases} \text{True}, & n' \ge 1 \quad(\text{spend one: } n'' = n' - 1) \\[4pt] \text{False}, & n' < 1 \end{cases} $$
And when denied, the client deserves to know exactly how long until it can try again — the time for one full token to accrue:
$$ \text{Retry-After} \;=\; \frac{1 - n'}{r} $$
Worked example (the lab's numbers). Capacity \( C = 2 \), refill \( r = 1 \)/s, bucket starts full:
t=0.0 n=2.0 → allow, n=1.0
t=0.0 n=1.0 → allow, n=0.0
t=0.0 n=0.0 → DENY. Retry-After = (1 − 0)/1 = 1.0 s → 429, Retry-After: 1
t=1.0 n=min(2, 0 + 1·1)=1.0 → allow, n=0.0
And a partial-token case: \( C=1,\ r=2 \)/s, spend the token at \( t=0 \), ask again at
\( t=0.25 \): \( n' = \min(1, 0 + 0.25\cdot 2) = 0.5 \), denied, \( \text{Retry-After}
= (1 - 0.5)/2 = 0.25 \) s. (This is test_bucket_retry_after_partial_token.)
Rate limit vs quota. A rate limit (the bucket: calls per second) smooths bursts and
protects the backend right now. A quota (calls per day/month) caps the long-window
total for fairness and billing ("your tier gets 1M calls/month"). Both return 429 when
exceeded, but they answer different questions — a client can be under its monthly quota and
still get 429'd for a one-second burst, and vice-versa. APIM has both: rate-limit-by-key
and quota-by-key.
Distributed reality. APIM runs a fleet of gateway nodes, so the bucket state is shared/ replicated and necessarily approximate — a global "10/s" might briefly allow 12 across nodes. The lab models the exact single-node bucket (which is the algorithm you must know); the distributed version is the same math with eventually-consistent counters.
Chapter 10: Route Matching — Templates, Params, and Specificity
Mechanism. A gateway holds a table of (method, path_template) operations and, for each
request, must find the one that matches. The template /users/{id} is split into segments
("users", "{id}"); the request /users/42 into ("users", "42"). They match iff:
- the methods are equal (
GET /ordersandPOST /ordersare different operations — method is part of the key), and - the segment counts are equal (
/users/42/ordersdoes not match/users/{id}), and - each template segment matches: a literal (
users) must equal the actual segment; a parameter ({id}) matches any single non-empty segment and binds its name (id → "42").
Most-specific-wins. Two templates can both match one path: /users/me and /users/{id}
both match GET /users/me. The gateway must pick the most specific — the one with the
most literal segments — so a concrete operation (/users/me, the current user) shadows
the generic parameterized one (/users/{id}). The tie-break is "literal count, then segment
count," and the lab sorts candidates by exactly that key. Getting this wrong means
/users/me routes to the by-id handler with id="me" — a real, subtle routing bug.
request: GET /users/me
/users/{id} → matches, specificity (literals=1, segs=2)
/users/me → matches, specificity (literals=2, segs=2) ← wins (more literals)
No candidate matches → None → the gateway returns 404.
Chapter 11: The Pipeline Order Is the Policy
The lab's handle runs four stages, and the order is not arbitrary — it is the security
and fairness policy:
1. match_route no route? → 404 (short-circuit)
2. authorize no/invalid token? → 401
valid, wrong scope? → 403
3. rate-limit bucket empty? → 429 + Retry-After
4. backend all clear → 200
Why 404 before 401. If no route matches, there is no operation to authorize against
— required_scopes is undefined. So routing must come first; a request to a nonexistent path
is 404, full stop, with or without a token. (There's a subtle information-disclosure debate
— some APIs return 404 for unauthorized access to existing resources to avoid confirming
they exist — but the structural reason is simpler: you can't authorize what you can't
match.)
Why 401/403 before 429. Authorization runs before rate limiting for two reasons.
First, don't let unauthorized probing cost rate budget: if rate-limit came first, an
attacker with no token could drain a client's bucket (or, if the bucket is shared, deny
service) purely by spamming requests that were going to be 401'd anyway. Second, cost
attribution: a 401 tells the attacker "you need a token," a 429 tells them "you're going
too fast" — you want the honest signal (401) for an unauthenticated caller, and you only
spend the (more expensive to compute, and budget-consuming) rate check on callers who've
proven they're real. The lab pins this with test_pipeline_401_before_429.
Why the backend is last. The backend is the expensive, stateful thing you're protecting.
Every cheap rejection (404, 401, 403, 429) that happens before the backend call is a
backend request you didn't have to make — the gateway is a shield, and the order is the
shield's logic.
Lab Walkthrough Guidance
Lab 01 — API Gateway, suggested order (matches the file top-to-bottom):
Route+match_route(Ch. 10). Validate the template; split into segments; match method + arity + each segment; extract params; among matches return the most-specific (most literal segments).Noneon no match. Test param extraction, most-specific-wins, method dispatch, and404.authorize(Ch. 7–8). Fail closed: not-a-dict / not-valid/ noscp→401;scpnot a superset ofrequired_scopes→403; else200. Test all three, including the superset-is-fine case and the "needs two scopes, has one"403.TokenBucket.allow(Ch. 9). Lazy refillmin(capacity, tokens + elapsed·refill); reject a backwardnow; consume one if≥ 1; elseretry_after = (1 − tokens)/refill. Test allow-to-capacity-then-429, the partial-tokenretry_after, refill after elapsed time, and the cap.RateLimiter— one bucket per client, lazily created. Test per-client isolation (Alice draining doesn't affect Bob).handle(Ch. 11). The four-stage pipeline; pin the order with the404-before-401and401-before-429tests; emitRetry-After(whole seconds, rounded up, min 1) on429.
Success Criteria
You are ready for the next phase when you can, from memory:
- Explain REST vs RPC and why uniform methods give every layer free, doc-less semantics.
- List which HTTP methods are safe and idempotent, and explain why idempotency decides whether a client may retry on a timeout.
- Name the status-code families and the
4xx/5xxretry rule. - State the
401/403/429distinction and the client action each should trigger, and give the production failure mode of getting401-vs-403backwards. - Draw the APIM policy pipeline (
inbound → backend → outbound → on-error) and placevalidate-jwt,rate-limit-by-key, and routing in the right section. - Separate authentication (
validate-jwt: signature/iss/aud/exp) from authorization (scp/roles), and explainscp-vs-roles(delegated vs app, by grant). - Implement a token bucket and compute its
Retry-After, and distinguish rate-limit from quota. - Justify the gateway pipeline order (
404before401,401before429).
Interview Q&A
Q: A client sends a write, the server processes it, but the response times out. What
should the client do?
It depends entirely on idempotency. If the write was idempotent — a PUT to a known id, a
DELETE, or a POST carrying an idempotency key the server dedups on — the client safely
retries: worst case it re-applies the same state. If it was a plain non-idempotent POST
(POST /charge), the client must not blindly retry — it might create a second resource or
double-charge. This is the reason idempotency matters: the network can't tell the client
whether the request succeeded, and idempotency is what makes that ambiguity survivable. As a
principal I design writes to be idempotent precisely so retries are safe by default.
Q: Walk me through what an API gateway does to a request, in order, and what each stage
returns on failure.
Four stages. Route match — find the operation by method + path template; no match is a
404, and it runs first because there's no operation to authorize against otherwise.
Authorize — validate-jwt (signature, iss, aud, exp) authenticates: failure is
401; then the scope check (scp ⊇ required) authorizes: failure is 403. Rate limit —
a token bucket per caller; empty bucket is 429 with a Retry-After. Backend — only a
fully-authorized, in-budget request reaches the origin, returning 200. The order is the
policy: 404 before auth, auth before rate-limit so probing can't cost rate budget, backend
last because it's the expensive thing the gateway shields.
Q: 401 vs 403 — when does each fire, and why does it matter operationally?
401 means un-authenticated — no token, or an invalid/expired one; the literal name
"Unauthorized" is a historical misnomer. The client should re-authenticate. 403 means
authenticated but not authorized — a perfectly valid token that simply lacks the required
scope or role; re-authenticating won't help, the client needs more scope. Operationally the
distinction is critical: if you return 401 when you meant 403, a smart client refreshes
its token and retries the same forbidden call forever — a refresh storm against Entra and
a hot loop against you. If you return 403 when you meant 401 (e.g. on an expired token),
the client won't refresh and gives up on a call that a fresh token would have allowed. I've
seen both cause incidents.
Q: Implement a token bucket. What should Retry-After say when you reject?
A bucket holds up to capacity tokens (the burst), refills continuously at refill_per_sec
(the sustained rate), and each request spends one. I refill lazily on each call: tokens = min(capacity, tokens + elapsed·refill). If tokens ≥ 1, consume one and allow; else deny.
On denial, Retry-After should be exactly the time for one token to accrue:
(1 − tokens) / refill_per_sec, rounded up to whole seconds with a floor of 1. That precise
value lets a well-behaved client back off exactly long enough and no longer — it
self-stabilizes the system. The min(capacity, …) cap is the subtle part: you can't bank
idle time into unlimited burst.
Q: Rate limit vs quota — what's the difference, and why have both?
A rate limit caps short-term speed (calls per second) — it smooths bursts and protects the
backend's instantaneous capacity; it's the token bucket. A quota caps the long-window total
(calls per day/month) — it's about fairness and billing, e.g. "your tier gets 1M
calls/month." Both return 429, but they answer different questions: a client can be well
under its monthly quota and still get rate-limited for a one-second spike, and can be slow
enough to never hit the rate limit yet exhaust its monthly quota by mid-month. APIM exposes
rate-limit-by-key and quota-by-key for exactly this — you usually want both.
Q: Where does authentication end and authorization begin at the gateway, and why is the
order fixed?
Authentication is validate-jwt: prove the token is real (signature), from your tenant
(iss), for this API (aud — skip it and you're a confused deputy accepting tokens minted
for another service), and current (exp/nbf). That answers who. Only after it passes do
I read the scp/roles claims to authorize what — does this caller hold the scopes this
operation needs. The order is fixed because you must never read a claim from a token you
haven't verified — until the signature checks out, the whole payload is attacker-controlled
JSON. AuthN failure is 401, AuthZ failure is 403, and both fail closed.
References
- RFC 9110 — HTTP Semantics (2022) — the authoritative spec for methods, idempotency
(§9.2.2), safe methods (§9.2.1), and status codes (§15);
401§15.5.2,403§15.5.4,404§15.5.5,429(RFC 6585 §4),Retry-After§10.2.3. - RFC 6585 — Additional HTTP Status Codes (2012) —
429 Too Many Requestsand its intended use withRetry-After. - RFC 6749 — The OAuth 2.0 Authorization Framework (2012) — §3.3 scope (the space-
delimited
scpconvention), the grant types, and §5.2 theinvalid_token/insufficient_scopeerrors behind401/403. - RFC 6750 — OAuth 2.0 Bearer Token Usage —
WWW-Authenticateand theerror="invalid_token"/error="insufficient_scope"semantics the gateway emits. - Microsoft Learn — Azure API Management policies — the
validate-jwt,rate-limit-by- key,quota-by-key, and policy-pipeline (inbound/backend/outbound/on-error) reference. - Microsoft Learn — Protect an API in APIM using OAuth 2.0 and Microsoft Entra ID — the
end-to-end
validate-jwt+ scope-check configuration this lab models. - Microsoft identity platform —
scp(delegated) vsroles(app) claims — which grant produces which claim, and how an API authorizes each. - Roy Fielding, Architectural Styles and the Design of Network-based Software Architectures (2000), Ch. 5 — the original definition of REST and its constraints.
- The track's own CHEATSHEET.md and GLOSSARY.md.