Skip to content

OAuth & JWT

So far identity has lived inside one system. The moment a third party is involved — “let this app read my calendar,” “log in with Google” — you face a new problem: how do you grant someone else limited access to your account without handing them your password? OAuth answers the delegation question; JWT answers the “how do we carry proof around” question. They’re often used together but they solve different problems, and conflating them causes endless confusion.

OAuth2 is a framework for delegated authorization. Its founding insight is that giving an app your password is catastrophic — the app could do anything you can, forever, and you couldn’t revoke it without changing your password everywhere. OAuth replaces that with a scoped, revocable access token issued by the service that holds your account.

The four roles:

RESOURCE OWNER → you (the human)
CLIENT → the app wanting access (e.g. a photo printer)
AUTHORIZATION SERVER → the one that authenticates you & issues tokens (e.g. Google)
RESOURCE SERVER → the API holding your data (e.g. Google Photos)

The canonical Authorization Code flow:

1. Client sends you to the Auth Server: "this app wants read access to your photos"
2. You authenticate THERE (the client never sees your password) and consent
3. Auth Server redirects back to the client with a short-lived AUTHORIZATION CODE
4. Client exchanges that code (plus its own secret) for an ACCESS TOKEN — server-to-server
5. Client calls the Resource Server with the access token; it returns only the scoped data

The genius is the scope (“read photos” only) and the separation: your password stays with the party you already trust, and the client gets a narrow, expirable key instead. That’s least privilege applied to delegation.

JWT: a token you can verify without asking

Section titled “JWT: a token you can verify without asking”

A JSON Web Token is a compact, signed, self-contained claim. “Self-contained” is the key word: the receiver can verify it using only a key, without calling back to whoever issued it. It has three base64url parts joined by dots:

header . payload . signature
─────── ─────── ─────────
{alg,typ} {claims} sign(header.payload, key)
header : which signing algorithm
payload: the claims — sub (subject), exp (expiry), iss (issuer), scopes, roles…
signature: proves the first two parts weren't altered AND came from the issuer

The signature is everything. Anyone can read a JWT (it is not encrypted — only signed), but only the holder of the key can produce a valid one. Change one byte of the payload and the signature no longer matches, so a tampered token is rejected. This is what lets any server verify a token with just the public key — no shared session store, no database lookup.

Tokens force a brutal trade-off. A short lifetime limits the damage of a stolen token; a long lifetime spares users from re-logging-in constantly. You can’t have both with one token — so you use two:

ACCESS TOKEN → short-lived (minutes), sent on every API call.
If stolen, it expires fast. Self-verifiable (often a JWT).
REFRESH TOKEN → long-lived (days/weeks), sent ONLY to the auth server
to mint fresh access tokens. Kept secret; never hits APIs.

The access token is the disposable day-pass; the refresh token is the harder-to-steal master key you present only at one well-guarded door. This split is how systems get both convenience and a small blast radius.

The statelessness benefit and the revocation pitfall

Section titled “The statelessness benefit and the revocation pitfall”

Here is the whole trade, stated plainly — and it directly continues the sessions-vs-tokens story from Authentication and the scaling story in Statelessness & Sessions.

What statelessness buys us: any server can verify a JWT with just a key — no shared session store, no per-request database lookup. This is a huge win for horizontal scaling and for multi-service architectures, where a token minted by the auth service is trusted by every downstream service independently.

What it costs us: you can’t easily un-issue a self-contained token. A session can be revoked by deleting one row; a valid JWT is valid until it expires, even if you fire the employee or detect the theft thirty seconds after issuing it. The mitigations all claw back some statelessness:

short expiry → limits the window, but never to zero
token blocklist → check a denylist of revoked IDs (a lookup — partly stateful again)
token versioning → bump a per-user counter; tokens with old versions fail
rotate signing key → nuclear option: invalidates EVERY token at once

There is no perfectly clean answer. You are trading instant revocation for stateless scale, and the right balance — short access tokens plus a refresh mechanism, occasionally backed by a denylist for high-stakes actions — is exactly the kind of deliberate trade this whole part is about.

Under the hood — how a server finds the right key (kid + JWKS)

Section titled “Under the hood — how a server finds the right key (kid + JWKS)”

“Verify the signature with the issuer’s key” raises a practical question: which key? Issuers rotate signing keys and often run several at once, so a JWT’s header carries a kid (key ID) naming the key that signed it. The verifier fetches the issuer’s public keys from a well-known JWKS (JSON Web Key Set) endpoint — a URL publishing the current public keys as JSON — caches them, and picks the one whose kid matches.

This is what makes stateless verification and key rotation coexist. The auth server can introduce a new key, start signing with it, and keep publishing the old public key until the last token signed with it expires — no downtime, no shared session store. It also sharpens the warning above: pin the expected algorithm server-side and only trust keys from the issuer’s real JWKS. Letting the token choose its own algorithm or its own key source is how the alg: none and key-confusion attacks get their foot in the door.

OAuth lets a user delegate scoped access without surrendering their password; OIDC adds a proper login on top; JWTs are the signed, self-verifying envelope that carries identity and permissions across trust boundaries without a central lookup. Together they let independent systems trust each other’s claims — buying enormous scale at the price of harder revocation. Whatever crosses these boundaries must also be protected on the wire and at rest, which is exactly Encryption in Transit & at Rest →.

OAuth and JWT carry identity across systems — adopt them through the five questions:

  • Why do they exist? Because giving a third-party app your password is catastrophic — it could do anything you can, forever, unrevocable without changing it everywhere. OAuth replaces that with a scoped, expirable token; JWT is the self-verifying envelope that carries the proof.
  • What problem do they solve? OAuth2 = delegated authorization (a “read photos only” token, password stays with the party you trust); OIDC adds login on top; a JWT lets any server verify identity with just a key — no central session store, no per-request lookup — which is the win for horizontal scale and multi-service trust.
  • What are the trade-offs? The whole bargain is stateless scale for harder revocation: a self-contained JWT is valid until it expires even after you fire the employee. The mitigations (short expiry, denylist, token versioning, key rotation) all claw back some statelessness. Hence access/refresh token split: a disposable minutes-long day-pass plus a well-guarded master key.
  • When should I avoid it? Use OAuth2 to access an API, OIDC to log a user in — bolting login onto bare OAuth2 is a classic subtle bug. Avoid long-lived JWTs for high-stakes actions where instant revocation matters; reach for a session there instead.
  • What breaks if I remove it (or implement it naively)? Without delegation you’re back to password-sharing; without JWT every service needs a shared session lookup. And the signature is everything: honor alg: none or let the token pick its own verification and an attacker forges a valid token (the 2015 JWT library breaks) — never let the thing you verify tell you how to verify it.
  1. What problem does OAuth2 solve that “just give the app your password” creates?
  2. Distinguish OAuth2 from OIDC — which one is for logging a user in?
  3. A JWT is signed but not encrypted. What can an attacker do with it, and what can’t they?
  4. Why split authority into a short access token and a long refresh token instead of one token?
  5. State the statelessness benefit of JWTs and the revocation pitfall, plus one way to mitigate it.
Show answers
  1. Giving an app your password is catastrophic — it could do anything you can, forever, and you couldn’t revoke it without changing your password everywhere. OAuth2 solves this with delegated authorization: a scoped, revocable, expirable access token issued by the service that holds your account, so your password stays with the party you already trust and the app gets a narrow key instead.
  2. OAuth2 grants access to an API (delegated authorization); it was never designed to prove who you are. OIDC (OpenID Connect) is a thin layer on top that adds an ID token (a JWT describing the authenticated user) for logging a user in. “Sign in with Google” is OIDC.
  3. A JWT is signed, not encrypted, so an attacker can read it (the payload is just base64url — never put secrets in it) but cannot forge a valid one without the signing key. Change one byte and the signature no longer matches, so a tampered token is rejected — which is what lets any server verify it with just a key, no lookup.
  4. One token can’t be both safe-if-stolen and convenient. A short access token (minutes, sent on every API call) limits the damage of theft because it expires fast; a long refresh token (days/weeks, sent only to the auth server to mint new access tokens) spares users from constant re-login. The split gives both convenience and a small blast radius — a disposable day-pass plus a well-guarded master key.
  5. Benefit: any server can verify a JWT with just a key — no shared session store, no per-request lookup — a huge win for horizontal scale and multi-service architectures. Pitfall: you can’t easily un-issue a self-contained token; it stays valid until expiry even after you fire the employee or detect the theft. Mitigations include short expiry, a token blocklist/denylist of revoked IDs, token versioning, or rotating the signing key (the nuclear option) — each clawing back some statelessness.