hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Testing Auth and Security with @hectoday/http

Why Auth Tests Are Different

  • Testing Security, Not Just Functionality
  • Project Setup

Testing Authentication

  • Testing Login Flows
  • Testing Sessions and Cookies
  • Testing 2FA Flows

Testing Authorization

  • Testing Access Boundaries
  • Testing API Keys and Scopes

Testing Security Properties

  • Testing Rate Limiting and Lockout
  • Testing Token Security
  • Testing Input Handling

Putting It Together

  • A Security Test Suite

Testing 2FA Flows

2FA adds complexity to testing

The login flow splits into two steps when 2FA is enabled. Tests must verify both steps and the security properties of the intermediate state.

Testing the two-step flow

import { TOTP } from "otpauth";

// Helper: generate a valid TOTP code for testing
function generateTestCode(secret: string): string {
  const totp = new TOTP({ secret, algorithm: "SHA1", digits: 6, period: 30 });
  return totp.generate();
}

describe("2FA login flow", () => {
  // Assume alice has 2FA enabled with a known secret
  const ALICE_TOTP_SECRET = "TESTSECRETFORTESTING"; // Set in test seed

  it("password step returns requiresTwoFactor", async () => {
    const res = await login("[email protected]", "password123");
    assert.strictEqual(res.status, 200);
    const body = await res.json();
    assert.strictEqual(body.requiresTwoFactor, true);
  });

  it("pending session cannot access protected routes", async () => {
    const res = await login("[email protected]", "password123");
    const cookie = getCookie(res);

    const protectedRes = await authenticatedRequest("/me", cookie);
    assert.strictEqual(
      protectedRes.status,
      401,
      "Pending session must not access protected routes",
    );
  });

  it("valid TOTP code completes login", async () => {
    const loginRes = await login("[email protected]", "password123");
    const cookie = getCookie(loginRes);

    const code = generateTestCode(ALICE_TOTP_SECRET);
    const res = await authenticatedRequest("/login/2fa", cookie, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ code }),
    });

    assert.strictEqual(res.status, 200);

    // Session should now be active
    const meRes = await authenticatedRequest("/me", cookie);
    assert.strictEqual(meRes.status, 200);
  });

  it("invalid TOTP code returns 401", async () => {
    const loginRes = await login("[email protected]", "password123");
    const cookie = getCookie(loginRes);

    const res = await authenticatedRequest("/login/2fa", cookie, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ code: "000000" }),
    });

    assert.strictEqual(res.status, 401);

    // Session should still be pending
    const meRes = await authenticatedRequest("/me", cookie);
    assert.strictEqual(meRes.status, 401);
  });

  it("2FA endpoint without pending session returns 401", async () => {
    const res = await request("/login/2fa", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ code: "123456" }),
    });
    assert.strictEqual(res.status, 401);
  });
});

The key test is “pending session cannot access protected routes.” This verifies that the empty-userId design from the 2FA course actually blocks access.

Testing recovery codes

describe("recovery codes", () => {
  it("valid recovery code completes login", async () => {
    const loginRes = await login("[email protected]", "password123");
    const cookie = getCookie(loginRes);

    // Use a known recovery code from test seed
    const res = await authenticatedRequest("/login/2fa", cookie, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ recoveryCode: "test-code-1" }),
    });

    assert.strictEqual(res.status, 200);
  });

  it("recovery code works only once", async () => {
    // First use
    const loginRes1 = await login("[email protected]", "password123");
    const cookie1 = getCookie(loginRes1);
    await authenticatedRequest("/login/2fa", cookie1, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ recoveryCode: "test-code-2" }),
    });

    // Second use (same code)
    const loginRes2 = await login("[email protected]", "password123");
    const cookie2 = getCookie(loginRes2);
    const res = await authenticatedRequest("/login/2fa", cookie2, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ recoveryCode: "test-code-2" }),
    });

    assert.strictEqual(res.status, 401, "Recovery code must be single-use");
  });
});

The single-use test is critical. Without it, a captured recovery code could be used forever.

Exercises

Exercise 1: Write the 2FA flow tests. You will need to seed a user with a known TOTP secret in your test setup.

Exercise 2: Write a test that verifies the TOTP code is time-sensitive: generate a code, advance the clock (or wait), and verify it expires.

Exercise 3: Write a test for the 2FA setup flow: call /me/2fa/setup, extract the secret, generate a code, call /me/2fa/verify. Verify the user now requires 2FA on login.

Why is the 'pending session cannot access protected routes' test the most important 2FA test?

← Testing Sessions and Cookies Testing Access Boundaries →

© 2026 hectoday. All rights reserved.