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=0Pros:
- 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 neededTry 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 deniedTwo-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 inPKCE — 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 verifierOAuth 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 sessionPros:
- 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 Cookie | JWT | OAuth (OIDC) | Passkey | |
|---|---|---|---|---|
| Stateful | ✅ (DB) | ❌ (self-contained) | ❌ (token-based) | ✅ (stored public key) |
| Revoke | Instant | Hard | Via refresh token | Instant (delete public key) |
| Scale-out | Needs shared store | Very easy | Easy | Very easy |
| Mobile / API | Cookies awkward | Best fit | Best fit | Ideal but adoption is in progress |
| UX (password) | Required | Required | Delegated to provider | None |
| Security | Standard | Secret hygiene required | Complex implementation | Strongest |
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 DB3. 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
- RFC 6749 (OAuth 2.0) — datatracker
- RFC 7519 (JWT) — datatracker
- WebAuthn (passkeys) — W3C
- OWASP Authentication Cheat Sheet — OWASP
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.