Chapter 3 · Authentication & Access· Intermediate· ~18 min

Sessions, Cookies, OAuth

Past "use OAuth" - the token shapes, refresh rotation, PKCE for every public client, and the attacks that recur.

You will learn

  • The real trade-off between opaque session IDs and self-contained JWTs
  • Refresh-token rotation and the absolute family-revocation rule
  • Why PKCE belongs on every public client, including SPAs
  • The four recurring OAuth attacks: open redirect, authorization-code interception, mix-up, IDP-confusion
  • How to think about logout in a multi-tab, multi-service world

Opaque session IDs vs self-contained JWTs

Two shapes for "you are signed in":

  • Opaque session ID: a random string, looked up server-side against a store. Revoking is one delete. The store has to be highly available. Stateless services are harder.
  • Self-contained token (JWT): signed payload carrying identity and claims. Stateless verification. Revoking before expiry requires a denylist (which reintroduces the store), and any leaked token is valid until it expires.

Rule of thumb:

  • If you control both ends and have low session count: opaque IDs, period.
  • If you serve many services that must accept tokens without a central call per request: short-lived JWTs (5 to 15 minutes), paired with refresh tokens.
  • Never long-lived JWTs as session cookies. The blast radius of a leak is too large.

Refresh tokens and rotation

The standard pattern: a short-lived access token (minutes) plus a long-lived refresh token (hours to days). When the access token expires, the client posts the refresh token to the authorization server and gets a new pair.

Two critical defenses:

  • Rotation: each refresh exchange returns a new refresh token and invalidates the old one. Refresh tokens are one-time-use.
  • Family revocation: if a refresh token is presented twice, the server has detected token theft. Revoke the entire token family (the original and every descendant) and force the user to re-authenticate.

Without rotation, a stolen refresh token grants indefinite access. Without family revocation, an attacker can use a stolen token once even after the legitimate user rotates it.

PKCE on every public client

PKCE (Proof Key for Code Exchange) protects the authorization code from interception. The client generates a high-entropy code_verifier, sends its SHA-256 hash (code_challenge) with the auth request, and presents the verifier when exchanging the code for tokens. An attacker who intercepts the code can't redeem it without the verifier.

Originally designed for mobile apps. The current OAuth security best-current-practice (RFC 9700) recommends PKCE for every client, including confidential server-side apps. The cost is negligible; the defense-in-depth is real.

The four attacks that recur

  1. Open redirect on the redirect_uri. The auth server allows any URI that matches a loose registration pattern; the attacker sends the user to https://app.example.com/redirect?next=https://evil.example; the auth server bounces back through evil.example carrying the code. Fix: exact-match redirect URIs only.
  2. Authorization-code interception. On mobile, two apps register the same custom scheme; one steals codes intended for the other. Fix: PKCE plus, where possible, app-claimed universal links / app links.
  3. OAuth mix-up. The client supports multiple identity providers and does not bind the token response to the IDP that issued it. An attacker who controls a legitimate-but-malicious IDP gets the client to send another IDP's code to it. Fix: include iss in the authorization response and verify it.
  4. IDP confusion / IDP-mix-up. Variant of the above where the attacker's IDP returns claims for another user's account. Fix: bind sub claims to a specific iss; do not match users across IDPs by email alone.

Logout in a multi-tab world

Logout has gotten complicated. Three layers:

  • Local logout: drop the session cookie in this browser. Easy.
  • Global logout: tell the auth server to revoke active tokens. The OAuth back-channel logout spec defines this.
  • Cross-tab: a logout in one tab should propagate to others. Use the storage event or BroadcastChannel API to notify open tabs.

Federated identity layers complicate further: logging out of your app does not log you out of Google. The session at the IDP is separate. For high-security applications, prompt the user before reusing an existing IDP session.

Boring is good

For a typical web app: opaque session cookies with HttpOnly + Secure + SameSite=Lax, rotating refresh tokens for API access, PKCE everywhere, exact redirect URIs, and one round of threat modelling against the four attacks above. Boring is the goal.