feat: implement comprehensive CSRF protection

This commit is contained in:
2025-07-11 18:06:51 +02:00
committed by Kaj Kowalski
parent e7818f5e4f
commit 3e9e75e854
44 changed files with 14964 additions and 6413 deletions

View File

@ -0,0 +1,324 @@
/**
* CSRF Hooks Tests
*
* Tests for React hooks that manage CSRF tokens on the client side.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, waitFor } from "@testing-library/react";
import { useCSRF, useCSRFFetch, useCSRFForm } from "../../lib/hooks/useCSRF";
// Mock fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;
// Mock document.cookie
Object.defineProperty(document, "cookie", {
writable: true,
value: "",
});
describe("CSRF Hooks", () => {
beforeEach(() => {
vi.clearAllMocks();
document.cookie = "";
});
afterEach(() => {
vi.resetAllMocks();
});
describe("useCSRF", () => {
it("should initialize with loading state", () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true, token: "test-token" }),
});
const { result } = renderHook(() => useCSRF());
expect(result.current.loading).toBe(true);
expect(result.current.token).toBeNull();
expect(result.current.error).toBeNull();
});
it("should fetch token on mount when no cookie exists", async () => {
const mockToken = "test-csrf-token";
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true, token: mockToken }),
});
const { result } = renderHook(() => useCSRF());
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(mockFetch).toHaveBeenCalledWith("/api/csrf-token", {
method: "GET",
credentials: "include",
});
expect(result.current.token).toBe(mockToken);
expect(result.current.error).toBeNull();
});
it("should use existing token from cookies", async () => {
const existingToken = "existing-csrf-token";
document.cookie = `csrf-token=${existingToken}`;
// Mock fetch to ensure it's not called when token exists
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true, token: "should-not-be-used" }),
});
const { result } = renderHook(() => useCSRF());
await waitFor(() => {
expect(result.current.token).toBe(existingToken);
});
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
// Should not fetch from server if cookie exists
expect(mockFetch).not.toHaveBeenCalled();
});
it("should handle fetch errors", async () => {
mockFetch.mockRejectedValueOnce(new Error("Network error"));
const { result } = renderHook(() => useCSRF());
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.error).toBeTruthy();
expect(result.current.token).toBeNull();
});
it("should handle invalid response", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: false }),
});
const { result } = renderHook(() => useCSRF());
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.error).toBeTruthy();
expect(result.current.token).toBeNull();
});
it("should refresh token manually", async () => {
const newToken = "refreshed-csrf-token";
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true, token: "initial-token" }),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true, token: newToken }),
});
const { result } = renderHook(() => useCSRF());
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
await result.current.refreshToken();
await waitFor(() => {
expect(result.current.token).toBe(newToken);
});
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});
describe("useCSRFFetch", () => {
it("should add CSRF token to POST requests", async () => {
const token = "test-token";
document.cookie = `csrf-token=${token}`;
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true }),
});
const { result } = renderHook(() => useCSRFFetch());
await waitFor(() => {
expect(result.current.token).toBe(token);
});
await result.current.csrfFetch("/api/test", {
method: "POST",
body: JSON.stringify({ data: "test" }),
});
expect(mockFetch).toHaveBeenCalledWith(
"/api/test",
expect.objectContaining({
method: "POST",
credentials: "include",
headers: expect.objectContaining({
"x-csrf-token": token,
}),
})
);
});
it("should not add CSRF token to GET requests", async () => {
const token = "test-token";
document.cookie = `csrf-token=${token}`;
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true }),
});
const { result } = renderHook(() => useCSRFFetch());
await waitFor(() => {
expect(result.current.token).toBe(token);
});
await result.current.csrfFetch("/api/test", {
method: "GET",
});
expect(mockFetch).toHaveBeenCalledWith(
"/api/test",
expect.objectContaining({
method: "GET",
credentials: "include",
})
);
const callArgs = mockFetch.mock.calls[0][1];
expect(callArgs.headers?.["x-csrf-token"]).toBeUndefined();
});
it("should handle missing token gracefully", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true }),
});
const { result } = renderHook(() => useCSRFFetch());
await result.current.csrfFetch("/api/test", {
method: "POST",
body: JSON.stringify({ data: "test" }),
});
expect(mockFetch).toHaveBeenCalledWith(
"/api/test",
expect.objectContaining({
method: "POST",
credentials: "include",
})
);
});
});
describe("useCSRFForm", () => {
it("should add CSRF token to form data", async () => {
const token = "test-token";
document.cookie = `csrf-token=${token}`;
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true }),
});
const { result } = renderHook(() => useCSRFForm());
await waitFor(() => {
expect(result.current.token).toBe(token);
});
const formData = new FormData();
formData.append("data", "test");
await result.current.submitForm("/api/test", formData);
expect(mockFetch).toHaveBeenCalledWith(
"/api/test",
expect.objectContaining({
method: "POST",
credentials: "include",
body: expect.any(FormData),
})
);
const callArgs = mockFetch.mock.calls[0][1];
const submittedFormData = callArgs.body as FormData;
expect(submittedFormData.get("csrf_token")).toBe(token);
});
it("should add CSRF token to JSON data", async () => {
const token = "test-token";
document.cookie = `csrf-token=${token}`;
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true }),
});
const { result } = renderHook(() => useCSRFForm());
await waitFor(() => {
expect(result.current.token).toBe(token);
});
const data = { data: "test" };
await result.current.submitJSON("/api/test", data);
expect(mockFetch).toHaveBeenCalledWith(
"/api/test",
expect.objectContaining({
method: "POST",
credentials: "include",
headers: expect.objectContaining({
"Content-Type": "application/json",
}),
body: JSON.stringify({ ...data, csrfToken: token }),
})
);
});
it("should handle missing token in form submission", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true }),
});
const { result } = renderHook(() => useCSRFForm());
const formData = new FormData();
formData.append("data", "test");
await result.current.submitForm("/api/test", formData);
expect(mockFetch).toHaveBeenCalledWith(
"/api/test",
expect.objectContaining({
method: "POST",
credentials: "include",
body: expect.any(FormData),
})
);
});
});
});

240
tests/unit/csrf.test.ts Normal file
View File

@ -0,0 +1,240 @@
/**
* CSRF Protection Unit Tests
*
* Tests for CSRF token generation, validation, and protection mechanisms.
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { generateCSRFToken, verifyCSRFToken, CSRFProtection, CSRF_CONFIG } from "../../lib/csrf";
// Mock Next.js modules
vi.mock("next/headers", () => ({
cookies: vi.fn(() => ({
get: vi.fn(),
set: vi.fn(),
})),
}));
describe("CSRF Protection", () => {
describe("Token Generation and Verification", () => {
it("should generate a valid CSRF token", () => {
const token = generateCSRFToken();
expect(token).toBeDefined();
expect(typeof token).toBe("string");
expect(token.length).toBeGreaterThan(0);
expect(token.includes(":")).toBe(true);
});
it("should verify a valid CSRF token", () => {
const token = generateCSRFToken();
const isValid = verifyCSRFToken(token);
expect(isValid).toBe(true);
});
it("should reject an invalid CSRF token", () => {
const isValid = verifyCSRFToken("invalid-token");
expect(isValid).toBe(false);
});
it("should reject an empty CSRF token", () => {
const isValid = verifyCSRFToken("");
expect(isValid).toBe(false);
});
it("should reject a malformed CSRF token", () => {
const isValid = verifyCSRFToken("malformed:token:with:extra:parts");
expect(isValid).toBe(false);
});
});
describe("CSRFProtection Class", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should generate token response with correct structure", () => {
const response = CSRFProtection.generateTokenResponse();
expect(response).toHaveProperty("token");
expect(response).toHaveProperty("cookie");
expect(response.cookie).toHaveProperty("name", CSRF_CONFIG.cookieName);
expect(response.cookie).toHaveProperty("value");
expect(response.cookie).toHaveProperty("options");
expect(response.cookie.options).toHaveProperty("httpOnly", true);
expect(response.cookie.options).toHaveProperty("path", "/");
});
it("should validate GET requests without CSRF token", async () => {
const request = new Request("http://localhost/api/test", {
method: "GET",
}) as any;
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
it("should validate HEAD requests without CSRF token", async () => {
const request = new Request("http://localhost/api/test", {
method: "HEAD",
}) as any;
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
it("should validate OPTIONS requests without CSRF token", async () => {
const request = new Request("http://localhost/api/test", {
method: "OPTIONS",
}) as any;
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
it("should reject POST request without CSRF token", async () => {
const request = new Request("http://localhost/api/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ data: "test" }),
}) as any;
// Mock cookies method to return no token
Object.defineProperty(request, "cookies", {
value: {
get: vi.fn(() => undefined),
},
});
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(false);
expect(result.error).toContain("CSRF token missing");
});
it("should validate POST request with valid CSRF token", async () => {
const token = generateCSRFToken();
const request = new Request("http://localhost/api/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
[CSRF_CONFIG.headerName]: token,
},
body: JSON.stringify({ data: "test" }),
}) as any;
// Mock cookies method to return the same token
Object.defineProperty(request, "cookies", {
value: {
get: vi.fn(() => ({ value: token })),
},
});
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
it("should reject POST request with mismatched CSRF tokens", async () => {
const headerToken = generateCSRFToken();
const cookieToken = generateCSRFToken();
const request = new Request("http://localhost/api/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
[CSRF_CONFIG.headerName]: headerToken,
},
body: JSON.stringify({ data: "test" }),
}) as any;
// Mock cookies method to return different token
Object.defineProperty(request, "cookies", {
value: {
get: vi.fn(() => ({ value: cookieToken })),
},
});
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(false);
expect(result.error).toContain("mismatch");
});
it("should handle form data CSRF token", async () => {
const token = generateCSRFToken();
const formData = new FormData();
formData.append("csrf_token", token);
formData.append("data", "test");
const request = new Request("http://localhost/api/test", {
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
},
body: formData,
}) as any;
// Mock cookies method to return the same token
Object.defineProperty(request, "cookies", {
value: {
get: vi.fn(() => ({ value: token })),
},
});
// Mock clone method to return a request that can be parsed
Object.defineProperty(request, "clone", {
value: vi.fn(() => ({
formData: async () => formData,
})),
});
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
it("should handle JSON body CSRF token", async () => {
const token = generateCSRFToken();
const bodyData = { csrfToken: token, data: "test" };
const request = new Request("http://localhost/api/test", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(bodyData),
}) as any;
// Mock cookies method to return the same token
Object.defineProperty(request, "cookies", {
value: {
get: vi.fn(() => ({ value: token })),
},
});
// Mock clone method to return a request that can be parsed
Object.defineProperty(request, "clone", {
value: vi.fn(() => ({
json: async () => bodyData,
})),
});
const result = await CSRFProtection.validateRequest(request);
expect(result.valid).toBe(true);
});
});
describe("CSRF Configuration", () => {
it("should have correct configuration values", () => {
expect(CSRF_CONFIG.cookieName).toBe("csrf-token");
expect(CSRF_CONFIG.headerName).toBe("x-csrf-token");
expect(CSRF_CONFIG.cookie.httpOnly).toBe(true);
expect(CSRF_CONFIG.cookie.sameSite).toBe("lax");
expect(CSRF_CONFIG.cookie.maxAge).toBe(60 * 60 * 24); // 24 hours
});
it("should use secure cookies in production", () => {
// This would depend on NODE_ENV, which is set in the config
expect(typeof CSRF_CONFIG.cookie.secure).toBe("boolean");
});
});
});

View File

@ -0,0 +1,373 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { NextResponse } from "next/server";
// Mock Next.js response for testing headers
const createMockResponse = (headers: Record<string, string> = {}) => {
return new Response(null, { headers });
};
describe("HTTP Security Headers", () => {
describe("Security Header Configuration", () => {
it("should include X-Content-Type-Options header", () => {
const response = createMockResponse({
"X-Content-Type-Options": "nosniff",
});
expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
});
it("should include X-Frame-Options header for clickjacking protection", () => {
const response = createMockResponse({
"X-Frame-Options": "DENY",
});
expect(response.headers.get("X-Frame-Options")).toBe("DENY");
});
it("should include X-XSS-Protection header for legacy browser protection", () => {
const response = createMockResponse({
"X-XSS-Protection": "1; mode=block",
});
expect(response.headers.get("X-XSS-Protection")).toBe("1; mode=block");
});
it("should include Referrer-Policy header for privacy protection", () => {
const response = createMockResponse({
"Referrer-Policy": "strict-origin-when-cross-origin",
});
expect(response.headers.get("Referrer-Policy")).toBe(
"strict-origin-when-cross-origin"
);
});
it("should include X-DNS-Prefetch-Control header", () => {
const response = createMockResponse({
"X-DNS-Prefetch-Control": "off",
});
expect(response.headers.get("X-DNS-Prefetch-Control")).toBe("off");
});
});
describe("Content Security Policy", () => {
it("should include a comprehensive CSP header", () => {
const expectedCsp = [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https:",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
"upgrade-insecure-requests",
].join("; ");
const response = createMockResponse({
"Content-Security-Policy": expectedCsp,
});
expect(response.headers.get("Content-Security-Policy")).toBe(expectedCsp);
});
it("should have restrictive default-src policy", () => {
const csp = "default-src 'self'";
const response = createMockResponse({
"Content-Security-Policy": csp,
});
const cspValue = response.headers.get("Content-Security-Policy");
expect(cspValue).toContain("default-src 'self'");
});
it("should allow inline styles for TailwindCSS compatibility", () => {
const csp = "style-src 'self' 'unsafe-inline'";
const response = createMockResponse({
"Content-Security-Policy": csp,
});
const cspValue = response.headers.get("Content-Security-Policy");
expect(cspValue).toContain("style-src 'self' 'unsafe-inline'");
});
it("should prevent object embedding", () => {
const csp = "object-src 'none'";
const response = createMockResponse({
"Content-Security-Policy": csp,
});
const cspValue = response.headers.get("Content-Security-Policy");
expect(cspValue).toContain("object-src 'none'");
});
it("should prevent framing with frame-ancestors", () => {
const csp = "frame-ancestors 'none'";
const response = createMockResponse({
"Content-Security-Policy": csp,
});
const cspValue = response.headers.get("Content-Security-Policy");
expect(cspValue).toContain("frame-ancestors 'none'");
});
it("should upgrade insecure requests", () => {
const csp = "upgrade-insecure-requests";
const response = createMockResponse({
"Content-Security-Policy": csp,
});
const cspValue = response.headers.get("Content-Security-Policy");
expect(cspValue).toContain("upgrade-insecure-requests");
});
});
describe("Permissions Policy", () => {
it("should include restrictive Permissions-Policy header", () => {
const expectedPolicy = [
"camera=()",
"microphone=()",
"geolocation=()",
"interest-cohort=()",
"browsing-topics=()",
].join(", ");
const response = createMockResponse({
"Permissions-Policy": expectedPolicy,
});
expect(response.headers.get("Permissions-Policy")).toBe(expectedPolicy);
});
it("should disable camera access", () => {
const policy = "camera=()";
const response = createMockResponse({
"Permissions-Policy": policy,
});
const policyValue = response.headers.get("Permissions-Policy");
expect(policyValue).toContain("camera=()");
});
it("should disable microphone access", () => {
const policy = "microphone=()";
const response = createMockResponse({
"Permissions-Policy": policy,
});
const policyValue = response.headers.get("Permissions-Policy");
expect(policyValue).toContain("microphone=()");
});
it("should disable geolocation access", () => {
const policy = "geolocation=()";
const response = createMockResponse({
"Permissions-Policy": policy,
});
const policyValue = response.headers.get("Permissions-Policy");
expect(policyValue).toContain("geolocation=()");
});
it("should disable interest-cohort for privacy", () => {
const policy = "interest-cohort=()";
const response = createMockResponse({
"Permissions-Policy": policy,
});
const policyValue = response.headers.get("Permissions-Policy");
expect(policyValue).toContain("interest-cohort=()");
});
});
describe("HSTS Configuration", () => {
it("should include HSTS header in production environment", () => {
// Mock production environment
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = "production";
const response = createMockResponse({
"Strict-Transport-Security":
"max-age=31536000; includeSubDomains; preload",
});
expect(response.headers.get("Strict-Transport-Security")).toBe(
"max-age=31536000; includeSubDomains; preload"
);
// Restore original environment
process.env.NODE_ENV = originalEnv;
});
it("should have long max-age for HSTS", () => {
const hstsValue = "max-age=31536000; includeSubDomains; preload";
const response = createMockResponse({
"Strict-Transport-Security": hstsValue,
});
const hsts = response.headers.get("Strict-Transport-Security");
expect(hsts).toContain("max-age=31536000"); // 1 year
});
it("should include subdomains in HSTS", () => {
const hstsValue = "max-age=31536000; includeSubDomains; preload";
const response = createMockResponse({
"Strict-Transport-Security": hstsValue,
});
const hsts = response.headers.get("Strict-Transport-Security");
expect(hsts).toContain("includeSubDomains");
});
it("should be preload-ready for HSTS", () => {
const hstsValue = "max-age=31536000; includeSubDomains; preload";
const response = createMockResponse({
"Strict-Transport-Security": hstsValue,
});
const hsts = response.headers.get("Strict-Transport-Security");
expect(hsts).toContain("preload");
});
});
describe("Header Security Validation", () => {
it("should not expose server information", () => {
const response = createMockResponse({});
// These headers should not be present or should be minimal
expect(response.headers.get("Server")).toBeNull();
expect(response.headers.get("X-Powered-By")).toBeNull();
});
it("should have all required security headers present", () => {
const requiredHeaders = [
"X-Content-Type-Options",
"X-Frame-Options",
"X-XSS-Protection",
"Referrer-Policy",
"X-DNS-Prefetch-Control",
"Content-Security-Policy",
"Permissions-Policy",
];
const allHeaders: Record<string, string> = {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "strict-origin-when-cross-origin",
"X-DNS-Prefetch-Control": "off",
"Content-Security-Policy": "default-src 'self'",
"Permissions-Policy": "camera=()",
};
const response = createMockResponse(allHeaders);
requiredHeaders.forEach((header) => {
expect(response.headers.get(header)).toBeTruthy();
});
});
it("should have proper header values for security", () => {
const securityHeaders = {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "strict-origin-when-cross-origin",
"X-DNS-Prefetch-Control": "off",
};
const response = createMockResponse(securityHeaders);
// Verify each header has the expected security value
Object.entries(securityHeaders).forEach(([header, expectedValue]) => {
expect(response.headers.get(header)).toBe(expectedValue);
});
});
});
describe("Development vs Production Headers", () => {
it("should not include HSTS in development", () => {
// Mock development environment
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = "development";
const response = createMockResponse({});
// HSTS should not be present in development
expect(response.headers.get("Strict-Transport-Security")).toBeNull();
// Restore original environment
process.env.NODE_ENV = originalEnv;
});
it("should include all other headers in development", () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = "development";
const devHeaders = {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Content-Security-Policy": "default-src 'self'",
"Permissions-Policy": "camera=()",
};
const response = createMockResponse(devHeaders);
Object.entries(devHeaders).forEach(([header, expectedValue]) => {
expect(response.headers.get(header)).toBe(expectedValue);
});
process.env.NODE_ENV = originalEnv;
});
});
});
describe("Security Header Integration", () => {
describe("CSP and Frame Protection Alignment", () => {
it("should have consistent frame protection between CSP and X-Frame-Options", () => {
// Both should prevent framing
const cspResponse = createMockResponse({
"Content-Security-Policy": "frame-ancestors 'none'",
});
const xFrameResponse = createMockResponse({
"X-Frame-Options": "DENY",
});
expect(cspResponse.headers.get("Content-Security-Policy")).toContain(
"frame-ancestors 'none'"
);
expect(xFrameResponse.headers.get("X-Frame-Options")).toBe("DENY");
});
});
describe("Next.js Compatibility", () => {
it("should allow necessary Next.js functionality in CSP", () => {
const csp = "script-src 'self' 'unsafe-eval' 'unsafe-inline'";
const response = createMockResponse({
"Content-Security-Policy": csp,
});
const cspValue = response.headers.get("Content-Security-Policy");
// Next.js requires unsafe-eval for dev tools and unsafe-inline for some functionality
expect(cspValue).toContain("'unsafe-eval'");
expect(cspValue).toContain("'unsafe-inline'");
});
it("should allow TailwindCSS inline styles in CSP", () => {
const csp = "style-src 'self' 'unsafe-inline'";
const response = createMockResponse({
"Content-Security-Policy": csp,
});
const cspValue = response.headers.get("Content-Security-Policy");
expect(cspValue).toContain("style-src 'self' 'unsafe-inline'");
});
});
});

View File

@ -0,0 +1,142 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import crypto from "node:crypto";
// Mock crypto to test both real and mocked behavior
const originalRandomBytes = crypto.randomBytes;
describe("Password Reset Token Security", () => {
beforeEach(() => {
// Restore original crypto function for these tests
crypto.randomBytes = originalRandomBytes;
});
describe("Token Generation Security Properties", () => {
it("should generate tokens with 64 characters (32 bytes as hex)", () => {
const token = crypto.randomBytes(32).toString('hex');
expect(token).toHaveLength(64);
});
it("should generate unique tokens on each call", () => {
const token1 = crypto.randomBytes(32).toString('hex');
const token2 = crypto.randomBytes(32).toString('hex');
const token3 = crypto.randomBytes(32).toString('hex');
expect(token1).not.toBe(token2);
expect(token2).not.toBe(token3);
expect(token1).not.toBe(token3);
});
it("should generate tokens with proper entropy (no obvious patterns)", () => {
const tokens = new Set();
const numTokens = 100;
// Generate multiple tokens to check for patterns
for (let i = 0; i < numTokens; i++) {
const token = crypto.randomBytes(32).toString('hex');
tokens.add(token);
}
// All tokens should be unique
expect(tokens.size).toBe(numTokens);
});
it("should generate tokens with hex characters only", () => {
const token = crypto.randomBytes(32).toString('hex');
const hexPattern = /^[0-9a-f]+$/;
expect(token).toMatch(hexPattern);
});
it("should have sufficient entropy to prevent brute force attacks", () => {
// 32 bytes = 256 bits of entropy
// This provides 2^256 possible combinations
const token = crypto.randomBytes(32).toString('hex');
// Verify we have the expected length for 256-bit security
expect(token).toHaveLength(64);
// Verify character distribution is roughly uniform
const charCounts = {};
for (const char of token) {
charCounts[char] = (charCounts[char] || 0) + 1;
}
// Should have at least some variety in characters
expect(Object.keys(charCounts).length).toBeGreaterThan(5);
});
it("should be significantly more secure than Math.random() approach", () => {
// Generate tokens using both methods for comparison
const secureToken = crypto.randomBytes(32).toString('hex');
const weakToken = Math.random().toString(36).substring(2, 15);
// Secure token should be much longer
expect(secureToken.length).toBeGreaterThan(weakToken.length * 4);
// Secure token has proper hex format
expect(secureToken).toMatch(/^[0-9a-f]{64}$/);
// Weak token has predictable format
expect(weakToken).toMatch(/^[0-9a-z]+$/);
expect(weakToken.length).toBeLessThan(14);
});
});
describe("Token Collision Resistance", () => {
it("should have virtually zero probability of collision", () => {
const tokens = new Set();
const iterations = 10000;
// Generate many tokens to test collision resistance
for (let i = 0; i < iterations; i++) {
const token = crypto.randomBytes(32).toString('hex');
expect(tokens.has(token)).toBe(false); // No collisions
tokens.add(token);
}
expect(tokens.size).toBe(iterations);
});
});
describe("Performance Characteristics", () => {
it("should generate tokens in reasonable time", () => {
const startTime = Date.now();
// Generate 1000 tokens
for (let i = 0; i < 1000; i++) {
crypto.randomBytes(32).toString('hex');
}
const endTime = Date.now();
const duration = endTime - startTime;
// Should complete in under 1 second
expect(duration).toBeLessThan(1000);
});
});
describe("Token Format Validation", () => {
it("should always produce lowercase hex", () => {
for (let i = 0; i < 10; i++) {
const token = crypto.randomBytes(32).toString('hex');
expect(token).toBe(token.toLowerCase());
expect(token).toMatch(/^[0-9a-f]{64}$/);
}
});
it("should never produce tokens starting with predictable patterns", () => {
const tokens = [];
for (let i = 0; i < 100; i++) {
tokens.push(crypto.randomBytes(32).toString('hex'));
}
// Check that tokens don't all start with same character
const firstChars = new Set(tokens.map(t => t[0]));
expect(firstChars.size).toBeGreaterThan(1);
// Check that we don't have obvious patterns like all starting with '0'
const zeroStart = tokens.filter(t => t.startsWith('0')).length;
expect(zeroStart).toBeLessThan(tokens.length * 0.8); // Should be roughly 1/16
});
});
});