CodingNic

Security Hardening

Exercises

Security Hardening 25 min read

Exercises

Objectives

By the end of this lesson, you should be able to:

  • Combine rate limiting and environment-loaded secrets on a single login route
  • Confirm, end to end, that a scripted attack fails against the hardened route
  • Apply a second, differently tuned limiter to a different endpoint

⚠️ A note on verification: every command and every output in this lesson was actually run, with real HTTP requests against a real Express app.

Exercise: A Fully Hardened Auth Flow

a) Set up .env:

text
JWT_SECRET=a-long-randomly-generated-string-not-committed-to-git

b) Build registration and login routes, each with its own rate limiter:

javascript
require('dotenv').config();
const rateLimit = require('express-rate-limit');

const registerLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'TooManyRequests', message: 'Too many registration attempts, try again later' }
});

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 3,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'TooManyRequests', message: 'Too many login attempts, try again later' }
});

app.post('/api/v1/auth/register', registerLimiter, async (req, res) => {
  const { email, password } = req.body;
  const passwordHash = await bcrypt.hash(password, 10);
  users.push({ id: users.length + 1, email, passwordHash });
  res.status(201).json({ email });
});

app.post('/api/v1/auth/login', loginLimiter, async (req, res) => {
  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' });
  }
  const token = jwt.sign({ sub: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
  res.json({ token });
});

Registration and login each get their own limiter, with their own max, registration allows a few more attempts since mistyping a new password is a more common accident than a real attack, login stays tighter, since it’s the more common attack target. The JWT is signed with process.env.JWT_SECRET, loaded from .env, never a hardcoded string.

c) Attack the login route, five attempts, the correct password last:

text
Attempt 1: "wrong1" -> 401 {"error":"InvalidCredentials","message":"Incorrect email or password"}
Attempt 2: "wrong2" -> 401 {"error":"InvalidCredentials","message":"Incorrect email or password"}
Attempt 3: "wrong3" -> 401 {"error":"InvalidCredentials","message":"Incorrect email or password"}
Attempt 4: "wrong4" -> 429 {"error":"TooManyRequests","message":"Too many login attempts, try again later"}
Attempt 5: "sunshine123" -> 429 {"error":"TooManyRequests","message":"Too many login attempts, try again later"}

Same result as the last lesson, the correct password on attempt 5 never even reaches the bcrypt comparison, blocked by the limiter first. Layering rate limiting on top of hashing, correct credential logic, and an environment-loaded signing secret is what actually stops this attack in a real application.

d) Extend it. Add a third limiter to a password-reset-request endpoint (Module 2’s change-password route, or a new one), reasoning through what max and windowMs make sense for that specific endpoint, and explain your choice in one or two sentences.

e) Explain the layers. In your own words, write one sentence for each of the following, explaining what would go wrong for an attacker attempting the brute-force attack from Lesson 1 if only that single layer, and none of the others, were in place: hashing alone, rate limiting alone, environment-loaded secrets alone.

Recap

This module added the layer that a correct authentication and authorization system still needs against real-world attacks, rate limiting to stop automated password guessing, and secrets management to keep signing keys out of source code, both composing with everything built in Modules 1 through 5 into genuine defense in depth.

Next module: testing fundamentals with Jest, writing automated tests instead of manually running scripts to verify behavior.