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/429 trio everyone gets backwards), pagination and content negotiation, Azure API Management and its policy pipeline, authentication-vs-authorization at the edge (validate-jwt then 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

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, OPTIONS are safe: a crawler can fetch them freely. (A GET that deletes something is a famous bug class — search engines have wiped admin panels by following "delete" links that were GETs.)
  • Idempotent — performing it N times leaves the resource in the same state as performing it once. This is the property that decides retryability.
MethodSafeIdempotentMeaningMay a client retry on timeout?
GETread a resourceyes
HEADread headers onlyyes
PUTreplace a resource wholesaleyes — second PUT = same state
DELETEremove a resourceyes — already-gone stays gone
POSTcreate / process (non-idempotent)no — may create a duplicate
PATCH❌ (not guaranteed)partial updateonly 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:

CodeNameWhen
400Bad Requestmalformed syntax, unparseable JSON, missing required field
401Unauthorizedun-authenticated — no/invalid/expired token (see Ch. 8)
403Forbiddenauthenticated but not authorized — wrong scope/role
404Not Foundno such resource / route
405Method Not Allowedroute exists, but not for this method
406 / 415Not Acceptable / Unsupported Media Typecontent-negotiation mismatch
409Conflictthe write conflicts with current state (e.g. optimistic-concurrency)
422Unprocessable Entitysyntactically valid but semantically wrong
429Too Many Requestsrate/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 / limitGET /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 discards offset rows).
  • Cursor / keysetGET /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 a next cursor (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:

  1. Signature — recompute/verify with the issuer's public key (selected by the token's kid from the issuer's JWKS). Until this passes, the payload is attacker-controlled JSON — read nothing from it.
  2. iss — the token was issued by your Entra tenant, not some other issuer.
  3. 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.
  4. 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.
  • rolesapp 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 scp401; 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.

CodeLiteral meaningReal meaningCorrect client action
401Unauthorizedun-authenticated — no/invalid/expired tokenre-authenticate (get a fresh token)
403Forbiddenauthenticated, not authorized — valid token, wrong scopeacquire more scope/role (re-auth won't help)
429Too Many Requestsauthenticated + authorized, over budgetback off for Retry-After seconds, then retry

Why the distinction is load-bearing:

  • Return 403 when you meant 401 (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. Return 401 when you meant 403 and 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/401 when you meant 429 and the client won't back off — it treats throttling as a permanent failure. 429 with a Retry-After is a cooperative signal: "you're fine, just slow down for N seconds." A well-behaved client honors it and the system self-stabilizes; a 429 without Retry-After leaves 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:

  1. the methods are equal (GET /orders and POST /orders are different operations — method is part of the key), and
  2. the segment counts are equal (/users/42/orders does not match /users/{id}), and
  3. 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 againstrequired_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):

  1. 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). None on no match. Test param extraction, most-specific-wins, method dispatch, and 404.
  2. authorize (Ch. 7–8). Fail closed: not-a-dict / not-valid / no scp401; scp not a superset of required_scopes403; else 200. Test all three, including the superset-is-fine case and the "needs two scopes, has one" 403.
  3. TokenBucket.allow (Ch. 9). Lazy refill min(capacity, tokens + elapsed·refill); reject a backward now; consume one if ≥ 1; else retry_after = (1 − tokens)/refill. Test allow-to-capacity-then-429, the partial-token retry_after, refill after elapsed time, and the cap.
  4. RateLimiter — one bucket per client, lazily created. Test per-client isolation (Alice draining doesn't affect Bob).
  5. handle (Ch. 11). The four-stage pipeline; pin the order with the 404-before-401 and 401-before-429 tests; emit Retry-After (whole seconds, rounded up, min 1) on 429.

Success Criteria

You are ready for the next phase when you can, from memory:

  1. Explain REST vs RPC and why uniform methods give every layer free, doc-less semantics.
  2. List which HTTP methods are safe and idempotent, and explain why idempotency decides whether a client may retry on a timeout.
  3. Name the status-code families and the 4xx/5xx retry rule.
  4. State the 401/403/429 distinction and the client action each should trigger, and give the production failure mode of getting 401-vs-403 backwards.
  5. Draw the APIM policy pipeline (inbound → backend → outbound → on-error) and place validate-jwt, rate-limit-by-key, and routing in the right section.
  6. Separate authentication (validate-jwt: signature/iss/aud/exp) from authorization (scp/roles), and explain scp-vs-roles (delegated vs app, by grant).
  7. Implement a token bucket and compute its Retry-After, and distinguish rate-limit from quota.
  8. Justify the gateway pipeline order (404 before 401, 401 before 429).

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. Authorizevalidate-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 Requests and its intended use with Retry-After.
  • RFC 6749 — The OAuth 2.0 Authorization Framework (2012) — §3.3 scope (the space- delimited scp convention), the grant types, and §5.2 the invalid_token/ insufficient_scope errors behind 401/403.
  • RFC 6750 — OAuth 2.0 Bearer Token UsageWWW-Authenticate and the error="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) vs roles (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.