Introduction
Authentication is the backbone of every web application. Two dominant patterns have emerged: stateless JWT (JSON Web Token) auth and stateful session-based auth. Both solve the same problem — verifying who a user is on subsequent requests — but they differ fundamentally in storage, revocation, and security properties. This article provides a detailed comparison to help you choose the right approach for your application.
JWT Structure
A JWT is a self-contained token consisting of three base64url-encoded segments separated by dots:
header.payload.signature
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"sub": "user123",
"iat": 1712345678,
"exp": 1712349278,
"role": "admin"
}
// Signature (HMAC-SHA256 of header + payload)
| Segment | Contents |
|---|---|
| Header | Algorithm (HS256, RS256) and token type |
| Payload | Claims — sub (subject), iat (issued at), exp (expiration), custom data |
| Signature | Verifies the token hasn’t been tampered with |
Since the payload is only base64-encoded (not encrypted), sensitive data must never be placed in the payload unless using JWE (JSON Web Encryption).
Session-Based Auth
In session-based auth, the server stores session data (typically in memory, Redis, or a database). The client only holds a session ID (a random opaque string) in a cookie:
Head-to-Head Comparison
| Aspect | JWT | Session |
|---|---|---|
| Storage location | Client holds all data | Server holds session data |
| Server state | Stateless — no DB lookup needed | Stateful — requires session store query |
| Scalability | Excellent — no shared session store | Requires centralized Redis/DB for multi-instance |
| Revocation | Difficult — valid until exp | Immediate — delete session from store |
| Token size | Larger (hundreds of bytes) | Small (session ID ~32 bytes) |
| XSS vulnerability | Token accessible to JS (localStorage) | httpOnly cookie not accessible to JS |
| CSRF protection | Requires manual handling | SameSite cookie + CSRF token |
Revocation Challenge with JWT
The most significant weakness of JWT is revocation. Once issued, a JWT remains valid until its exp claim. If a user logs out, changes password, or is banned, the token cannot be invalidated without server-side state — which defeats the stateless premise.
Mitigation Strategies
1. Short-lived access tokens + refresh tokens:
// Access token: 15 minutes
// Refresh token: 7 days (stored in database)
The access token has a short TTL so damage is limited if leaked. The refresh token is stored server-side, so it can be revoked.
2. Token blacklist (deny-list):
// On logout, add jti (JWT ID) to Redis with TTL matching token expiry
await redis.set(`blacklist:${jti}`, 'true', 'EX', remainingTTL);
This reintroduces server state but only for revoked tokens, not all active sessions.
3. Rotate token signature key:
Changing the signing key invalidates all tokens — a blunt but effective tool for emergencies.
XSS and XSRF Considerations
| Threat | JWT (localStorage) | Session (httpOnly cookie) |
|---|---|---|
| XSS | Token stolen via document.cookie or localStorage | Token not accessible to JS (httpOnly) |
| CSRF | Not vulnerable (manual Authorization header) | Vulnerable without SameSite or CSRF token |
JWTs stored in localStorage are vulnerable to XSS — any injected script can read them. HttpOnly cookies prevent this but require CSRF protection since cookies are automatically sent.
Best practice for JWTs: Store the access token in an httpOnly, Secure, SameSite=Strict cookie (not localStorage). The server reads it from the Authorization header, not a cookie, to avoid CSRF.
// Set JWT in httpOnly cookie
res.cookie('token', jwt, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000
});
Hybrid Approaches
Many production systems combine both patterns:
| Approach | Access Token | Refresh Token |
|---|---|---|
| JWT-only | JWT in memory | JWT with long exp |
| Session-style | Opaque session ID | — |
| Hybrid | Short-lived JWT (15 min) | Opaque refresh token in DB |
The hybrid model gives you the stateless efficiency of JWT for most requests while retaining revocation capability through the refresh token.
Decision Matrix
| Your Priority | Recommended Approach |
|---|---|
| Minimal server DB load | JWT (stateless) |
| Instant user logout / ban | Session-based |
| Microservices / API gateways | JWT (pass-through) |
| Mobile apps | JWT (no cookie storage) |
| Server-rendered web apps | Session (httpOnly cookie) |
| Third-party API access | JWT (bearer token) |
Conclusion
There is no universal winner. JWT excels in distributed systems, mobile apps, and API-first architectures where statelessness is valuable. Session auth provides superior control for revocation, logout, and server-side security enforcement. The hybrid pattern — short-lived JWTs backed by revocable refresh tokens — offers a pragmatic compromise that captures the strengths of both approaches while mitigating their weaknesses.
