Home/Learn/Full-Stack Integration/Session Cookies, JWTs and OAuth

Session Cookies, JWTs and OAuth

Intermediate
Auth in the Browser

Three mechanisms with different trade-offs. The real question is not which is more modern, it is whether you can revoke a credential before it expires.

Overview

Auth choices get argued about as fashion — sessions are old, JWTs are modern — when the actual distinction is where the truth lives. A session cookie is a meaningless id whose meaning is in your database, so revoking it is a delete. A JWT carries its own claims and is verified by signature, so nothing needs to be looked up and nothing can be taken back before it expires. That single property drives everything else: how you log someone out everywhere, what happens when a role changes, and how long a token should live.

Sessions

The server remembers. Stateful, revocable, and still the right default for a browser app.

Opaque id, state on the server
# Login
session_id = secrets.token_urlsafe(32)
await redis.setex(f"session:{session_id}", 86400, json.dumps({"user_id": u.id}))
response.set_cookie("sid", session_id, httponly=True, secure=True,
                    samesite="lax", max_age=86400)

# Every request
data = await redis.get(f"session:{request.cookies['sid']}")

# Logout — genuinely gone, immediately
await redis.delete(f"session:{session_id}")

# Properties:
#   + revoke instantly, log out everywhere, change a role mid-session
#   + the cookie is opaque, so nothing leaks if it is read
#   + no size limit on what you store
#   - a lookup per request (Redis: sub-millisecond, rarely the issue)
#   - state to run and scale

# "Sessions do not scale" is repeated far more often than it is true.
# Redis handles this trivially at the size almost every product is.

JWTs

The token carries its own claims. Stateless, verifiable anywhere — and not revocable.

Readable by anyone, revocable by no one
// header.payload.signature — base64url, NOT encrypted.
{ "sub": "42", "role": "admin", "exp": 1757310000, "iat": 1757306400 }

// Anyone holding it can read it. Never put anything secret in it.
JSON.parse(atob(token.split('.')[1]))     // trivially decoded

// Verified with a signature, so no database lookup is needed.
payload = jwt.decode(token, SECRET, algorithms=["HS256"])

// The problem: you cannot take it back. Fire an employee at 10:00
// and their admin token stays valid until it expires. Demote a user
// and their old role travels in the token until then.

// Which forces one of these:
//   - short expiry (5-15 min) plus a refresh token   <- the usual answer
//   - a denylist of revoked jti values               <- now stateful again
//   - a token_version on the user, compared per request  <- also stateful

// Two rules that are not optional:
//   1. always specify algorithms= on decode. Omitting it has allowed
//      "alg": "none" forgeries in real libraries.
//   2. verify on the SERVER. Decoding a JWT in the browser tells you
//      what to display, never what the user may do.

// Good fit: service-to-service calls, mobile clients, a stateless API
// behind a gateway. Weaker fit: a browser session that must be
// revocable.

OAuth and OIDC

Delegating identity to Google or GitHub, and what the flow protects against.

Authorization Code + PKCE, exchanged server-side
// OAuth 2.0 = authorisation (may this app act for you).
// OIDC adds identity (who you are) as an id_token.

// The Authorization Code flow with PKCE, which is the only one to
// use in a browser today:
//   1. redirect to Google with client_id, redirect_uri, state,
//      code_challenge
//   2. the user consents
//   3. Google redirects back with ?code=...&state=...
//   4. YOUR SERVER exchanges code + code_verifier + client_secret
//      for tokens
//   5. your server creates ITS OWN session or token for your app

// Step 4 happens server-side because the client secret must never
// reach the browser. Step 5 matters too: after OAuth, issue your own
// session — do not hand Google's access token to your frontend and
// call it authentication.

// state prevents CSRF on the callback; PKCE prevents an intercepted
// code being redeemed by someone else.

// The implicit flow (tokens in the URL fragment) is deprecated.
// Any tutorial still teaching it is out of date.

// In practice most teams use a provider — Auth0, Clerk, Supabase
// Auth, NextAuth — which is a reasonable call. Understand the flow
// anyway, because you will debug a redirect_uri mismatch.

Key Points to Remember

  • 1A session cookie is an opaque id whose meaning lives server-side, which makes revocation immediate
  • 2A JWT carries readable claims verified by signature and cannot be revoked before it expires
  • 3Never put secrets in a JWT and always pass algorithms explicitly when decoding
  • 4Short-lived access tokens plus refresh tokens are the standard workaround for JWT revocation
  • 5Use the Authorization Code flow with PKCE, exchange the code server-side, and then issue your own session

Interview Questions

Sign in to ask Aria
1

What is the fundamental trade-off between session cookies and JWTs?

Medium
2

How do you log a user out of a JWT-based system immediately?

Hard
3

Why must the OAuth code exchange happen on the server?

Hard

Ask Aria about Session Cookies, JWTs and OAuth

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…