mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 13:52:16 +01:00
feat: implement comprehensive CSRF protection
This commit is contained in:
253
tests/integration/csrf-protection.test.ts
Normal file
253
tests/integration/csrf-protection.test.ts
Normal file
@ -0,0 +1,253 @@
|
||||
/**
|
||||
* CSRF Protection Integration Tests
|
||||
*
|
||||
* End-to-end tests for CSRF protection in API endpoints and middleware.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { csrfProtectionMiddleware, csrfTokenMiddleware } from "../../middleware/csrfProtection";
|
||||
import { generateCSRFToken } from "../../lib/csrf";
|
||||
|
||||
describe("CSRF Protection Integration", () => {
|
||||
describe("CSRF Token Middleware", () => {
|
||||
it("should serve CSRF token on GET /api/csrf-token", async () => {
|
||||
const { req } = createMocks({
|
||||
method: "GET",
|
||||
url: "/api/csrf-token",
|
||||
});
|
||||
|
||||
const request = {
|
||||
method: "GET",
|
||||
nextUrl: { pathname: "/api/csrf-token" },
|
||||
} as NextRequest;
|
||||
|
||||
const response = csrfTokenMiddleware(request);
|
||||
expect(response).not.toBeNull();
|
||||
|
||||
if (response) {
|
||||
const body = await response.json();
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.token).toBeDefined();
|
||||
expect(typeof body.token).toBe("string");
|
||||
}
|
||||
});
|
||||
|
||||
it("should return null for non-csrf-token paths", async () => {
|
||||
const request = {
|
||||
method: "GET",
|
||||
nextUrl: { pathname: "/api/other" },
|
||||
} as NextRequest;
|
||||
|
||||
const response = csrfTokenMiddleware(request);
|
||||
expect(response).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSRF Protection Middleware", () => {
|
||||
it("should allow GET requests without CSRF token", async () => {
|
||||
const request = {
|
||||
method: "GET",
|
||||
nextUrl: { pathname: "/api/dashboard" },
|
||||
} as NextRequest;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).not.toBe(403);
|
||||
});
|
||||
|
||||
it("should allow HEAD requests without CSRF token", async () => {
|
||||
const request = {
|
||||
method: "HEAD",
|
||||
nextUrl: { pathname: "/api/dashboard" },
|
||||
} as NextRequest;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).not.toBe(403);
|
||||
});
|
||||
|
||||
it("should allow OPTIONS requests without CSRF token", async () => {
|
||||
const request = {
|
||||
method: "OPTIONS",
|
||||
nextUrl: { pathname: "/api/dashboard" },
|
||||
} as NextRequest;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).not.toBe(403);
|
||||
});
|
||||
|
||||
it("should block POST request to protected endpoint without CSRF token", async () => {
|
||||
const request = {
|
||||
method: "POST",
|
||||
nextUrl: { pathname: "/api/dashboard/sessions" },
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
cookies: {
|
||||
get: () => undefined,
|
||||
},
|
||||
clone: () => ({
|
||||
json: async () => ({}),
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).toBe(403);
|
||||
|
||||
const body = await response.json();
|
||||
expect(body.success).toBe(false);
|
||||
expect(body.error).toContain("CSRF token");
|
||||
});
|
||||
|
||||
it("should allow POST request to unprotected endpoint without CSRF token", async () => {
|
||||
const request = {
|
||||
method: "POST",
|
||||
nextUrl: { pathname: "/api/unprotected" },
|
||||
} as NextRequest;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).not.toBe(403);
|
||||
});
|
||||
|
||||
it("should allow POST request with valid CSRF token", async () => {
|
||||
const token = generateCSRFToken();
|
||||
|
||||
const request = {
|
||||
method: "POST",
|
||||
nextUrl: { pathname: "/api/dashboard/sessions" },
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
"x-csrf-token": token,
|
||||
}),
|
||||
cookies: {
|
||||
get: () => ({ value: token }),
|
||||
},
|
||||
clone: () => ({
|
||||
json: async () => ({ csrfToken: token }),
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).not.toBe(403);
|
||||
});
|
||||
|
||||
it("should block POST request with mismatched CSRF tokens", async () => {
|
||||
const headerToken = generateCSRFToken();
|
||||
const cookieToken = generateCSRFToken();
|
||||
|
||||
const request = {
|
||||
method: "POST",
|
||||
nextUrl: { pathname: "/api/dashboard/sessions" },
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
"x-csrf-token": headerToken,
|
||||
}),
|
||||
cookies: {
|
||||
get: () => ({ value: cookieToken }),
|
||||
},
|
||||
clone: () => ({
|
||||
json: async () => ({ csrfToken: headerToken }),
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it("should protect all state-changing methods", async () => {
|
||||
const methods = ["POST", "PUT", "DELETE", "PATCH"];
|
||||
|
||||
for (const method of methods) {
|
||||
const request = {
|
||||
method,
|
||||
nextUrl: { pathname: "/api/trpc/test" },
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
cookies: {
|
||||
get: () => undefined,
|
||||
},
|
||||
clone: () => ({
|
||||
json: async () => ({}),
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).toBe(403);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Protected Endpoints", () => {
|
||||
const protectedPaths = [
|
||||
"/api/auth/signin",
|
||||
"/api/register",
|
||||
"/api/forgot-password",
|
||||
"/api/reset-password",
|
||||
"/api/dashboard/sessions",
|
||||
"/api/platform/companies",
|
||||
"/api/trpc/test",
|
||||
];
|
||||
|
||||
protectedPaths.forEach((path) => {
|
||||
it(`should protect ${path} endpoint`, async () => {
|
||||
const request = {
|
||||
method: "POST",
|
||||
nextUrl: { pathname: path },
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
cookies: {
|
||||
get: () => undefined,
|
||||
},
|
||||
clone: () => ({
|
||||
json: async () => ({}),
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle malformed requests gracefully", async () => {
|
||||
const request = {
|
||||
method: "POST",
|
||||
nextUrl: { pathname: "/api/dashboard/sessions" },
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
cookies: {
|
||||
get: () => undefined,
|
||||
},
|
||||
clone: () => ({
|
||||
json: async () => {
|
||||
throw new Error("Malformed JSON");
|
||||
},
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it("should handle missing headers gracefully", async () => {
|
||||
const request = {
|
||||
method: "POST",
|
||||
nextUrl: { pathname: "/api/dashboard/sessions" },
|
||||
headers: new Headers(),
|
||||
cookies: {
|
||||
get: () => undefined,
|
||||
},
|
||||
clone: () => ({
|
||||
json: async () => ({}),
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const response = await csrfProtectionMiddleware(request);
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user