aditOAuth 2.0 + PKCEstudy guide
A system map: watch a request travel browser → web server → API → issuer.
The big picture, told mostly in diagrams. Flip to Detailed for the wire-level truth.
Full detail: every parameter, header, claim & the real crypto.
system architecture · interactive

Where everything lives, and what travels between

This is the same OAuth + PKCE flow drawn as a real four-tier system. Press Play (or Step) and watch a login travel left to right and back: from your browser, through your app's web server, out to the API, and over to the auth server that issues the tokens. Each hop lights up the wire it uses.

your app traffic · browser ↔ your web server front channel · browser ↔ issuer (exposed) back channel · server ↔ server (private)
Browser
your device ·
untrusted hallway
Web Server
your backend ·
confidential client
API Server
your data ·
resource server
Auth Server
the issuer ·
mints tokens
app Step 1 / 13

Why this hop matters

Browser

Holds no tokens in this (BFF) setup. It only carries redirects and a session cookie. Treat it as a public space anyone might be watching.

Web Server

Your app's confidential client. It keeps the PKCE verifier, does the token exchange, and stores the access & refresh tokens server-side — off the browser.

API Server

Guards your data. Accepts a request only with a valid access token, which it checks against the issuer's public keys (usually cached).

Auth Server

The only party that sees your password. Authenticates you, verifies PKCE, and issues the tokens. Google, GitHub, Okta, your SSO.

Want the message-by-message version, or every parameter? Switch to Simple or Detailed up top — this map is the bird's-eye companion to those.

from first principles → production detail

Understand OAuth 2.0 + PKCE
one layer at a time.

"Log in with Google." A photo app posting to your socials. Your phone staying signed in for weeks. They all run on the same handful of moves — an authorization code, a one-way hash, a short-lived access token and a refresh token. This builds that picture up slowly, with diagrams you can poke at and real cryptography that runs in your browser.

Three ways to lookMap for the system topology, Simple for the diagram story, Detailed for every header.
Step through itTwo interactive players — a system map and a message sequence — walk the whole flow.
Real crypto, liveGenerate a true PKCE challenge, decode a JWT, watch hashing and encryption differ.
How to read this

You're in Simple. Go top to bottom — each chapter assumes only the ones before it. When a section clicks, flip to Detailed to see the wire-level truth, or hit Map for the bird's-eye system view. Nothing here calls a real server.

01 Why OAuth exists

The problem it solves, before any tokens show up.

✕ the dangerous way
you — your password → app
App now has your whole account. Forever.
✓ the OAuth way
you — a limited key → app
App gets a narrow, revocable key. Never your password.
OAuth = delegated authorization: let an app do a few specific things for you, without handing over your password.
The valet-key analogy

A valet key starts the engine and opens the door but not the trunk or glovebox. You hand it to a stranger without worry. OAuth is the valet key for your data: the app gets a credential that does a few specific things (your scopes), not your master key (your password) — and you can deactivate that one key any time.

Two precise distinctions worth nailing down early, because they get muddled constantly:

  • Authentication (who you are) vs authorization (what you may do). Plain OAuth 2.0 is an authorization framework. OpenID Connect (OIDC) is a thin identity layer on top that adds login — it's where the id_token comes from. "Log in with Google" is OIDC; "let this app read my Google Photos" is OAuth.
  • Spec lineage: OAuth 2.0 is RFC 6749 (2012). Modern security rules live in RFC 9700, the Security Best Current Practice (2025), and the in-progress OAuth 2.1 draft, which deletes the old insecure flows and makes PKCE mandatory for everyone.
What OAuth 2.1 changes current best practice

Not a new protocol — OAuth 2.0 with the footguns removed: the Implicit and Password grants are gone, redirect URIs must match by exact string, and PKCE is required on every authorization-code flow. Build new today and you build 2.1.

02 The four players

The colours here match the system map. Keep them in your head.

You + your Browser

Resource Owner

The human who owns the data and clicks "Allow," plus the browser that carries the redirects. You grant access; you never hand the app your password.

The App

Client

The thing that wants access. A confidential client (a web server) can keep a secret; a public client (SPA, mobile) can't. Collects and uses the tokens.

The API

Resource Server

Where your data lives. Doesn't know your password — it just checks that the access token presented is valid, then serves the request.

The Issuer

Authorization Server

Google, GitHub, Okta, your SSO. Authenticates you, asks consent, and issues the tokens. The only party that ever sees your password.

The browser deserves special mention: it's the courier that carries redirects between the App and the Issuer, but it is not trusted — it's a public hallway. That untrusted hallway is exactly why the front-channel / back-channel split (next) matters so much.

03 Front vs back channel

The single most useful mental model for the whole protocol.

Front channel

Data passed through the browser as redirects. Visible in history, logs, extensions. Assume it's public — so it carries only non-secrets and a one-time code.

Back channel

A direct server-to-server HTTPS call that never touches the browser. The safe place — where secrets are exchanged and real tokens are delivered.

Postcard vs sealed envelope. OAuth's golden rule: never put a secret on a postcard.

This is also why the old Implicit flow died. It returned the access token itself on the front channel (in the URL fragment) to save a round-trip — the most valuable secret, on a postcard. Modern guidance (RFC 9700, OAuth 2.1) forbids it: always use the Authorization Code flow, always with PKCE, even for pure browser apps. The clever part of PKCE (chapter 5) is how it keeps a postcard-delivered code safe even when someone photographs the postcard.

04 The flow, step by step

The message sequence. Watch each party's held secrets light up.

Step through the dance. The glowing lanes are the sender and receiver; the badges show which secret each party holds right now.

Where the Map shows where things live, this shows what travels and when. It follows the view toggle: 5 beats in Simple, all 11 steps with real HTTP in Detailed. Use / too.

Track: Simple — the 5-beat story
Step 1 / 5
Youowner
The Appclient
Issuerauth server
The APIresource
local

Why this step matters

05 PKCE up close

"Proof Key for Code Exchange" — what makes a postcard-delivered code safe.

verifier · kept secret
dBjftJeZ4CVP-mB9…
SHA-256
one-way
challenge · sent up front
E9Melhoa2OwvFrEM…
Tear the ticket in half. Keep the secret half; send only a one-way fingerprint of it. Redeeming the code later needs the secret half — which a thief never had.
The attack it kills

The authorization code comes back on the front channel. On a phone, a malicious app can sometimes register the same redirect address and intercept it. Without PKCE the attacker trades the stolen code for your tokens. With PKCE, redeeming the code requires the secret verifier — which never left the real app, and can't be computed backwards from the hash.

Try it for real

This runs the actual PKCE math in your browser. Generate a verifier, watch the real challenge appear, then edit the verifier and watch it change completely.

Live PKCE generator

code_challenge = BASE64URL( SHA-256( code_verifier ) ) · method S256

0 chars
A–Z a–z 0–9 - . _ ~
▼  SHA-256, then Base64URL  ▼
The asymmetry that makes it work

① → ③ is instant. ③ → ① is computationally impossible — that's "one-way." So it's safe to shout the challenge across the public front channel while the verifier stays hidden until the private back-channel token request.

The exact rules RFC 7636

ThingRule
code_verifier43–128 chars from [A-Z] [a-z] [0-9] - . _ ~. ≥256 bits of entropy — i.e. 32 random bytes → Base64URL → 43 chars.
code_challengeBASE64URL(SHA256(ASCII(verifier))). Base64URL = Base64 with +/-_ and no = padding.
code_challenge_methodS256 (required for new clients) or plain (deprecated by RFC 9700). The AS stores method + challenge with the pending code.
VerificationAt the token endpoint the AS recomputes SHA256(verifier) and compares to the stored challenge. Mismatch ⇒ rejected.

PKCE began as a mobile defence, but OAuth 2.1 now mandates it for every client — including server-side ones — as defence-in-depth against authorization-code injection. It costs nothing and closes a whole attack class.

06 Hash vs encode vs encrypt vs sign

Four words people swap by mistake. OAuth uses all four, for different jobs.

Type into the box and watch the same input behave four different ways. That difference is the whole chapter.

You just relied on hashing for PKCE; tokens rely on encoding and signing; transport relies on encryption. Mixing these up is the #1 source of "wait, is this secure?" confusion.

① Encoding — Base64URL reversible · no key · NOT security

A different alphabet for the same bytes, so binary survives a URL. Anyone can reverse it — hides nothing.

↳ This is what a JWT's header & payload are: encoded, not encrypted. Readable by anyone.

② Hashing — SHA-256 one-way · no key · fixed length

A fingerprint. Same input → same hash, always. Change one character → avalanche (try it). No way back.

↳ The engine of PKCE and of password storage. No "decrypt" button — because there's no way back.

③ Signing — HMAC-SHA256 integrity + authenticity · needs key

A hash mixed with a secret key: "someone who holds the key wrote exactly this, unaltered." It vouches for the message; it doesn't hide it.

edit the input, then verify →

↳ This is a JWT's signature. Change one byte of the token and verification fails. Real JWTs usually use RS256/ES256 (asymmetric) rather than shared-secret HMAC.

④ Encryption — AES-GCM reversible WITH the key · hides content

Scrambles the message unreadable — but recoverable by anyone with the key. Unlike hashing there's a way back; unlike encoding you need the secret.

— click Encrypt —

↳ Try decrypting after changing the passphrase — it fails. That's the difference from encoding. This is what TLS/HTTPS does to every message in transit.

The cheat-sheet

TechniqueReversible?Needs a key?JobWhere in OAuth
Encoding
Base64URL
Yes — by anyoneNoURL-safe, readable bytesJWT header/payload; PKCE output format
Hashing
SHA-256
No — one wayNoFingerprint; prove knowledge of a secretPKCE challenge; password/token storage
Signing
HMAC / RS256
re-verifyYesIntegrity + authorshipJWT signature; client assertions
Encryption
AES / TLS
Yes — with the keyYesHide contentHTTPS transport; tokens at rest; JWE
The mistake this prevents

"It's a JWT, so it's encrypted and safe in the URL." No. A standard JWT is signed and Base64URL-encoded, not encrypted — anyone can decode and read every claim (you'll do exactly that next chapter). Never put secrets inside a JWT payload.

07 state and nonce

Two small random values, two different attacks. Separate jobs from PKCE.

state → anti-CSRF

The app sends a random state; the issuer echoes it back with the code; the app checks they match. If not, someone is trying to feed your session a code they obtained. Reject it. Protects the redirect coming back.

nonce → anti-replay

For OIDC login, the app sends a random nonce; the issuer bakes it inside the signed id_token; the app checks it matches — proving the identity token was minted for this login, not replayed. Protects the id_token.

Three guards, three jobs — don't conflate

PKCE binds the code to the app that started the flow. state binds the response to the request your app sent. nonce binds the id_token to this login. Use all three; none substitutes for another.

08 The three tokens

Access, refresh, ID — three jobs, three lifetimes.

Access tokenthe working key
Refresh tokenthe renewal coupon
ID tokenidentity receipt
who you area signed JWT for the app to read
Access expires in minutes (small blast radius). Refresh lasts days–weeks (renews access). ID just says who logged in.
TokenSent toLifetimeFormJob
Accessthe API, as Authorization: Bearer …~5–60 minJWT or opaqueThe working key to call the API.
Refreshonly the issuer's /tokendays–weeksusually opaqueMint new access tokens without re-login.
ID OIDCread by the app itselfshortsigned JWTTell the app who logged in. Not a key to call APIs.
A classic bug

Sending the ID token to your API as the credential. ID token = proof of login to the app; access token = the key to the API. Different aud on purpose. Use each for its job.

Anatomy of a JWT — decode one live

Three Base64URL chunks joined by dots: header.payload.signature. The default below is a sample access token — remember chapter 6: the first two parts are merely encoded, so you're about to read them in the clear.

Live JWT decoder

All local. The signature is shown but not verified (that needs the issuer's public key).

Read that payload again

You just read every claim with no key. A JWT is signed, not secret. Its safety comes from the signature (you can't forge a valid one) and from HTTPS + storage — never from the payload being hidden.

JWT vs opaque access tokens

JWT (self-contained)Opaque (reference)
ValidationLocal: verify signature with the issuer's cached public key. No network call.Remote: call the issuer's /introspect (RFC 7662).
RevocationHard — valid until exp. Keep lifetimes short.Instant — issuer says "inactive."
Trade-offFast, stateless, scalable; leak lasts until expiry.Centrally controllable; a round-trip per request (often cached).

JWT access-token profile: RFC 9068. Bearer usage: RFC 6750.

09 How the API validates a token

What the resource server checks before serving your data.

When a request arrives with Authorization: Bearer …, the API asks four quick questions: real? (signature), still valid? (expiry), meant for me? (audience), allowed to do this? (scope). For a JWT all four are usually a sub-millisecond local check — a big reason this design scales.

The JWT validation checklist

  1. Signature — fetch the issuer's public keys from its JWKS endpoint (/.well-known/jwks.json, RFC 7517), pick the key by the header's kid, verify.
  2. exp not past (and nbf not future).
  3. iss equals the trusted issuer.
  4. aud equals this API — so a token for service A can't be replayed at B.
  5. scope covers the requested action.
Caching, the right way performance

The API caches the JWKS public keys (they rotate rarely) and validates millions of JWTs with zero network calls — fully stateless. It refetches only when a new kid appears. Opaque tokens instead cache introspection results for a few seconds. Either way: cache the validation material, never the password.

10 Where tokens live — storage & caching

Getting a token is half the battle; holding it without leaking it is the other half.

A token is a bearer credential — whoever holds it can use it. So where you keep it decides how badly an XSS bug or a stolen device hurts. Every browser option is a trade-off; the industry now keeps tokens off the browser when stakes are high.

WhereExposed to XSS?Verdict
localStorageYes — any script reads itDiscouraged for tokens. One XSS = full theft.
In-memory (JS variable)Partly — still scriptableBetter; gone on reload, but XSS-reachable while live.
HttpOnly + Secure cookieNo — invisible to JSStrong vs XSS. Add SameSite + anti-CSRF.
Native Keychain / KeystoreN/AThe right home on mobile — OS-encrypted.
The pattern the spec now recommends: Backend-for-Frontend (BFF)

For browser apps handling anything sensitive, the current IETF Best Current Practice says: don't let the browser hold OAuth tokens at all. A small backend you own (the Web Server in the Map) is the confidential client. It does the token exchange, stores access & refresh tokens server-side, and hands the browser only a hardened HttpOnly, Secure session cookie. The browser can be fully XSS-compromised and still have no token to steal.

The Browser-Based Apps BCP ranks three architectures, most-secure first:

  1. Backend-For-Frontend (BFF) — backend holds tokens, browser gets a cookie. Recommended for business/sensitive/PII apps.
  2. Token-Mediating Backend — a backend brokers tokens but passes the access token to the browser.
  3. Browser-based OAuth client — the SPA does it all (PKCE in-browser, tokens in memory). OK for low-risk apps; most exposed.

What's safe to cache, and for how long

  • Access token — until exp; refresh just before. Memory or server session, never logs/URLs.
  • Refresh token — crown jewel. Server-side (BFF) or OS secure storage; with rotation so a leak self-destructs.
  • JWKS public keys (API side) — cache aggressively; public, rotate rarely. This is what makes stateless validation fast.
  • Authorization code — never "stored." Single-use, lives seconds; redeem immediately.

Sender-constrained tokens are the frontier: DPoP (and mTLS) cryptographically bind a token to a key the client holds, so a stolen bearer token is useless without it.

11 Staying logged in — refresh & rotation

How you stay signed in for weeks while access tokens expire in minutes.

When the access token expires, the app quietly trades the refresh token for a new one on the back channel — no password, no interaction. That's why apps "remember" you.

refresh v1 refresh v2 refresh v3 ✓
someone replays the old v1whole family revoked → everyone re-logs in
Rotation: each refresh issues a new token and kills the old one. Reusing a dead one trips the alarm.
Wristband + ride tickets

The refresh token is a day wristband; access tokens are single-ride tickets that expire fast. Each time a ticket runs out you flash the wristband for a new one. Lose a ticket → someone gets one ride. The wristband is the valuable thing — so modern systems make it self-destruct if copied.

POST /token
grant_type=refresh_token
&refresh_token=v2.aBc…        ← the current one

200 OK
{ "access_token":"…new…",
  "refresh_token":"v3.dEf…",  ← ROTATED: v2 is now dead
  "expires_in":900 }

Later, if v2 appears again  ⇒  AS assumes theft
                            ⇒  revoke the whole family (v1,v2,v3…)  ⇒  re-auth
Why reuse detection works

After a rotation, the real client holds the newest token and a thief holds a stale copy (or vice-versa). Whoever presents the old one trips the alarm. A small grace/overlap window tolerates a dropped response. Pair with short lifetimes, revocation (RFC 7009), and event logging.

12 Threat → defence cheat-sheet

Every mechanism here exists to stop a specific attack.

The attackWhat goes wrongThe defence
Code interceptionA rogue app grabs the code off the front channelPKCE (ch. 5)
CSRF on the redirectAttacker injects their code into your sessionstate (ch. 7)
ID-token replayAn old identity token is reusednonce (ch. 7)
Token theft via XSSMalicious JS reads tokens from storageHttpOnly / BFF (ch. 10)
Leaked access tokenA stolen token gets usedShort lifetimes + aud/scope; DPoP (ch. 9–10)
Stolen refresh tokenLong-term silent accessRotation + reuse detection (ch. 11)
Wrong-audience replayToken for A used at Baud validation (ch. 9)
The one-sentence summary

Keep secrets off the postcard, prove possession instead of revealing it, make the powerful tokens short-lived or self-destructing, and keep them out of reach of scripts. Everything else is detail.

13 Glossary & the real specs

The vocabulary in one place, plus primary sources.

Quick glossary

Resource OwnerYou — the user who owns the data.
ClientThe app requesting access.
Authorization ServerIssues tokens after authenticating you.
Resource ServerThe API holding your data.
ScopeThe specific permissions granted.
Auth codeSingle-use, short-lived; traded for tokens.
Access tokenShort-lived key sent to the API.
Refresh tokenLong-lived; renews access tokens.
ID tokenOIDC proof of who logged in.
PKCEVerifier/challenge binding the code to the client.
JWTSigned, encoded claims: header.payload.signature.
JWKSThe issuer's public keys for verifying JWTs.
BFFBackend-for-Frontend: tokens off the browser.

Primary sources

You made it

You now have the whole map: why OAuth exists, the four players, the front/back-channel rule, the full PKCE flow, the crypto behind it, the three tokens, validation, storage, and refresh. Re-read any chapter in Detailed, or take the Map for the system view — both should read very differently now.