Warming up the neural circuits...
By the end of this chapter you will:
Sessions are the "boring" option. They're also the default choice of banks, government systems, and most server-rendered web apps. Sessions don't get the hype gets, but they're simpler, more secure for browser-based apps, and support instant revocation. This chapter teaches you when boring is better.
You arrive at a venue and hand your coat to the attendant. They give you a numbered ticket. When you want your coat back, you show the ticket — they match it to their rack and return your coat. The ticket (session ID) is meaningless on its own. The real data (your coat = user data) stays securely on the server. If you lose the ticket, the attendant can invalidate it instantly. No attacker with just the ticket can access your coat unless the venue also knows the ticket belongs to you (which they verify by asking for ID — or by checking IP/user-agent).
1. User logs in → server creates a session record (in DB/Redis) with a random session ID.
2. Server sends session ID to browser as a cookie: Set-Cookie: sessionId=abc123
3. Browser sends the cookie with every request: Cookie: sessionId=abc123
4. Server looks up session ID → finds user data → attaches to request.
5. User logs out → server deletes session record. Cookie is useless.
6. (Optional) Session expires after N minutes of inactivity.const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('
| What it does | Without it | |
|---|---|---|
| HttpOnly | Cookie inaccessible to (document.cookie) | XSS attack can steal session ID → full account takeover |
| Secure | Cookie only sent over HTTPS | Man-in-the-middle can intercept cookie over HTTP |
| SameSite=Lax | Cookie NOT sent on cross-site requests (except top-level navigation GET) | CSRF attacks can use your session without your knowledge |
| SameSite=Strict | Cookie NOT sent on ANY cross-site request | Maximum CSRF protection, but breaks "click link to app from email" flow |
cookie: {
httpOnly: true, // ✅ Always
secure: true, // ✅ Always in production
sameSite: 'lax', // ✅ Default. Use 'strict' for banking apps.
maxAge: 86400000, // 24 hours
domain: 'example.com',
Session fixation: an attacker sets a known session ID in a victim's browser, waits for them to log in, then uses the same session ID to access their account.
Attacker: visits /login → gets session ID "abc123"
Attacker: sends victim a link: https://app.com/login?sessionId=abc123
Victim: clicks link, logs in. Session "abc123" now contains victim's user data.
Attacker: uses session "abc123" → logged in as victim.Prevention: req.session.regenerate() after login. This creates a new, random session ID after authentication. The old session ID the attacker set is now useless.
| Store | Pros | Cons | Best for |
|---|---|---|---|
| Memory (default) | Fastest, zero setup | Lost on server restart. Doesn't scale (each server has its own memory) | Development only |
| Redis | Fast (~0.5ms), shared across servers, built-in expiry | Extra infrastructure | Production — best choice |
| Database (PostgreSQL) | No extra infrastructure, durable | Slower (~5ms), more load on primary DB | Small projects that already have a DB |
| Factor | Sessions | JWTs |
|---|---|---|
| Verification speed | ~1ms (Redis/DB lookup per request) | ~0.1ms (no lookup needed) |
| Revocation | Instant — delete session from store | Requires blocklist or short expiry + refresh |
| Server scalability | Requires shared session store (Redis) | Stateless — any server can verify |
| XSS risk | Low — HttpOnly cookie, JS can't read | High if stored in localStorage. Safe if in HttpOnly cookie (but then why not sessions?) |
| CSRF risk | Present — cookies sent automatically | Low — token must be explicitly attached to requests |
| Mobile/native apps | Cookies are unnatural; need token endpoints | Natural — just store token securely |
| Microservices | Requires shared Redis or sticky sessions | Stateless — ideal. RS256 lets any service verify. |
| Logout | Simple — destroy session | Complex — must revoke refresh token + wait for access token to expire |
Browser-based apps (server-rendered, MPAs): Sessions. Simple, secure, HttpOnly cookies. Mobile/native apps: JWTs + refresh tokens. No cookies. SPA + (same origin): Sessions (with HttpOnly cookies) or BFF pattern. Microservices: JWTs with RS256. Stateless verification across services.
Banks overwhelmingly use session-based auth — not JWTs. Why?
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Default session secret | secret: 'keyboard cat' — the express-session default. Known to every attacker. | Generate a strong random secret: openssl rand -hex 32 |
| Not regenerating session after login | Session fixation vulnerability — attacker can preset a session ID | req.session.regenerate() after successful authentication |
| Memory store in production | Sessions lost on restart. Memory leak as sessions accumulate. Doesn't scale past 1 server. | Redis (recommended) or database-backed session store |
| Missing HttpOnly flag | XSS can steal session cookie via document.cookie | Always httpOnly: true |
| Missing Secure flag in production | Cookie sent over HTTP — interceptable on public Wi-Fi | secure: true in production |
sameSite: 'none' without understanding | Enables cross-site requests — needed for OAuth/SSO flows, but opens CSRF vector | Use lax by default. none only when you specifically need cross-site cookies (and always with secure: true) |
maxmemory-policy to allkeys-lru with a maxmemory limit. Sessions should expire and evict old sessions when memory is full.maxAge sets absolute expiry. Implement idle expiry by touching the session on each request (req.session.touch()).req.session.regenerate() and verify the attack no longer works.How do sessions work? The server creates a session record (in DB/Redis/memory) with a random ID. The ID is sent to the browser as a cookie. Subsequent requests include the cookie, and the server looks up the session to identify the user.
What are HttpOnly cookies and why are they important? Cookies with the HttpOnly flag cannot be read by JavaScript (document.cookie). This prevents XSS attacks from stealing session tokens.
What happens when a user logs out? The server deletes the session record. The cookie becomes useless (it references a session that no longer exists). The browser may also be instructed to remove the cookie via res.clearCookie().
Compare sessions and JWTs for a browser-based web application. Sessions: server-side (DB/Redis lookup per request), instant revocation, HttpOnly cookies protect against XSS, but vulnerable to CSRF (mitigated by SameSite cookies). JWTs: stateless (no lookup), better for microservices, but revocation is complex, and client-side storage is risky (localStorage) or loses stateless benefits (HttpOnly cookie — why not just sessions?). For purely browser-based apps, sessions are generally simpler and more secure.
What is session fixation and how do you prevent it? An attacker tricks a victim into authenticating with a known session ID, then uses that ID to hijack the authenticated session. Prevent by: regenerating the session ID after login (req.session.regenerate()), never accepting session IDs from URL parameters, and binding sessions to additional attributes (IP, User-Agent) as secondary checks.
How would you handle sessions in a horizontally scaled application? Use a shared session store (Redis) accessible to all application instances. Configure express-session with connect-redis. All instances read/write to the same Redis cluster. Alternatives: sticky sessions ( routes same user to same server — fragile), or JWTs (stateless — but with the revocation tradeoff).
Sessions are the "boring but correct" choice for browser-based authentication. Store session data server-side (Redis), send only an opaque session ID to the browser (HttpOnly + Secure + SameSite cookie). Regenerate session IDs after login to prevent fixation. Sessions support instant revocation, idle timeouts, and multi-device management. They require a shared store (Redis) for horizontal scaling but are otherwise simpler and more secure than JWTs for browser-based apps.
req.session.regenerate() after login to prevent fixation.sameSite: 'lax' protect against? CSRF attacks — the cookie is not sent on cross-site POST requests.