Skip to content
yutils

How Authentication Actually Works (Session, JWT, OAuth, Passkey)

Session cookies vs JWT vs OAuth flows vs passkey — what each model actually stores, where the trust lives, why refresh tokens exist, and how passkeys replace passwords entirely with WebAuthn.

~9 min read

How does the server know "this is you" on every request after login? A session ID in a cookie? A JWT? An OAuth access token? And why can you "Sign in with Apple / Google" without typing a password — or use passkeys to skip passwords entirely? This guide walks through the four common authentication models — session cookies, JWT, OAuth (OIDC), and passkeys — and where the trust actually lives.

Model 1 — Session cookies (the classic)

1. Login:
   POST /login
   {username, password}
   ↓
   Server:
   - bcrypt-verify password
   - Generate a random session ID (e.g. "abc123...")
   - Store {sessionId: userId} in DB / Redis
   - Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax

2. Subsequent requests:
   Cookie: session=abc123
   ↓
   Server:
   - DB lookup: sessionId → userId
   - request.user = User{...}

3. Logout:
   - Delete the session from DB
   - Set-Cookie: session=; expires=0

Pros:

  • Session ID is opaque (meaningless random)
  • Server can revoke instantly (delete from DB)
  • Cookies with HttpOnly / Secure / SameSite are safe
  • User profile changes apply on next request (server-side state)

Cons:

  • DB lookup on every request (Redis softens this)
  • Multi-server deployments need shared storage (sticky session or shared store)
  • Scale-out gets clunky

Model 2 — JWT (JSON Web Token)

1. Login:
   POST /login
   {username, password}
   ↓
   Server:
   - bcrypt-verify password
   - Create JWT: header.payload.signature
     payload = {sub: "user-42", exp: 1700000000}
     signature = HMAC-SHA256(header.payload, SECRET)
   - Respond: {token: "eyJ..."}

2. Subsequent requests:
   Authorization: Bearer eyJ...
   ↓
   Server:
   - Verify signature with SECRET
   - Check exp
   - request.user = {id: payload.sub}
   ↑ No DB lookup needed

Try it — JWT Decoder breaks a JWT into header / payload / signature. The signature uses the same algorithm as HMAC Generator.

JWT pros / cons

Pros:

  • Stateless — no server session storage. Trivially shared across servers / microservices.
  • Self-contained — user info travels in the payload
  • Great fit for mobile, APIs, distributed systems

Cons:

  • Hard to revoke — a valid token works until it expires. Blacklists undo the statelessness.
  • Bigger than session IDs (~500 bytes vs ~30)
  • Payload is base64url, not encrypted — anyone can decode. Don't put secrets in the payload.
  • Leaking the secret = every token forgeable. Plan key rotation.

Refresh tokens — softening JWT revocation

On login:
- access token: short expiry (15 min), client storage
- refresh token: long expiry (30 days), HttpOnly cookie + DB row

Requests:
1. Use access token until it returns 401
2. Exchange refresh token for a new access token:
   POST /auth/refresh  (cookie auto-sends)
   ↓
   Server: validates refresh in DB → issues new access
3. Logout / suspicion: delete the DB refresh row
   → next refresh attempt is denied

Two-token pattern — short stateless access + long stateful refresh. Revocable + scalable.

Model 3 — OAuth 2.0 (third-party login)

"Sign in with Apple / Google / GitHub" mechanics. Authorization Code Flow + PKCE is the modern standard:

1. User clicks "Sign in with Google"

2. Our site → Google's authorize URL:
   https://accounts.google.com/o/oauth2/v2/auth
     ?client_id=...
     &redirect_uri=https://ours.com/callback
     &response_type=code
     &scope=openid email profile
     &state=random_token       ← CSRF defense
     &code_challenge=...       ← PKCE
     &code_challenge_method=S256

3. User logs in at Google + consents to our site's permissions

4. Google → our site's callback (with code):
   https://ours.com/callback?code=ABC123&state=random_token

5. Our server → Google's token endpoint:
   POST https://oauth2.googleapis.com/token
   {
     code: "ABC123",
     client_id: "...",
     client_secret: "...",
     code_verifier: "..."      ← PKCE
   }
   ↓
   Google replies: {
     access_token: "ya29...",     ← for Google API calls
     refresh_token: "...",
     id_token: "eyJ..."           ← JWT with user info
   }

6. Our server:
   - Verify id_token signature (Google's public key)
   - Extract user info (email, name) from payload
   - Upsert user in our DB
   - Issue our own session/JWT → user is logged in

PKCE — safety for public clients

Native apps / mobile / SPAs can't keep client_secret secret (reverse engineering). PKCE adds protection:

Client generates a random code_verifier each time (~43 chars)
code_challenge = SHA-256(code_verifier), base64url

Step 2: send only code_challenge to Google; keep code_verifier
Step 5: send code_verifier; Google verifies the hash matches

→ A man-in-the-middle who grabs the code can't use it without the verifier

OAuth vs OpenID Connect (OIDC)

  • OAuth 2.0 = authorization. "Let this app use my Drive files."
  • OIDC = authentication layer on top of OAuth. id_token (JWT) carries identity. "This user's email is X."

"Sign in with Google" is OIDC — we just want to know who the user is, not access Drive.

Model 4 — Passkeys (WebAuthn)

Standardized in 2022+. Replaces passwords entirely. Apple / Google / Microsoft default-pushed in 2024:

1. Registration:
   - Server: generates a random challenge
   - Browser asks the user "create a passkey for this site?" (Touch ID / Face ID)
   - Device generates a new key pair:
     - Private key stays on the device (secure enclave)
     - Public key sent to the server
   - Server stores (publicKey, userId)

2. Authentication:
   - Server: generates a random challenge
   - Browser asks the device to unlock (Touch ID / Face ID)
   - Device signs the challenge with the private key
   - Server verifies with the stored public key
   - Success → start a session

Pros:

  • No password — nothing to type or remember
  • Phishing-resistant — the passkey is bound to the origin; fake sites can't reuse it
  • DB breaches are toothless — server only stores public keys
  • iCloud Keychain / Google Password Manager sync across devices

Cons:

  • Recovery needs another trusted device or backup
  • Ecosystem lock-in (Apple / Google cloud sync isn't portable yet)
  • Not every server supports WebAuthn yet

2FA (Two-Factor Authentication)

  • TOTP — Google Authenticator. HMAC-SHA1 of the current 30-second window → 6-digit code.
  • SMS — code via text. Weak (SIM swapping).
  • WebAuthn / security key — YubiKey-style hardware. Strongest.
  • Passkey — essentially device + biometric = built-in 2FA.

When to use which

Session CookieJWTOAuth (OIDC)Passkey
Stateful✅ (DB)❌ (self-contained)❌ (token-based)✅ (stored public key)
RevokeInstantHardVia refresh tokenInstant (delete public key)
Scale-outNeeds shared storeVery easyEasyVery easy
Mobile / APICookies awkwardBest fitBest fitIdeal but adoption is in progress
UX (password)RequiredRequiredDelegated to providerNone
SecurityStandardSecret hygiene requiredComplex implementationStrongest

Practical defaults:

  • Classic web site — session cookie. Simple, safe.
  • Mobile + web — JWT (access + refresh).
  • Third-party login — OAuth/OIDC.
  • Modern / security-sensitive — passkey with a fallback.

Common pitfalls

1. Leaked JWT secret

Secret leak = every token forgeable. Manage via .env, rotate keys, prefer RS256 (asymmetric) so verifiers only need the public key.

2. Secrets in the JWT payload

# Bad
payload: {sub: "42", role: "admin", ssn: "..."}
↓ base64url is not encryption
↓ anyone can decode

# Good
payload: {sub: "42"}
↓ look up extra info in the DB

3. CSRF (cookie model)

Cookies auto-send cross-site → use SameSite=Lax/Strict + CSRF tokens. JWTs in the Authorization header naturally avoid CSRF.

4. JWT in localStorage

XSS attackers can read localStorage. HttpOnly cookies are safer, but bring CSRF concerns. Trade-off.

5. Missing OAuth state parameter

Without it, an attacker can stash their own OAuth code into a callback URL and trick the victim into completing the flow. Always validate the state.

6. Password-reset tokens that don't expire

Indefinite reset links + leaked email = permanent compromise. Use 15-60 min, one-time, single-use.

References

Summary

  • Four models — session cookie / JWT / OAuth (OIDC) / passkey.
  • Session cookies are stateful + DB-looked-up. Instant revoke. Classic web default.
  • JWT is stateless + self-contained. Easy to scale, hard to revoke → access + refresh pattern.
  • OAuth 2.0 (+ OIDC) is third-party login. PKCE secures public clients; state parameter prevents CSRF.
  • Passkeys remove passwords entirely. Phishing-resistant, breach-resistant. The 2024 default direction.
  • 2FA — TOTP / SMS (weak) / WebAuthn (strong) / passkey (built-in).
  • Storage — HttpOnly cookie vs localStorage. Cookies are XSS- safe but need CSRF defense.
  • Try it — JWT Decoder for JWTs, HMAC Generator for signatures, Bcrypt Hash for password hashes.
Back to guides