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.
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.
$ 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 keptThe 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.