mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 18:52:08 +01:00
feat: implement comprehensive CSRF protection
This commit is contained in:
240
tests/unit/csrf.test.ts
Normal file
240
tests/unit/csrf.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user