Skip to content

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