JWT vs Session Authentication: Choosing the Right Approach
Introduction
Every web application must answer: how do we know who this request belongs to? The two dominant patterns are session-based authentication (server stores state, client holds session ID) and JWT (self-contained signed tokens, often stateless on server).
Neither is universally superior—choice depends on architecture (monolith vs microservices), client types (browser vs mobile), revocation needs, and security requirements. This guide compares mechanisms, attacks, scaling patterns, and when to combine both.
Table of Contents
- Session-Based Authentication
- JWT Structure and Flow
- Security Comparison
- Scaling and Stateless Services
- Token Storage in Browsers
- Refresh Tokens and Rotation
- Hybrid Patterns
- Best Practices
- Common Mistakes
- Interview Questions
- Summary
- References
Session-Based Authentication
After login, server creates session record:
const session = await sessionStore.create({
userId: user.id,
role: user.role,
expiresAt: Date.now() + 86400000,
});
res.cookie("sid", session.id, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 86400000,
});
Subsequent requests send cookie; server looks up session in Redis or DB:
async function authenticate(req, res, next) {
const sid = req.cookies.sid;
if (!sid) return res.status(401).json({ error: "Unauthorized" });
const session = await sessionStore.get(sid);
if (!session || session.expiresAt < Date.now()) {
return res.status(401).json({ error: "Session expired" });
}
req.user = { id: session.userId, role: session.role };
next();
}
Revocation is instant—delete session. Server state required.
JWT Structure and Flow
JWT has three Base64URL parts: header, payload, signature.
// Payload (claims)
{
"sub": "user-123",
"role": "admin",
"iat": 1704067200,
"exp": 1704153600
}
import jwt from "jsonwebtoken";
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "15m", algorithm: "HS256" }
);
// Verify
const payload = jwt.verify(token, process.env.JWT_SECRET);
Servers verify signature without DB lookup—stateless until you need revocation lists.
Use RS256 (public/private key) when multiple services verify tokens—auth service signs, APIs verify with public key only.
Security Comparison
| Aspect | Sessions | JWT |
|---|---|---|
| Revocation | Easy (delete session) | Hard without blocklist/short TTL |
| XSS theft | Cookie httpOnly helps | localStorage vulnerable |
| CSRF | Cookie needs SameSite/CSRF token | Bearer header less CSRF-prone |
| Size | Small cookie (session ID) | Large header every request |
| Secret compromise | Rotate session store | All tokens invalid if key leaked |
JWT is not encrypted by default—payload is readable; don't put secrets in claims.
Scaling and Stateless Services
Sessions require shared session store when multiple app instances:
[LB] → App1, App2, App3 → Redis (sessions)
JWT lets APIs verify locally:
[LB] → API1, API2, API3 (verify JWT with public key)
Good for microservices and edge—but short-lived access tokens + refresh flow still needs storage or rotation strategy.
Token Storage in Browsers
Avoid localStorage for sensitive tokens—any XSS exfiltrates.
Preferred for SPAs:
- HttpOnly secure cookie for refresh token
- Memory for access token (lost on tab close)
- Or BFF pattern—backend holds tokens; browser only has session cookie
// BFF: frontend calls /api/me; BFF attaches JWT server-side
Mobile apps often use secure storage (Keychain/Keystore) with refresh tokens.
Refresh Tokens and Rotation
Short access token (15 min) limits exposure:
const access = signAccess(user, "15m");
const refresh = signRefresh(user, "7d");
await refreshStore.save(refreshId, {
userId: user.id,
familyId: crypto.randomUUID(),
});
Rotation: each refresh use issues new refresh token, invalidates old—detects theft if old token reused:
if (stored.token !== presentedRefresh) {
await refreshStore.revokeFamily(familyId);
throw new UnauthorizedError("Token reuse detected");
}
Hybrid Patterns
Common production setup:
- Session cookie for same-site web app
- JWT for API-to-API and mobile
- OAuth2/OIDC (Auth0, Keycloak) issuing JWTs; your API validates
NextAuth.js, Passport, and similar abstract both patterns.
Best Practices
- Short access token TTL—15 minutes or less for JWT.
- HttpOnly, Secure, SameSite cookies for browser sessions.
- RS256 for multi-service verification.
- Refresh token rotation with family revocation.
- Never store JWT in localStorage for high-security apps.
- Rate limit auth endpoints—brute force protection.
Common Mistakes
- Long-lived JWT without revocation plan.
- HS256 shared secret across many services—key sprawl.
- Sensitive data in JWT payload—PII visible to client.
- Ignoring CSRF with cookie-based sessions on SPA.
- No
algvalidation—algorithm confusion attacks (use established libraries). - Rolling your own crypto—use
jsonwebtoken,jose, or OIDC providers.
Interview Questions
- How does session authentication work?
- JWT structure and verification steps?
- How to revoke a JWT?
- XSS vs CSRF—which auth pattern is affected?
- When prefer sessions over JWT?
- Explain refresh token rotation.
- BFF pattern purpose?
- RS256 vs HS256?
OAuth2 and OIDC Integration
Most production apps delegate identity to providers (Google, GitHub, Auth0):
User → OAuth provider login
→ Redirect with code
→ Your server exchanges code for tokens
→ Session cookie OR JWT issued to client
Your API validates access tokens from provider or your own auth service—do not parse opaque provider tokens without verification docs.
OIDC adds standardized ID tokens with sub, email, aud claims—verify iss, aud, and signature against provider JWKS endpoint.
Multi-Factor Authentication
Sessions simplify MFA—mark session mfaVerified after second factor. JWT MFA requires short-lived step-up tokens or embedded amr claims checked on sensitive operations.
if (req.user.role === "admin" && !req.session.mfaVerified) {
return res.status(403).json({ error: "MFA required" });
}
Never store MFA secrets in JWT payload—server-side verification only.
Compliance and Audit Logging
Log authentication events with correlation IDs:
{
"event": "login_success",
"userId": "u_123",
"ip": "203.0.113.1",
"method": "oauth_google",
"timestamp": "2025-07-01T12:00:00Z"
}
Retention policies differ by jurisdiction—design logs without storing raw passwords or refresh tokens.
JWT Best Practices in Microservices
Service mesh pattern:
[Client] → [API Gateway] validates JWT
→ [Service A] validates JWT (same public key)
→ [Service B] validates JWT
Distribute public keys via JWKS URL; rotate signing keys with overlapping validity windows. Services cache JWKS with TTL and refresh on kid mismatch.
Never pass JWTs in URL query strings—logs and referrer headers leak them.
Session Fixation and Rotation
On login success, rotate session ID:
req.session.regenerate((err) => {
if (err) return next(err);
req.session.userId = user.id;
req.session.save(() => res.redirect("/dashboard"));
});
Prevents session fixation attacks where attacker sets victim's session ID before authentication.
Conduct annual auth architecture review—session store capacity, JWT key rotation procedures, and OAuth provider dependency risks. Third-party auth outages become your login outage; maintain status page communication templates and optional maintenance mode that explains authentication provider issues without exposing internal implementation details to attackers mapping your infrastructure dependencies.
Penetration testers routinely target session fixation, JWT algorithm confusion, and OAuth redirect URI manipulation—run third-party assessments after major auth changes and remediate findings before public launch deadlines rather than accepting risk transfers that become breach headlines when attackers chain low-severity findings into account takeover flows affecting customers who trusted your login page branding and security marketing claims displayed prominently on the marketing site homepage hero section visible to millions of prospective users evaluating your product against competitors with mature security programs and published SOC2 reports.
Summary
Sessions offer simple revocation and smaller cookies; JWTs enable stateless verification across services. Browsers favor HttpOnly session or refresh cookies; APIs favor short-lived JWTs with rotation. Choose based on revocation needs, client type, and architecture—hybrid approaches are common at scale.