Authentication with Sessions and JWT

advanced
18 min

Authentication with Sessions and JWT

Most real applications need to know who is making a request. This lesson covers the two dominant approaches in the Express world: server-side sessions with cookies, and stateless JSON Web Tokens (JWT). You will learn how each works, how to implement both, and how to choose between them. One rule applies everywhere: passwords are always hashed, never stored in plain text.

Hashing Passwords First

Whatever strategy you pick, registration looks the same. Hash the password with bcrypt and store only the hash:

bash
npm install bcrypt
js
const bcrypt = require('bcrypt'); app.post('/api/register', async (req, res) => { const { email, password } = req.body; if (!email || !password || password.length < 8) { return res.status(400).json({ error: 'Email and 8+ char password required' }); } const passwordHash = await bcrypt.hash(password, 12); const user = await User.create({ email, passwordHash }); res.status(201).json({ id: user._id, email: user.email }); });

bcrypt is deliberately slow, which is exactly what you want against brute-force attacks. Verify at login with bcrypt.compare(password, user.passwordHash).

Option 1: Sessions and Cookies

With sessions, the server remembers logged-in users. On login you store the user ID in a session; the browser gets a signed cookie holding just the session ID:

bash
npm install express-session
js
const session = require('express-session'); app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: true, maxAge: 86400000 }, })); app.post('/api/login', async (req, res) => { const user = await User.findOne({ email: req.body.email }); const ok = user && await bcrypt.compare(req.body.password, user.passwordHash); if (!ok) return res.status(401).json({ error: 'Invalid credentials' }); req.session.userId = user._id; res.json({ message: 'Logged in' }); });

Protect routes with a tiny guard that checks req.session.userId and returns 401 otherwise. Logout is req.session.destroy(). In production, store sessions in Redis or MongoDB instead of the default memory store, which leaks memory and empties on restart.

Option 2: JSON Web Tokens

JWTs flip the model: the server remembers nothing. On login it signs a token containing the user ID; the client sends it back in the Authorization header of every request:

bash
npm install jsonwebtoken
js
const jwt = require('jsonwebtoken'); app.post('/api/login', async (req, res) => { const user = await User.findOne({ email: req.body.email }); const ok = user && await bcrypt.compare(req.body.password, user.passwordHash); if (!ok) return res.status(401).json({ error: 'Invalid credentials' }); const token = jwt.sign({ sub: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' }); res.json({ token }); }); function requireAuth(req, res, next) { const header = req.headers.authorization || ''; const token = header.startsWith('Bearer ') ? header.slice(7) : null; if (!token) return res.status(401).json({ error: 'Token required' }); try { req.user = jwt.verify(token, process.env.JWT_SECRET); next(); } catch { res.status(401).json({ error: 'Invalid or expired token' }); } } app.get('/api/profile', requireAuth, async (req, res) => { res.json({ userId: req.user.sub }); });

Anyone can decode a JWT payload, so never put secrets in it; the signature only guarantees it was not modified. Keep lifetimes short and pair access tokens with refresh tokens for longer sessions.

Choosing Between Them

Sessions suit server-rendered sites: revocation is instant (delete the session) and cookies with httpOnly resist token theft via XSS. JWTs suit APIs consumed by mobile apps, SPAs, and microservices, because any server holding the secret can verify requests without shared session storage. Many production systems combine both ideas: short-lived JWTs delivered in httpOnly cookies.

Key Takeaways

  • Always hash passwords with bcrypt; compare at login, never store plain text.
  • Sessions keep state on the server and fit cookie-based, server-rendered apps.
  • JWTs are stateless, verified by signature, and fit APIs and mobile clients.
  • Store secrets in environment variables and set httpOnly, secure cookies.
  • Short token lifetimes limit the damage of a leaked JWT.

Final lesson: Deploying an Express App and Best Practices, where your project goes live.

Authentication with Sessions and JWT - Express.js | CodeYourCraft | CodeYourCraft