Sessions & Cookies
Once a user logs in, how does the next request know it's still them? The oldest, most battle-tested answer is the server-side session: the server keeps a record of who's logged in and hands the browser an opaque cookie that points to it. The cookie travels automatically on every request; the server looks up the session and recovers the user. This page covers how sessions work, the all-important secure cookie flags, where to store sessions, and how this model compares to stateless JWTs.
How a session works
LOGIN
client → POST /login (credentials)
server verifies, creates session { userId: 42 }, stores it,
sends Set-Cookie: sid=<random-id>; HttpOnly; Secure
↑ the cookie holds only an opaque ID, NOT the user data
EVERY LATER REQUEST
client → GET /profile Cookie: sid=<random-id> (sent automatically)
server looks up session by sid → { userId: 42 } → loads user → req.userexpress-session
npm install express-session connect-redis redis
import session from 'express-session'
import { RedisStore } from 'connect-redis'
import { createClient } from 'redis'
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
app.use(session({
store: new RedisStore({ client: redis }), // NOT the default MemoryStore
secret: process.env.SESSION_SECRET, // signs the cookie
resave: false,
saveUninitialized: false, // don't store empty sessions
cookie: {
httpOnly: true, // JS can't read it → blocks XSS theft
secure: process.env.NODE_ENV === 'production', // HTTPS-only
sameSite: 'lax', // CSRF mitigation
maxAge: 1000 * 60 * 60 * 24, // 24h
},
}))
// Log in: store the user id on the session
app.post('/login', async (req, res) => {
/* ...verify password... */
req.session.userId = user.id // express-session persists it + sets cookie
res.json({ ok: true })
})
// Log out: destroy the server-side session
app.post('/logout', (req, res) => {
req.session.destroy(() => res.json({ ok: true }))
})The secure cookie flags — get these right
Flag | What it does | Why it matters |
|---|---|---|
| Hides cookie from | An XSS script can’t steal the session |
| Sent only over HTTPS | Prevents interception on plain HTTP |
| Limits cross-site sending | Mitigates CSRF |
| Sets cookie lifetime | Bounds how long a stolen cookie lives |
| Scopes where it’s sent | Least privilege — don’t over-share |
Never use the default in-memory store
Session security beyond the flags
Regenerate the session ID on login (
req.session.regenerate) to prevent session fixation.Destroy the session on logout and on password change — don’t just clear the cookie.
Set an idle timeout and an absolute timeout so abandoned sessions expire.
Use a long, random
secretfrom the environment; rotate it carefully.Store as little as possible in the session — just an id; load fresh user data per request.
Sessions vs tokens
Server-side session | Stateless JWT | |
|---|---|---|
State | Stored on server | Self-contained in the token |
Revocation | Easy — delete the record | Hard — valid until expiry |
Scaling | Needs shared store (Redis) | No lookup needed |
Best fit | Web apps, first-party | APIs, mobile, microservices |