hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Securing Your API with @hectoday/http

The Threat Landscape

  • What Could Go Wrong
  • Project Setup

Brute-Force Protection

  • Rate Limiting Login Attempts
  • Account Lockout
  • Timing Attack Prevention

CSRF Protection

  • What Is CSRF?
  • CSRF Tokens
  • CSRF for API Consumers

Token Hardening

  • Refresh Token Rotation
  • Token Revocation
  • Secure Token Storage

Password Reset

  • The Password Reset Flow
  • Building the Reset Routes
  • Reset Security

Putting It All Together

  • Security Headers
  • Logging and Monitoring
  • Security Checklist
  • Capstone: Hardened Auth API

Project Setup

Starting point

This course builds on the capstone from “Authentication with Hectoday HTTP.” If you completed that course, you have a project with signup, login, logout, sessions, JWTs, and role-based access control.

If you did not complete it, you can still follow along. The project structure is straightforward and we will show the relevant code as we modify it.

Copy or fork the auth course project

Start with the auth course capstone files:

src/
  app.ts              # setup() with routes, hooks
  server.ts           # starts the server
  db.ts               # User type, in-memory store, seed data
  schemas.ts          # Zod schemas
  sessions.ts         # session store
  cookies.ts          # cookie helpers
  jwt.ts              # JWT helpers
  auth.ts             # authenticate, requireAdmin, authenticatedAdmin
  routes/
    auth.ts           # POST /signup, POST /login, POST /logout
    users.ts          # GET /me, GET /users, DELETE /users/:id
    token-auth.ts     # POST /token/login, GET /token/me

If you are starting fresh, install the dependencies:

npm install @hectoday/http zod srvx bcryptjs jose
npm install -D typescript @types/node @types/bcryptjs tsx

Add session expiry

The auth course’s getSession stored sessions without expiry. Let’s add it now. Update src/sessions.ts:

// src/sessions.ts
export interface Session {
  userId: string;
  createdAt: number;
}

const store = new Map<string, Session>();

const MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours

export function createSession(userId: string): string {
  const id = crypto.randomUUID();
  store.set(id, { userId, createdAt: Date.now() });
  return id;
}

export function getSession(id: string): Session | undefined {
  const session = store.get(id);
  if (!session) return undefined;

  if (Date.now() - session.createdAt > MAX_AGE) {
    store.delete(id);
    return undefined;
  }

  return session;
}

export function deleteSession(id: string): void {
  store.delete(id);
}

export function deleteUserSessions(userId: string): void {
  for (const [id, session] of store) {
    if (session.userId === userId) {
      store.delete(id);
    }
  }
}

Two changes from the auth course version: expired sessions are rejected and cleaned up automatically, and deleteUserSessions lets us invalidate all sessions for a user (useful after a password change or account compromise).

Add structured logging

Throughout this course, we will log security events: failed logins, rate limit hits, account lockouts, password resets. A simple console.log works but structured logging makes events searchable and filterable.

Create src/logger.ts:

// src/logger.ts
export function log(event: string, data: Record<string, unknown> = {}): void {
  console.log(
    JSON.stringify({
      timestamp: new Date().toISOString(),
      event,
      ...data,
    }),
  );
}

Usage:

log("login_failed", { email: "[email protected]", reason: "wrong_password", ip: "1.2.3.4" });
// {"timestamp":"2025-01-01T00:00:00.000Z","event":"login_failed","email":"[email protected]","reason":"wrong_password","ip":"1.2.3.4"}

JSON-formatted logs can be parsed by log aggregation tools (Datadog, Grafana, CloudWatch). We will use this log function throughout the course.

Add an IP extraction helper

Several defenses (rate limiting, logging) need the client’s IP address. Create src/ip.ts:

// src/ip.ts
export function getClientIp(request: Request): string {
  // Behind a reverse proxy (most production setups)
  const forwarded = request.headers.get("x-forwarded-for");
  if (forwarded) {
    return forwarded.split(",")[0].trim();
  }

  // Direct connection (development)
  return "unknown";
}

[!WARNING] X-Forwarded-For is set by reverse proxies (Nginx, Cloudflare, load balancers). In production, configure your proxy to set this header and trust only the first value. Without a proxy, this header can be spoofed by the client. For development on localhost, “unknown” is fine.

Update the app hooks

Add request logging to src/app.ts:

import { log } from "./logger.js";
import { getClientIp } from "./ip.js";

export const app = setup({
  onRequest: ({ request }) => ({
    startTime: Date.now(),
    ip: getClientIp(request),
  }),

  routes: [...],

  onResponse: ({ request, response, locals }) => {
    const duration = Date.now() - locals.startTime;
    const url = new URL(request.url);

    log("request", {
      method: request.method,
      path: url.pathname,
      status: response.status,
      duration,
      ip: locals.ip,
    });

    return response;
  },

  onError: ({ error, locals }) => {
    log("unhandled_error", { error: String(error), ip: locals.ip });
    return Response.json({ error: "Internal error" }, { status: 500 });
  },
});

Every request now produces a structured log line. Security events (which we add in later lessons) will stand out in the log stream.

Verify it works

npm run dev

Make a few requests and check the server output. You should see JSON log lines with timestamps, methods, paths, and status codes.

File layout

src/
  app.ts
  server.ts
  db.ts
  schemas.ts
  sessions.ts         # now with expiry and deleteUserSessions
  cookies.ts
  jwt.ts
  auth.ts
  logger.ts           # new — structured logging
  ip.ts               # new — IP extraction
  routes/
    auth.ts
    users.ts
    token-auth.ts

This is our baseline. Every lesson from here adds a defense.

Why do we add a deleteUserSessions function?

Why do we use JSON-formatted log output instead of plain text?

← What Could Go Wrong Rate Limiting Login Attempts →

© 2026 hectoday. All rights reserved.