Sessions¶
Session management in Express is not built-in. You bring express-session , configure a store , and hope you don't leave session data leaking everywhere
Sessions let you store user data across requests without sending it all in a cookie. The cookie just carries a session ID. The actual data lives on the server. But the devil is in the store - memory store dies on restart , Redis is fast but needs infrastructure , MongoDB is convenient but slower. Pick wrong and your session layer becomes your bottleneck
express-session¶
npm install express-session
const session = require('express-session')
app.use(session({
secret: 'your-secret-key', // used to sign the session ID cookie
resave: false, // don't save session if unmodified
saveUninitialized: false, // don't create session until something stored
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}))
The secret is used to sign the session ID cookie. Multiple secrets can be passed as an array - first is used for signing , rest for verification (enables rotation)
session usage¶
// storing session data
app.post('/login' , async (req , res) => {
const user = await authenticate(req.body.email , req.body.password)
req.session.userId = user.id
req.session.role = user.role
req.session.loggedInAt = Date.now()
res.json({ success: true })
})
// reading session data
app.get('/profile' , (req , res) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Not authenticated' })
}
res.json({
userId: req.session.userId,
role: req.session.role
})
})
// destroying session (logout)
app.post('/logout' , (req , res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: 'Logout failed' })
res.clearCookie('connect.sid') // clear the session cookie
res.json({ success: true })
})
})
session stores - don't use the default¶
The default memory store (MemoryStore) leaks memory and doesn't scale
# NEVER use MemoryStore in production
# It leaks memory , doesn't persist across restarts , doesn't scale
connect-redis¶
npm install connect-redis redis
const RedisStore = require('connect-redis').default
const { createClient } = require('redis')
const redisClient = createClient({ url: process.env.REDIS_URL })
redisClient.connect().catch(console.error)
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true , secure: true , maxAge: 86400000 }
}))
Redis is the gold standard for session storage. Fast , persistent , shared across instances
connect-mongo¶
npm install connect-mongo
const MongoStore = require('connect-mongo')
app.use(session({
store: MongoStore.create({
mongoUrl: process.env.MONGO_URI,
ttl: 24 * 60 * 60, // 24 hours in seconds
autoRemove: 'native' // let MongoDB TTL index clean up
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true , secure: true , maxAge: 86400000 }
}))
session security¶
app.use(session({
// SESSION SECURITY CHECKLIST:
name: 'connect.sid', // default - change it to avoid fingerprinting
secret: process.env.SESSION_SECRET, // from env , never hardcoded
resave: false, // avoid unnecessary writes
saveUninitialized: false, // don't create sessions for unauthenticated users
rolling: true, // reset maxAge on each response
cookie: {
httpOnly: true, // prevent XSS access
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hour expiry
path: '/' // scope
}
}))
session regeneration¶
After login , regenerate the session ID to prevent session fixation:
app.post('/login' , async (req , res) => {
const user = await authenticate(req.body.email , req.body.password)
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' })
}
// regenerate session after login
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' })
// now store data in the new session
req.session.userId = user.id
req.session.role = user.role
res.json({ success: true })
})
})
Also regenerate on privilege escalation:
app.post('/elevate' , async (req , res) => {
// after verifying user can be admin
req.session.regenerate((err) => {
req.session.userId = user.id
req.session.role = 'admin'
res.json({ success: true })
})
})
session fingerprinting¶
Bind sessions to client characteristics to detect theft:
// after login
req.session.userId = user.id
req.session.fingerprint = {
ip: req.ip,
userAgent: req.headers['user-agent']
}
// middleware to verify
const fingerprintCheck = (req , res , next) => {
if (!req.session.fingerprint) return next()
if (req.session.fingerprint.ip !== req.ip) {
// possible session hijacking - destroy and re-authenticate
console.warn('Session fingerprint mismatch - possible hijacking')
req.session.destroy()
return res.status(401).json({ error: 'Session expired' })
}
next()
}
session timeout and cleanup¶
// idle timeout
app.use((req , res , next) => {
if (req.session.lastAccess) {
const idleTime = Date.now() - req.session.lastAccess
if (idleTime > 30 * 60 * 1000) { // 30 minutes idle
req.session.destroy()
return res.status(401).json({ error: 'Session expired due to inactivity' })
}
}
req.session.lastAccess = Date.now()
next()
})
prerequisites¶
express_09_cookies.md - cookie-parser , signed cookies , cookie flags
next → express_11_auth.md