Login, Logout, and Protected Routes
Objectives
By the end of this lesson, you should be able to:
- Build a login endpoint that creates a session on success
- Build a logout endpoint that destroys it
- Write middleware that protects a route, requiring a valid session
๐ก Why this matters: This lesson connects everything so far, Module 1’s password verification and this module’s session setup, into the complete flow: login, a protected request, and logout.
โ ๏ธ A note on verification: every snippet and every output in this lesson was actually run, with real HTTP requests against a real Express app, including real session cookies.
Project File Structure
session-auth-demo/
โโโ package.json
โโโ app.js
โโโ test.js
The Login Endpoint
// app.js
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcryptjs');
const app = express();
app.use(express.json());
app.use(session({
secret: 'dev-only-secret',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 1000 * 60 * 60 }
}));
const users = [];
let nextId = 1;
(async () => {
users.push({ id: nextId++, email: 'erin@example.com', passwordHash: await bcrypt.hash('sunshine123', 10) });
})();
app.post('/api/v1/auth/login', async (req, res, next) => {
try {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'InvalidCredentials', message: 'Incorrect email or password' });
}
req.session.userId = user.id;
res.json({ id: user.id, email: user.email });
} catch (err) {
next(err);
}
});
On success, req.session.userId = user.id is the entire authentication step, from this point on, every request carrying this session’s cookie is treated as this user.
The Logout Endpoint
app.post('/api/v1/auth/logout', (req, res) => {
req.session.destroy(() => {
res.status(204).send();
});
});
req.session.destroy() removes the session entirely, the cookie the browser still holds now points at nothing, any future request with it is treated as logged out.
Protecting a Route
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Unauthorized', message: 'Login required' });
}
next();
}
app.get('/api/v1/me', requireAuth, (req, res) => {
const user = users.find(u => u.id === req.session.userId);
res.json({ id: user.id, email: user.email });
});
module.exports = app;
requireAuth is ordinary Express middleware (Node.js & Express Foundations, Module 7), checking req.session.userId and rejecting the request before it ever reaches the route handler if it’s missing.
Testing the Full Flow
Supertest’s request.agent() persists cookies across requests, exactly like a real browser, needed here since a session depends on the cookie set at login carrying forward:
// test.js
const request = require('supertest');
const app = require('./app');
(async () => {
const agent = request.agent(app);
const meBefore = await agent.get('/api/v1/me');
console.log('Before login:', meBefore.status, meBefore.body);
const login = await agent.post('/api/v1/auth/login').send({ email: 'erin@example.com', password: 'sunshine123' });
console.log('Login:', login.status, login.body);
const meAfter = await agent.get('/api/v1/me');
console.log('After login:', meAfter.status, meAfter.body);
await agent.post('/api/v1/auth/logout');
const meAfterLogout = await agent.get('/api/v1/me');
console.log('After logout:', meAfterLogout.status, meAfterLogout.body);
})();
Before login: 401 { error: 'Unauthorized', message: 'Login required' }
Login: 200 { id: 1, email: 'erin@example.com' }
After login: 200 { id: 1, email: 'erin@example.com' }
After logout: 401 { error: 'Unauthorized', message: 'Login required' }
The exact same route, GET /api/v1/me, behaves differently at each point, rejected before login, allowed after, rejected again after logout, purely based on the session tied to the request’s cookie.
A Failed Login Attempt
const badLogin = await request(app).post('/api/v1/auth/login').send({ email: 'erin@example.com', password: 'wrong' });
console.log(badLogin.status, badLogin.body);
401 { error: 'InvalidCredentials', message: 'Incorrect email or password' }
No session is created on a failed login, req.session.userId is simply never set.
Try It
- Build this login, logout, and protected-route flow, and confirm the exact sequence of statuses shown above.
- Confirm
GET /api/v1/mewithout ever logging in returns401. - Confirm a wrong password doesn’t create a session, by attempting
GET /api/v1/meafterward with the same (failed) client. - Add a second protected route,
GET /api/v1/dashboard, reusing the samerequireAuthmiddleware, and confirm it behaves identically to/api/v1/me.
Recap
- Login verifies credentials (Module 1), then sets
req.session.userId, creating an authenticated session. - Logout calls
req.session.destroy(), invalidating the session the cookie still points to. requireAuthmiddleware checksreq.session.userIdbefore allowing a request through, the same pattern any number of protected routes can reuse.
This is the final lesson of this module before exercises. Next module: JSON Web Tokens, a stateless alternative to the session-based approach built here.