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 Token Security

Tokens have complex lifecycle rules

Refresh tokens rotate, reused tokens invalidate families, denied access tokens are rejected, expired tokens fail. Each rule needs a test.

Refresh token rotation tests

describe("refresh token rotation", () => {
  it("refresh returns new access and refresh tokens", async () => {
    // Login to get initial tokens
    const loginRes = await request("/token/login", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email: "[email protected]", password: "password123" }),
    });
    const { accessToken, refreshToken } = await loginRes.json();

    // Refresh
    const refreshRes = await request("/token/refresh", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ refreshToken }),
    });
    assert.strictEqual(refreshRes.status, 200);

    const newTokens = await refreshRes.json();
    assert.ok(newTokens.accessToken, "Should return new access token");
    assert.ok(newTokens.refreshToken, "Should return new refresh token");
    assert.notStrictEqual(newTokens.refreshToken, refreshToken, "New refresh token must differ");
  });

  it("old refresh token is invalid after rotation", async () => {
    const loginRes = await request("/token/login", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email: "[email protected]", password: "password123" }),
    });
    const { refreshToken } = await loginRes.json();

    // Use it once (rotation)
    await request("/token/refresh", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ refreshToken }),
    });

    // Try to use the old one again
    const res = await request("/token/refresh", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ refreshToken }),
    });
    assert.strictEqual(res.status, 401, "Old refresh token must be rejected");
  });
});

Reuse detection tests

describe("refresh token reuse detection", () => {
  it("reusing a consumed token invalidates the entire family", async () => {
    const loginRes = await request("/token/login", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email: "[email protected]", password: "password123" }),
    });
    const { refreshToken: token1 } = await loginRes.json();

    // Rotate: token1 → token2
    const refresh1 = await request("/token/refresh", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ refreshToken: token1 }),
    });
    const { refreshToken: token2 } = await refresh1.json();

    // Reuse token1 (the consumed one)
    const reuseRes = await request("/token/refresh", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ refreshToken: token1 }),
    });
    assert.strictEqual(reuseRes.status, 401, "Reused token must be rejected");

    // Token2 should also be invalidated (family revocation)
    const token2Res = await request("/token/refresh", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ refreshToken: token2 }),
    });
    assert.strictEqual(token2Res.status, 401, "Entire family must be invalidated on reuse");
  });
});

This is the most sophisticated token test. It verifies that when a consumed refresh token is reused (indicating theft), the entire token family is invalidated — both the old token and the new one.

Access token deny list tests

describe("access token deny list", () => {
  it("denied token is rejected even if not expired", async () => {
    const loginRes = await request("/token/login", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email: "[email protected]", password: "password123" }),
    });
    const { accessToken } = await loginRes.json();

    // Verify it works
    const beforeRes = await request("/token/me", {
      headers: { authorization: `Bearer ${accessToken}` },
    });
    assert.strictEqual(beforeRes.status, 200);

    // Deny it (via admin action or password change)
    // This depends on your deny mechanism — might need a direct function call
    // denyToken(jti, expiresAt);

    // Verify it is rejected
    // const afterRes = await request("/token/me", {
    //   headers: { authorization: `Bearer ${accessToken}` },
    // });
    // assert.strictEqual(afterRes.status, 401);
  });
});

[!NOTE] Testing the deny list requires either an admin endpoint to deny specific tokens or direct access to the denyToken function. In a real test suite, you would import the function and call it directly, then verify the token fails authentication.

Exercises

Exercise 1: Write the rotation and reuse detection tests. These are the highest-value token tests.

Exercise 2: Test that an expired access token (after the 15-minute TTL) is rejected. You can temporarily set the TTL to 1 second for testing.

Exercise 3: Write a test that verifies two independent logins have separate token families. Reusing a token from login 1 should not affect login 2’s tokens.

Why does the reuse detection test check that token2 is also invalidated?

← Testing Rate Limiting and Lockout Testing Input Handling →

© 2026 hectoday. All rights reserved.