CodingNic

Session-Based Authentication

Login, Logout, and Protected Routes

Session-Based Authentication 20 min read

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

text
session-auth-demo/
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ app.js
โ””โ”€โ”€ test.js

The Login Endpoint

javascript
// 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

javascript
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

javascript
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:

javascript
// 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);
})();
text
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

javascript
const badLogin = await request(app).post('/api/v1/auth/login').send({ email: 'erin@example.com', password: 'wrong' });
console.log(badLogin.status, badLogin.body);
text
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

  1. Build this login, logout, and protected-route flow, and confirm the exact sequence of statuses shown above.
  2. Confirm GET /api/v1/me without ever logging in returns 401.
  3. Confirm a wrong password doesn’t create a session, by attempting GET /api/v1/me afterward with the same (failed) client.
  4. Add a second protected route, GET /api/v1/dashboard, reusing the same requireAuth middleware, 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.
  • requireAuth middleware checks req.session.userId before 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.