Update (January 2026): This article was originally framed as “cookie-based vs. token-based” authentication. That framing conflates separate concerns. This revised version untangles them.
Authentication confirms that users are who they say they are. (Authorization, by contrast, determines what a user can do.) When implementing auth for a web app, you face three independent decisions:
- Where is session state stored? Server (stateful) vs. token (stateless)
- Where is the credential stored client-side? Cookies vs. localStorage
- How is the credential transmitted? Cookie header vs. Authorization header
The common “cookie-based vs. token-based” framing conflates all three. This article examines each decision separately, then covers the security tradeoffs and common patterns that emerge from combining them.
1. Where Is Session State Stored?
This is the fundamental architectural choice.
Stateful (Session-based)
The server maintains session state:
- User logs in → server creates a session (in memory, database, or Redis)
- Server sends a session ID to the client
- Client sends the session ID with every request
- Server looks up the session ID to verify the user
The session ID is just a reference—it contains no user information. Every request requires a server-side lookup.
sequenceDiagram participant Client participant Server participant SessionStore as Session Store Client->>Server: Login (username, password) Server->>SessionStore: Create session SessionStore-->>Server: Session ID Server-->>Client: Set-Cookie with session_id Client->>Server: Request with session cookie Server->>SessionStore: Lookup session SessionStore-->>Server: User data Server-->>Client: Response
Sidenote: “Session cookie” refers to a cookie without Max-Age/Expires (deleted when browser closes). This is unrelated to session-based auth—any cookie can store a session ID.
Stateless (Token-based)
The token itself contains the session:
- User logs in → server creates a signed token with user data (claims)
- Server sends the token to the client
- Client sends the token with every request
- Server verifies the signature and reads claims directly—no lookup needed
The most common format is JSON Web Tokens (JWTs). Learn more at jwt.io.
sequenceDiagram participant Client participant Server Client->>Server: Login (username, password) Server->>Server: Create and sign JWT Server-->>Client: JWT (contains user claims) Client->>Server: Request with Authorization header Server->>Server: Verify signature and read claims Server-->>Client: Response
Important: JWTs are signed, not encrypted. The payload is base64-encoded and visible to anyone. Never store sensitive data in a JWT unless you use encrypted tokens (JWE).
2. Where Is the Credential Stored Client-Side?
This decision is independent of stateful vs. stateless. Both session IDs and JWTs can be stored in either location:
| Cookies | localStorage/sessionStorage | |
|---|---|---|
| Sent automatically | Yes (to matching domain) | No (must add manually) |
| JS accessible | Only without HttpOnly | Always |
| Size limit | ~4KB | ~5-10MB |
3. How Is the Credential Transmitted?
Also independent:
- Cookie header: Browser automatically includes cookies for the matching domain
- Authorization header: Client explicitly adds
Authorization: Bearer <token>
Security Tradeoffs
Each decision has security implications. Here’s how they interact:
XSS (Cross-Site Scripting)
Affected by: client-side storage (decision #2)
- HttpOnly cookies: Token theft prevented—JavaScript cannot access them. However, XSS can still make authenticated requests while the user is on the page (session riding).
- localStorage: Fully vulnerable. Malicious scripts can steal and exfiltrate tokens.
CSRF (Cross-Site Request Forgery)
Affected by: transmission method (decision #3)
- Cookie header: Vulnerable by default. Browsers auto-send cookies, so attackers can trigger authenticated requests from other sites.
- Authorization header: Protected. Attackers can’t read your localStorage (same-origin policy), so they can’t include the token.
Mitigation
- CSRF:
SameSite=Lax(browser default) blocks cross-site POST—sufficient for most apps.SameSite=Strictis stronger but breaks legitimate flows. CSRF tokens add defense-in-depth. - XSS: No storage mechanism fully protects you. Sanitize input, use CSP headers, avoid
dangerouslySetInnerHTML.
Token Revocation
Affected by: state location (decision #1)
- Stateful: Easy—delete the session server-side.
- Stateless: Hard—token valid until expiration. Workarounds: short expiry, blacklists (reintroduces state), refresh token rotation.
Performance
Affected by: state location (decision #1)
- Stateful: Database/cache lookup per request. Adds latency, but session data can be large.
- Stateless: Signature verification per request (fast, CPU-bound). Token sent with every request—JWTs can grow large.
Common Patterns
Real-world implementations combine these decisions strategically:
Stateful + HttpOnly cookies (decisions: server state, cookies, cookie header) The classic approach. Simple, easy revocation, works for most apps. Requires shared session store for multiple servers (Redis, database).
JWTs in HttpOnly cookies (decisions: token state, cookies, cookie header)
Stateless auth with token-theft protection. Add SameSite=Lax for CSRF protection. Scales horizontally without shared state, but revocation is harder.
Access + refresh tokens (decisions: hybrid state, cookies, cookie header) Short-lived access tokens (5-15 min) verified statelessly. Long-lived refresh tokens in HttpOnly cookies, validated server-side. Stateless performance with stateful revocation.
localStorage + Authorization header (decisions: token state, localStorage, auth header) Common in SPAs. No CSRF risk, but fully vulnerable to XSS token theft. Only use if you have strong XSS protections and accept the risk.
Summary
Choose based on your constraints:
- Need easy revocation? → Stateful (or hybrid with refresh tokens)
- Need horizontal scaling without shared state? → Stateless JWTs
- Default for most web apps: HttpOnly cookies +
SameSite=Laxwith either approach. Protects against token theft and CSRF with minimal complexity.