express-session

Server-Side Sessions with express-session

express-session 6,354 (1.19.0) keeps the data on the server and sends only an identifier. It generates a random ID, signs it into a cookie, and hands your handlers a req.session object that it saves at the end of the response if anything changed.

Session middleware, a login that regenerates, and logoutJavaScript
app.set('trust proxy', 1);          // so `secure` works behind nginx (Section 3.3.2)
app.use(session({
  name: 'sid',                             // not the default 'connect.sid'
  secret: process.env.SESSION_SECRET,
  resave: false,                           // do not rewrite unchanged sessions
  saveUninitialized: false,                // no cookie until you store something
  rolling: true,                           // maxAge becomes an idle timeout
  cookie: { httpOnly: true, sameSite: 'lax', maxAge: 30 * 60 * 1000,
            secure: process.env.NODE_ENV === 'production' }
}));
app.post('/login', async (req, res) => {       // argon2 setup: see Section 3.6.5
  const user = users.get(req.body.email);
  if (!user || !await argon2.verify(user.hash, req.body.password)) {
    return res.status(401).json({ error: 'invalid_credentials' });
  }
  req.session.regenerate(() => {           // new ID, old data discarded
    Object.assign(req.session, { userId: user.id, roles: user.roles });
    res.json({ ok: true, user: user.email });
  });
});
const requireSession = (req, res, next) => req.session.userId
  ? next() : res.status(401).json({ error: 'not_authenticated' });
app.get('/me', requireSession, (req, res) =>
  res.json({ id: req.session.userId, roles: req.session.roles, sid: req.sessionID }));
app.post('/logout', (req, res) => req.session.destroy(() =>   // delete, then clear
  res.clearCookie('sid', { httpOnly: true, sameSite: 'lax' }).json({ ok: true })));

Three of those booleans are defaults that are wrong for real apps. resave: true rewrites every session on every request, multiplying store traffic and creating lost-update races; saveUninitialized: true mints a session for every anonymous visitor; rolling: true makes maxAge an idle timeout rather than absolute. req.session.regenerate is the critical call: without it the ID that existed before the login survives it, and an attacker who planted a known ID in the victim's browser is signed in as the victim — session fixation.

Output of 37
$ curl -i -c jar -d '{"email":"ada@example.com","password":"correct horse battery"}' .../login
HTTP/1.1 200 OK    {"ok":true,"user":"ada@example.com"}
Set-Cookie: sid=s%3AF1azbYwLh46mpETtD9wqoOjIJgNzY77-.hxB3QBSzIb47gbYgwRLOJHRaDhJxwz90DdqiSn58gK
  8;
            Path=/; Expires=Thu, 17 Sep 2026 17:54:36 GMT; HttpOnly; SameSite=Lax
$ curl -b jar .../me  ->  {"id":"u_1","roles":["author"],"sid":"F1azbYwLh46mpETtD9wqoOjIJgNzY77
  -"}
$ curl -i -d '{"email":"ada@example.com","password":"hunter2"}' .../login   # wrong password
HTTP/1.1 401 Unauthorized  {"error":"invalid_credentials"}
$ curl -b jar -X POST .../logout   ->   {"ok":true}
$ curl -i -b jar .../me   ->   HTTP/1.1 401  {"error":"not_authenticated"}   # cookie kept

The cookie value is the signed form of req.sessionID: 24 random bytes in base64url, a dot, then the HMAC. The last request proves the data was deleted server-side, not merely forgotten by the client.