• Lesson 8 of 11
  • 6 min read
  • Lesson

API Authentication & Authorization (OAuth2/JWT)

API authentication and authorization: AuthN vs AuthZ, OAuth2 flows, JWT vs opaque tokens, scopes and RBAC, and securing REST endpoints against common attacks.

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 arethen what you may doon this exact objectover 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① AUTHENTICATIONwho are you?prove identity from a credentialmissing / invalid → 401 Unauthorizedpass② AUTHORIZATIONwhat may you do?may this principal do this, here?known but not allowed → 403 ForbiddenA 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 IDENTITYAPI keyshared secret ina header✓ simple svc-to-svc✗ coarse, hard to rotate✗ leaks easilynever in browser / URLBearer tokenAuthorization:Bearer <token>"bearer is granted"the token IS thecredential →TLS + short life = allOAuth 2.0delegated AUTHZuser grants an appscoped access — nopassword sharedNOT authenticationon its ownOIDCidentity layer ONTOP of OAuth2adds 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 + PKCEUserresource ownerClientSPA / mobile appAuthorization Serverlogin + consent · mints tokensResource Serverthe 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 protectspublic 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 . signatureHEADERalg (e.g. RS256)typ: JWTpin alg · reject alg:none.PAYLOAD (claims)sub · iss · aud · expscopes / rolesbase64, NOT encrypted — no secrets.SIGNATUREsign(header.payload,key) → integrityproves it wasn't forgedVERIFY: 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-permittedRBAC/ABAC passedGET /accounts/{id}attacker swaps id → 1042 → 1043no ownership checksomeone else's data 💥cross-tenant leakFIX: on every object access, verify the principal owns THAT exact object(tenant / owner check) — non-optionalUnguessable 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?
  • 401 Unauthorized, since their token has now expired
  • 429 Too Many, since the request rate was exceeded
  • 403 Forbidden, since identity is known but not allowed
  • 404 Not Found, since the record id was invalid here
Q2. Which statement about OAuth 2.0 and OIDC is correct?
  • OAuth2 alone authenticates a user from its access token
  • OIDC adds an identity layer over OAuth2's delegated authz
  • OIDC replaces OAuth2 entirely for machine-to-machine flows
  • OAuth2 encrypts the token payload so secrets ride inside
Q3. A JWT's chief weakness versus a server-side session is that it…
  • cannot ever be validated without a shared central store
  • leaks its full payload because each claim is encrypted
  • forces sticky routing of a user onto one single node
  • is hard to revoke before its own expiry time arrives
Q4. The right defense against BOLA on GET /accounts/{id} is to…
  • verify the caller's tenant actually owns that exact object
  • swap the integer id for a long unguessable random value
  • require a fresh login on each separate account lookup hit
  • cache the account record so repeated reads stay much faster

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.

Reveal model answer

“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.