Sessions

`express-session` Plus a Real Session Store

Sessions

Build a session-based auth flow. Cookies in, sessions in Redis, user attached to req.

4 min read Level 2/5 #express#sessions#auth
What you'll learn
  • Configure express-session
  • Use a Redis store
  • Implement login / logout

express-session is the classic middleware. Store sessions in Redis (or another shared store), put the session ID in a cookie.

Install

npm install express-session connect-redis ioredis

Configure

import express from "express";
import session from "express-session";
import { RedisStore } from "connect-redis";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL);

app.use(session({
  store:  new RedisStore({ client: redis }),
  secret: process.env.SESSION_SECRET,    // never commit
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure:   process.env.NODE_ENV === "production",   // HTTPS only in prod
    sameSite: "lax",
    maxAge:   24 * 60 * 60 * 1000,
  },
}));

That’s the entire auth infrastructure.

Login

import bcrypt from "bcrypt";

app.post("/auth/login", validate({ body: LoginSchema }), async (req, res) => {
  const { email, password } = req.validBody;

  const user = await db.users.findByEmail(email);
  if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
    return res.status(401).json({ error: { code: "invalid_credentials" } });
  }

  // store user id on the session — that's the whole login
  req.session.userId = user.id;

  res.json({ data: { id: user.id, email: user.email } });
});

Auth Middleware

export async function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: { code: "not_authenticated" } });
  }
  req.user = await db.users.findById(req.session.userId);
  if (!req.user) {
    req.session.destroy(() => {});
    return res.status(401).json({ error: { code: "not_authenticated" } });
  }
  next();
}

app.get("/me", requireAuth, (req, res) => {
  res.json({ data: req.user });
});

Logout

app.post("/auth/logout", (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).end();
    res.clearCookie("connect.sid");   // default cookie name
    res.status(204).end();
  });
});

The three cookie flags everyone gets wrong:

  • httpOnly: true — JS can’t read it (blocks XSS exfiltration)
  • secure: true — only sent over HTTPS (always on in production)
  • sameSite: "lax" — sent on same-site GETs; blocks most CSRF

In production: all three. In development with HTTPS off: secure: false (or you’ll spend hours wondering why the cookie won’t stick).

Store

MemoryStore (the default) is dev-only — sessions die when the process restarts. Always use a real store in production: Redis, Postgres, MongoDB. connect-redis is by far the most popular.

Session vs JWT — When To Pick This

Sessions shine when:

  • You’re already using Redis
  • Revocation matters (banning a user, password change, logout-all-devices)
  • You want simple, well-understood crypto
Cookie Auth Details →