Lesson 8 · Auth · visual edition

Authentication & authorization

~7 min · one diagram per idea · retrieval practice at the end

Published

API authenticationAuthorizationStatus codesHTTP methodsREST

Why this, first. Security is non-negotiable at the senior bar — a fuzzy answer here ends interviews. Interviewers probe three things hard: AuthN-vs-AuthZ precision, the OAuth2/JWT trade-offs you’d actually deploy, and object-level authorization (BOLA) — the #1 API vulnerability on the OWASP list. 1 Get these crisp and you sound like someone who has shipped and been breached. Stay vague and you sound like someone who has only read about it.

First prove who you are

then what you may do

on this exact object

over TLS, every node

The distinction everything hangs on

The fastest way to lose the room is to slur the two together. They are different questions, fail at different layers, and map to different status codes:

TWO GATES, IN ORDER · DIFFERENT FAILURES ① AUTHENTICATION who are you? prove identity from a credential missing / invalid → 401 Unauthorized pass ② AUTHORIZATION what may you do? may this principal do this, here? known but not allowed → 403 Forbidden A request can pass gate ① and fail gate ② on the very next line — the priciest bugs live in that gap.
401 = come back with valid identity; 403 = your identity is fine, the answer is still no. (401 is a misnomer; it means unauthenticated.)

Credential schemes and where each fits

FROM SHARED SECRET TO DELEGATED IDENTITY API key shared secret in a header ✓ simple svc-to-svc ✗ coarse, hard to rotate ✗ leaks easily never in browser / URL Bearer token Authorization: Bearer <token> "bearer is granted" the token IS the credential → TLS + short life = all OAuth 2.0 delegated AUTHZ user grants an app scoped access — no password shared NOT authentication on its own OIDC identity layer ON TOP of OAuth2 adds id_token (a JWT about you) → real AUTHENTICATION
Rule of thumb: OAuth2 = authorization; OIDC = authentication. Treating an OAuth access token as proof of identity is a classic senior gotcha.23
✗ Myth: “OAuth2 logs the user in, so the access token proves who they are.”

✓ Truth: OAuth2 delegates authorization; you need OIDC’s id_token to authenticate.

The OAuth2 authorization-code flow (+ PKCE)

Name the right grant for the client and you’ve signalled real-world use, not a tutorial. For users via web, mobile, or SPA, it’s Authorization Code + PKCE:

AUTHORIZATION CODE + PKCE User resource owner Client SPA / mobile app Authorization Server login + consent · mints tokens Resource Server the API · validates token ① authorize · sends code_challenge (PKCE) ② redirect back with auth code ③ exchange code + code_verifier → ④ access token (+ refresh) ⑤ call API · Authorization: Bearer <token>
PKCE binds the auth code to the requester, so an intercepted code is useless — it protects public clients that can't keep a secret. Authorization Code + PKCE supersedes the deprecated Implicit grant even for SPAs.

Auth Code + PKCE For users via web / mobile / SPA. PKCE protects public clients by binding the code to whoever requested it.

Client Credentials Machine-to-machine, no user. The service authenticates as itself — partner backends, internal services.

Scopes & audience Tokens carry scopes (e.g. orders:read ) and an aud. The resource server must check the token was issued for it.

Legacy — avoid Implicit and Resource-Owner-Password grants are deprecated; naming them as your default is a red flag.

JWT structure — and the trade-off vs. sessions

A JWT is three base64url parts joined by dots, signed so any node can verify it statelessly — which fits REST perfectly:

header . payload . signature HEADER alg (e.g. RS256) typ: JWT pin alg · reject alg:none . PAYLOAD (claims) sub · iss · aud · exp scopes / roles base64, NOT encrypted — no secrets . SIGNATURE sign(header.payload, key) → integrity proves it wasn't forged VERIFY: recompute sig over header+payload with the key — match? then check iss, aud, exp. No match → reject. Catch: hard to revoke before exp — a JWT you don't fully validate is just attacker-supplied JSON.
header.payload.signature, signed and verified statelessly by any node. The payload is base64, not encrypted, so never put secrets in it; and it is hard to revoke before exp.

JWT — self-contained Verified statelessly on any node, fits REST. Catch: payload is readable, and revocation before exp is hard.

Server-side session Opaque id pointing at server state. Trivial to revoke (delete the row), but stateful — every node needs a shared store.

The revocation answer that scores Short-lived access tokens + refresh tokens with rotation, backed by a denylist / introspection for emergency revoke.

Validate everything Check signature; pin alg, reject alg:none and alg-confusion; verify iss, aud, exp — on every node.

Validate every token, every request, on every node. Short expiry shrinks the blast radius; the denylist handles the “log this device out now” case. A JWT you don’t fully validate is just an attacker-supplied JSON blob.

The model that actually gets you breached

RBAC grants by role; ABAC decides from attributes (department, region, resource tags) for finer policy. Both answer “can this kind of principal do this kind of thing” — neither, alone, checks you own this object:

OWASP API #1 · BROKEN OBJECT LEVEL AUTHORIZATION ✓ authenticated ✓ role-permitted RBAC/ABAC passed GET /accounts/{id} attacker swaps id → 1042 → 1043 no ownership check someone else's data 💥 cross-tenant leak FIX: on every object access, verify the principal owns THAT exact object (tenant / owner check) — non-optional Unguessable ids are not authorization — they're obscurity.
Two siblings round out the top: #2 Broken Authentication (weak validation, credential stuffing, no login rate-limit) and #3 BOPLA (reading/writing fields the caller shouldn't, e.g. mass-assigning isAdmin).1

🔐 Memory rule: AuthN (who? → 401) before AuthZ (may you? → 403). OAuth2 = authorization, OIDC = authentication. Validate the JWT fully on every node; check object ownership on every access — that’s what stops BOLA.

Transport & hygiene (forgotten under pressure)

TLS always Bearer tokens over plaintext is game over — no exceptions, including internal hops.

Tokens never in URLs They leak into access logs, history, and Referer. Authorization header only.

Cookies hardened HttpOnly + Secure + SameSite keep tokens out of JS (XSS) and off cross-site requests (CSRF).

mTLS + rotate Mutual TLS authenticates both ends; rotation limits damage when a key inevitably leaks.

Retrieval practice

Close the lesson in your mind first, then answer from memory. Immediate feedback below.

Q1. An authenticated user requests another tenant's record and is refused. Correct status?

Q2. Which statement about OAuth 2.0 and OIDC is correct?

Q3. A JWT's chief weakness versus a server-side session is that it…

Q4. The right defense against BOLA on GET /accounts/{id} is to…

Rehearse the answer out loud

Interviewer: "Design authentication and authorization for a multi-tenant SaaS API used by both end users and partner backend services." ~60 seconds, from memory (no scrolling up) — then reveal and compare.


“I’d standardize on OAuth2. For end users — web, mobile, SPA — Authorization Code + PKCE, with OIDC on top for their identity. For partner backends, Client Credentials, no user in the loop. The authorization server issues short-lived signed JWT access tokens plus rotating refresh tokens.

Every request, on any node, I

validate the signature, algorithm, iss,aud, and exp

— fully stateless, so it scales horizontally. Scopes give coarse authZ, but the critical part is object-level checks: the principal’s tenant must own the requested object, which is how I shut down BOLA. For revocation I rely on short expiry plus a denylist / introspection. And the hygiene: TLS everywhere, tokens only in the Authorization header, secrets rotated, mTLS between services.”

Why this scores: it cleanly separates authN from authZ, picks the correct grant per client, handles JWT revocation honestly instead of pretending it’s free, and explicitly defends against BOLA — the one failure that actually breaches multi-tenant APIs.

I’m your teacher — ask me anything. Want to walk through a PKCE exchange step by step? Curious how refresh-token rotation detects replay? Want me to grade your rehearsal answer, or fire harder follow-ups — token introspection vs. denylist, mTLS vs. signed JWTs between services? Just say so in the chat.

Primary source

📖

OWASP API Security Top 10 (2023)

: the high-trust catalog of how real APIs get broken — start with API1:2023 (BOLA), API2 (Broken Authentication), and API3 (BOPLA). For the protocol details, OAuth 2.0 (RFC 6749) and the OIDC overview.

Interview — pick your bar

Answer out loud in ~60s, then reveal. Core = recall · Senior = trade-offs & failure modes · Staff = synthesis under ambiguity · System Design = open design round (a different axis, not a harder level).

Define authentication vs authorization.

Authentication (AuthN) answers "who are you?" — proving identity from a credential. Authorization (AuthZ) answers "what may you do?" — deciding whether a known principal may perform this action on this resource. AuthN happens first; AuthZ runs on every protected operation after.

Which status code maps to a missing/invalid credential vs an authenticated-but-not-permitted request?

Missing or invalid credential → 401 Unauthorized (a historical misnomer; it really means unauthenticated). Known principal who isn't permitted → 403 Forbidden.

Is OAuth2 an authentication protocol?

No. OAuth 2.0 is a delegated authorization framework: it lets a user grant an app scoped access to their resources without sharing a password. Treating an OAuth access token as proof of identity is a classic gotcha — the access token is for the resource server, not for identifying the user to the app.

What is the #1 risk on the OWASP API Top 10 (2023)?

API1: BOLA — Broken Object Level Authorization. The endpoint confirms you're authenticated (and maybe role-permitted) but never checks you own the specific object, so changing an id returns someone else's data. It's #1 because it's pervasive and high-impact.

What problem does PKCE solve, and how?

It protects public clients (SPAs, mobile apps that can't keep a secret) against authorization-code interception. The client generates a random code_verifier, sends its hash (code_challenge) on the auth request, and must present the original verifier at token exchange. An intercepted code is useless without the verifier, binding the code to the requester.

What is the aud (audience) claim and why must a resource server check it?

aud names the intended recipient(s) of the token. A resource server must verify the token was issued for it — otherwise a token minted for service A could be replayed against service B (the "confused deputy"). Audience-checking is what stops one service's token from being a skeleton key for another.

What is object-level authorization and why is it the one that bites?

It verifies the authenticated principal may act on this specific object, not merely that they're authenticated or role-permitted. It bites because RBAC/ABAC answer "can this kind of user do this kind of thing" but not "does this user own this record." Skipping it is OWASP API #1 — BOLA — the most common API breach.

What is the chief weakness of a JWT versus a server-side session?

Revocation. A self-contained token is valid until exp, no matter what — there's no row to delete. A server-side session is the opposite: trivial to revoke (delete it), but stateful (needs a shared store or sticky sessions). The design tension is pure statelessness vs instant logout.

Explain the alg: none and algorithm-confusion attacks.

alg: none: attacker sets the header algorithm to none and strips the signature; a naive verifier that trusts the header accepts an unsigned token. Algorithm confusion (RS256→HS256): the server expects an RSA-signed token (verify with the public key) but the attacker sends an HS256 token signed with that public key as the HMAC secret; a library that picks the algorithm from the header validates it. Defense: pin the expected algorithm(s) server-side and never let the token's header choose how it's verified.

mTLS vs signed JWTs for service-to-service auth — how do you choose?

mTLS authenticates the connection at the transport layer — strong, sender-bound, great inside a service mesh, but it tells you which service connected, not on whose behalf. Signed JWTs carry application-level claims (caller, scopes, end-user context, audience) and survive proxy hops, but are bearer by default (replayable if leaked). Mature systems combine them: mTLS for channel identity + a JWT for fine-grained, propagated authZ context.

How do you keep authorization consistent across dozens of microservices?

Externalize policy: a centralized decision model (e.g. a policy engine / OPA-style PDP, or a relationship-based system à la Google Zanzibar for object-level checks) with services as enforcement points. Propagate identity and scopes via signed tokens, version policies, and test them. The anti-pattern is ad-hoc if checks copy-pasted per service — they drift, and one missed check is a BOLA.

Design-round framework — drive any API auth prompt through these, out loud:
  1. Clarify actors & trust boundaries: end users (web/mobile/SPA) vs machine clients (partner backends, services), single- vs multi-tenant, regulatory constraints.
  2. Authentication: pick grants per actor — Authorization Code + PKCE (+ OIDC for identity) for users, Client Credentials or mTLS for machines.
  3. Token strategy: short-lived signed JWT access tokens + rotating refresh tokens; choose HS256 (one domain) vs RS256/ES256 + JWKS (multi-service); pin alg, check iss/aud/exp.
  4. Authorization layers: coarse scopes + role/attribute checks, then the one that bites — per-object owner/tenant checks to shut down BOLA — enforced in a shared data layer, deny-by-default per function.
  5. Revocation & key lifecycle: short expiry + denylist/introspection for emergency logout; JWKS kid rotation for signing keys.
  6. Transport & hygiene: TLS everywhere (incl. internal hops), tokens only in the Authorization header, secrets rotated, sensitive fields never in a JWT or logs.
  7. Threat model & observability: walk the OWASP API Top 10 (BOLA/BOPLA/BFLA, broken auth, SSRF); audit-log auth decisions; test cross-tenant and privilege-escalation paths.
Design end-to-end authentication and authorization for a multi-tenant SaaS API serving both end users and partner backends.

A strong answer covers: separate the actors first — interactive users vs machine-to-machine partners have different grants → standardize on OAuth2: end users via Authorization Code + PKCE with OIDC for identity (an id_token with sub/email), partner backends via Client Credentials or mTLS with no user in the loop → issue short-lived signed JWT access tokens + rotating refresh tokens; sign with RS256/ES256 and publish a JWKS so every resource server validates locally without holding signing power — preserving REST statelessness → on every request, validate signature, pin the algorithm (reject alg: none/confusion), and check iss, aud (audience is you, to stop confused-deputy replay), and exp/nbf → authorization in layers: scopes for coarse "what the token may do," role/attribute checks, and critically the per-object tenant/owner check derived from the token (WHERE id=:id AND tenant_id=:tokenTenant) enforced in a shared data layer so no endpoint can forget — this is what shuts down BOLA, the #1 OWASP API risk → revocation: short expiry for the common case + a denylist/introspection for emergency logout, family revocation on refresh-token reuse, JWKS kid rotation if a key leaks → hygiene: TLS everywhere including internal hops, tokens only in headers, secrets rotated, no PII in the JWT → name the trade-off: stateless JWTs scale out but make instant revocation hard, which the short-expiry + denylist combo resolves at the cost of a little state.

Design the authorization model and OWASP-API-Top-10 defenses for a public REST API exposing per-user resources by id.

A strong answer covers: start from the threat model — APIs expose object ids directly and are hit programmatically, so authorization (not injection) is the dominant breach class → authenticate with bearer tokens over TLS, but treat authentication as table stakes; the work is authorization → defend API1 BOLA with a per-object owner/tenant check on every access, derived from the token, in a shared data-access layer; make clear that unguessable UUIDs are obscurity, not a control → defend API3 BOPLA with explicit allow-lists for readable and writable fields — never bind request bodies straight onto the model (mass assignment like "isAdmin":true) and never over-return (passwordHash, others' PII) → defend API5 BFLA by denying by default and enforcing role/permission per function including non-GET methods, so a user can't reach admin routes by guessing a path or swapping a verb → defend API4 Unrestricted Resource Consumption with rate limiting (429 + Retry-After), pagination caps, payload-size/timeout/query-complexity limits, and per-principal quotas (also stops financial DoS) → defend API2 Broken Authentication with strict token validation (pin alg, check iss/aud/exp), login rate limiting + MFA, short token lifetimes → if the API fetches user-supplied URLs, defend API7 SSRF by allow-listing destinations, blocking private/link-local ranges, and resolve-then-validate against DNS rebinding → cross-cutting: return safe errors (no stack traces / SQL), audit-log authorization decisions, and add automated cross-tenant / privilege-escalation tests → name the trade-off: centralizing policy (a PDP / Zanzibar-style relationship model) keeps checks consistent across services but adds a dependency vs fast-but-drift-prone per-service if checks.

Was this lesson helpful? Thanks — noted.

These notes reflect my current understanding and are updated as I learn, build, and discover better explanations.