TL;DR
The #1 session security best practice is regenerating session IDs after authentication to prevent session fixation attacks. Use HttpOnly, Secure, SameSite cookies for session storage. Generate cryptographically random session IDs, regenerate after login, and implement proper session timeout and invalidation. Store sessions server-side with Redis or a database for easy revocation.
"A session is only as secure as its weakest moment. Regenerate on login, timeout on idle, and invalidate on logout."
Best Practice 1: Secure Cookie Settings 5 min
Every session cookie needs these security flags:
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
name: 'sessionId', // Custom name (not "connect.sid")
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // JavaScript cannot access
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
domain: '.example.com', // Specific domain
path: '/',
},
}));
| Cookie Flag | Purpose | Setting |
|---|---|---|
| HttpOnly | Prevents XSS theft | Always true |
| Secure | HTTPS only | Always true in production |
| SameSite | CSRF protection | "lax" or "strict" |
| Domain | Cookie scope | Specific domain, not broad |
| Path | URL scope | "/" or specific path |
Best Practice 2: Regenerate Session on Auth Changes 3 min
Prevent session fixation attacks by regenerating the session ID:
// CRITICAL: Regenerate session after login
async function login(req, email, password) {
const user = await authenticateUser(email, password);
if (user) {
// Regenerate session ID to prevent fixation
req.session.regenerate((err) => {
if (err) throw err;
// Store user info in new session
req.session.userId = user.id;
req.session.role = user.role;
req.session.loginTime = Date.now();
req.session.save((err) => {
if (err) throw err;
return user;
});
});
}
}
// Also regenerate after privilege changes
async function elevatePrivileges(req, newRole) {
req.session.regenerate((err) => {
if (err) throw err;
req.session.role = newRole;
req.session.save();
});
}
Best Practice 3: Session Timeout Strategy 5 min
Implement both idle and absolute timeouts:
const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 minutes
const ABSOLUTE_TIMEOUT = 8 * 60 * 60 * 1000; // 8 hours
function sessionTimeoutMiddleware(req, res, next) {
if (!req.session.userId) {
return next();
}
const now = Date.now();
// Check absolute timeout (since login)
if (now - req.session.loginTime > ABSOLUTE_TIMEOUT) {
return req.session.destroy(() => {
res.status(401).json({ error: 'Session expired' });
});
}
// Check idle timeout (since last activity)
if (req.session.lastActivity &&
now - req.session.lastActivity > IDLE_TIMEOUT) {
return req.session.destroy(() => {
res.status(401).json({ error: 'Session expired due to inactivity' });
});
}
// Update last activity
req.session.lastActivity = now;
next();
}
app.use(sessionTimeoutMiddleware);
Best Practice 4: Server-Side Session Storage 10 min
Store sessions in Redis or a database for control:
| Storage | Pros | Cons | Best For |
|---|---|---|---|
| Redis | Fast, TTL support | Extra infrastructure | Most applications |
| Database | No extra deps | Slower, needs cleanup | Simple apps |
| Memory | Simple | Lost on restart, no scale | Development only |
| JWT (client) | Stateless | Cannot revoke easily | API-only services |
// Prisma session store example
import { PrismaSessionStore } from '@quixo3/prisma-session-store';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
app.use(session({
store: new PrismaSessionStore(prisma, {
checkPeriod: 2 * 60 * 1000, // Cleanup every 2 min
dbRecordIdIsSessionId: true,
}),
// ... other options
}));
// Session schema in Prisma
// model Session {
// id String @id
// sid String @unique
// data String
// expiresAt DateTime
// }
Best Practice 5: Proper Session Invalidation 5 min
Clear sessions completely on logout:
async function logout(req, res) {
const userId = req.session.userId;
// Destroy the session
req.session.destroy((err) => {
if (err) {
console.error('Session destruction failed:', err);
}
// Clear the cookie
res.clearCookie('sessionId', {
httpOnly: true,
secure: true,
sameSite: 'lax',
domain: '.example.com',
path: '/',
});
res.json({ message: 'Logged out' });
});
}
// Logout from all devices (invalidate all sessions)
async function logoutAll(req, res) {
const userId = req.session.userId;
// With Redis: delete all sessions for user
const keys = await redisClient.keys(`sess:*`);
for (const key of keys) {
const session = await redisClient.get(key);
if (session && JSON.parse(session).userId === userId) {
await redisClient.del(key);
}
}
res.json({ message: 'Logged out from all devices' });
}
Best Practice 6: Session Security Checks 10 min
Add additional validation for sensitive operations:
// Bind session to browser fingerprint
function createSession(req, user) {
req.session.userId = user.id;
req.session.fingerprint = hashFingerprint(
req.ip,
req.headers['user-agent']
);
}
// Validate on each request
function validateSession(req, res, next) {
if (!req.session.userId) {
return next();
}
const currentFingerprint = hashFingerprint(
req.ip,
req.headers['user-agent']
);
// Strict mode: reject if fingerprint changes
if (req.session.fingerprint !== currentFingerprint) {
logger.warn('session.fingerprint_mismatch', {
userId: req.session.userId,
expected: req.session.fingerprint,
actual: currentFingerprint,
});
return req.session.destroy(() => {
res.status(401).json({ error: 'Session invalid' });
});
}
next();
}
// Re-authenticate for sensitive actions
async function requireRecentAuth(req, res, next) {
const AUTH_FRESHNESS = 5 * 60 * 1000; // 5 minutes
if (Date.now() - req.session.loginTime > AUTH_FRESHNESS) {
return res.status(403).json({
error: 'Please re-authenticate',
code: 'REAUTH_REQUIRED',
});
}
next();
}
Official Resources: For comprehensive session management guidance, see OWASP Session Management Cheat Sheet and OWASP Session Fixation Attack.
Should I use sessions or JWTs?
Sessions are easier to secure and revoke. Use JWTs for stateless APIs where you need to scale horizontally without shared storage. For web apps with user logins, sessions in HttpOnly cookies are typically safer.
What is session fixation?
An attack where the attacker sets a known session ID before the user logs in. After login, the attacker uses that same session ID to hijack the session. Prevent it by regenerating the session ID after login.
::faq-item{question="How do I handle "remember me" functionality?"} Use a separate persistent token stored in a cookie with longer expiry. When the user returns, validate the persistent token and create a new session. Store persistent tokens hashed in the database and rotate them on each use. ::