Learn / APIs, auth and backend architecture

OAuth 2.1, JWT and PKCE

Lesson 16 of 37 · 11 min read ·

The problem OAuth solves

A user wants to let your app read their Google Calendar. The wrong answer is asking for their Google password. OAuth exists so a user can grant a scoped, revocable delegation without ever showing you their credentials.

Four roles, worth naming precisely because the specs use them constantly:

  • Resource owner — the user.
  • Client — your application.
  • Authorization server — issues tokens (Google's login).
  • Resource server — holds the data (the Calendar API).

The authorization code flow with PKCE, step by step

This is the only flow you need in 2026. OAuth 2.1 removed the implicit and password grants, and PKCE is now required for every client, not just mobile.

  1. Your app generates a random code_verifier (43–128 characters) and hashes it: code_challenge = BASE64URL(SHA256(code_verifier)).
  2. Redirect the user to the authorization server with response_type=code, client_id, redirect_uri, scope, a random state, and the code_challenge (plus code_challenge_method=S256).
  3. The user authenticates on the authorization server and consents. Your app never sees the password.
  4. The server redirects back to your redirect_uri with a short-lived code and the state you sent.
  5. Verify state matches what you generated. This is your CSRF defence; skipping it is a real vulnerability.
  6. Your backend exchanges the code at the token endpoint, sending the original code_verifier.
  7. The server hashes the verifier, compares it to the challenge from step 2, and if they match returns an access_token and usually a refresh_token.

What PKCE prevents: an attacker who intercepts the authorization code — through a malicious app registering the same URL scheme, a leaky redirect, browser history — cannot exchange it, because they do not have the verifier. The code alone is useless. That is the entire point, and it is why PKCE is mandatory now.

The authorization code flow with PKCE. The verifier never leaves your app until step 7.The authorization code flow with PKCE. The verifier never leaves your app until step 7.

What a JWT actually is

Three base64url segments separated by dots: header, payload, signature.

It is signed, not encrypted. Anyone can read the payload. Paste one into a decoder and you will see the claims in plain text. Never put anything secret in a JWT.

The signature proves two things: it was issued by whoever holds the signing key, and it has not been modified since. Nothing else.

Validating one properly means checking all of:

  • The signature, against the expected key.
  • alg — against an allowlist you control. Never trust the token's own header to choose the algorithm; alg: none and algorithm-confusion attacks exist precisely because implementations did.
  • exp — expired tokens are rejected.
  • iss — issued by the authorization server you expect.
  • aud — intended for your API, not a different service that shares an issuer.

Using a maintained library is the correct move here. Hand-rolled JWT validation is a reliable source of vulnerabilities.

The stateless trade-off

A JWT's appeal is that any service can verify it without a database lookup. The price: you cannot revoke it. A stolen token is valid until it expires.

The standard mitigation is short-lived access tokens (5–15 minutes) plus a long-lived refresh token stored server-side, which can be revoked. Logout revokes the refresh token; the access token dies on its own shortly after. If you need instant revocation, you need a check against shared state, and at that point a plain opaque session token is often the simpler, better choice. Sessions are not outdated — they are frequently correct.

Where tokens live

  • Browser: httpOnly, Secure, SameSite cookies. localStorage is readable by any XSS, and one XSS then means full account takeover.
  • Mobile: the platform keychain / keystore.
  • Server-to-server: environment or a secrets manager. See secrets management.

Gotchas

  • Register exact redirect URIs. Wildcards enable token theft.
  • Scopes should be the minimum the feature needs — least privilege applies to tokens too.
  • Rotate refresh tokens on use and detect reuse; a reused refresh token means it leaked.
  • Clock skew breaks exp validation across machines. Allow a small leeway, and keep NTP running.

Prove you know it

Explain the seven steps above out loud, from memory, and answer two questions without notes: what exactly does PKCE prevent, and why can you not revoke a JWT? If you stall on either, that is the gap.

Go deeper