/** * CSRF Service Tests * * Tests CSRF token generation and validation with session binding. */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { CsrfService } from "./csrf.service"; describe("CsrfService", () => { let service: CsrfService; const originalEnv = process.env; beforeEach(() => { process.env = { ...originalEnv }; // Set a consistent secret for tests process.env.CSRF_SECRET = "test-secret-key-0123456789abcdef0123456789abcdef"; service = new CsrfService(); service.onModuleInit(); }); afterEach(() => { process.env = originalEnv; }); describe("onModuleInit", () => { it("should initialize with configured secret", () => { const testService = new CsrfService(); process.env.CSRF_SECRET = "configured-secret"; expect(() => testService.onModuleInit()).not.toThrow(); }); it("should throw in production without CSRF_SECRET", () => { const testService = new CsrfService(); process.env.NODE_ENV = "production"; delete process.env.CSRF_SECRET; expect(() => testService.onModuleInit()).toThrow( "CSRF_SECRET environment variable is required in production" ); }); it("should generate random secret in development without CSRF_SECRET", () => { const testService = new CsrfService(); process.env.NODE_ENV = "development"; delete process.env.CSRF_SECRET; expect(() => testService.onModuleInit()).not.toThrow(); }); }); describe("generateToken", () => { it("should generate a token with random:hmac format", () => { const token = service.generateToken("user-123"); expect(token).toContain(":"); const parts = token.split(":"); expect(parts).toHaveLength(2); }); it("should generate 64-char hex random part (32 bytes)", () => { const token = service.generateToken("user-123"); const randomPart = token.split(":")[0]; expect(randomPart).toHaveLength(64); expect(/^[0-9a-f]{64}$/.test(randomPart as string)).toBe(true); }); it("should generate 64-char hex HMAC (SHA-256)", () => { const token = service.generateToken("user-123"); const hmacPart = token.split(":")[1]; expect(hmacPart).toHaveLength(64); expect(/^[0-9a-f]{64}$/.test(hmacPart as string)).toBe(true); }); it("should generate unique tokens on each call", () => { const token1 = service.generateToken("user-123"); const token2 = service.generateToken("user-123"); expect(token1).not.toBe(token2); }); it("should generate different HMACs for different sessions", () => { const token1 = service.generateToken("user-123"); const token2 = service.generateToken("user-456"); const hmac1 = token1.split(":")[1]; const hmac2 = token2.split(":")[1]; // Even with same random part, HMACs would differ due to session binding // But since random parts differ, this just confirms they're different tokens expect(hmac1).not.toBe(hmac2); }); }); describe("validateToken", () => { it("should validate a token for the correct session", () => { const sessionId = "user-123"; const token = service.generateToken(sessionId); expect(service.validateToken(token, sessionId)).toBe(true); }); it("should reject a token for a different session", () => { const token = service.generateToken("user-123"); expect(service.validateToken(token, "user-456")).toBe(false); }); it("should reject empty token", () => { expect(service.validateToken("", "user-123")).toBe(false); }); it("should reject empty session ID", () => { const token = service.generateToken("user-123"); expect(service.validateToken(token, "")).toBe(false); }); it("should reject token without colon separator", () => { expect(service.validateToken("invalidtoken", "user-123")).toBe(false); }); it("should reject token with empty random part", () => { expect(service.validateToken(":somehash", "user-123")).toBe(false); }); it("should reject token with empty HMAC part", () => { expect(service.validateToken("somerandom:", "user-123")).toBe(false); }); it("should reject token with invalid hex in random part", () => { expect( service.validateToken( "invalid-hex-here-not-64-chars:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "user-123" ) ).toBe(false); }); it("should reject token with invalid hex in HMAC part", () => { expect( service.validateToken( "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:not-valid-hex", "user-123" ) ).toBe(false); }); it("should reject token with tampered HMAC", () => { const token = service.generateToken("user-123"); const parts = token.split(":"); // Tamper with the HMAC const tamperedToken = `${parts[0]}:0000000000000000000000000000000000000000000000000000000000000000`; expect(service.validateToken(tamperedToken, "user-123")).toBe(false); }); it("should reject token with tampered random part", () => { const token = service.generateToken("user-123"); const parts = token.split(":"); // Tamper with the random part const tamperedToken = `0000000000000000000000000000000000000000000000000000000000000000:${parts[1]}`; expect(service.validateToken(tamperedToken, "user-123")).toBe(false); }); }); describe("session binding security", () => { it("should bind token to specific session", () => { const token = service.generateToken("session-A"); // Token valid for session-A expect(service.validateToken(token, "session-A")).toBe(true); // Token invalid for any other session expect(service.validateToken(token, "session-B")).toBe(false); expect(service.validateToken(token, "session-C")).toBe(false); expect(service.validateToken(token, "")).toBe(false); }); it("should not allow token reuse across sessions", () => { const userAToken = service.generateToken("user-A"); const userBToken = service.generateToken("user-B"); // Each token only valid for its own session expect(service.validateToken(userAToken, "user-A")).toBe(true); expect(service.validateToken(userAToken, "user-B")).toBe(false); expect(service.validateToken(userBToken, "user-B")).toBe(true); expect(service.validateToken(userBToken, "user-A")).toBe(false); }); it("should use different secrets to generate different tokens", () => { // Generate token with current secret const token1 = service.generateToken("user-123"); // Create new service with different secret process.env.CSRF_SECRET = "different-secret-key-abcdef0123456789"; const service2 = new CsrfService(); service2.onModuleInit(); // Token from service1 should not validate with service2 expect(service2.validateToken(token1, "user-123")).toBe(false); // But service2's own tokens should validate const token2 = service2.generateToken("user-123"); expect(service2.validateToken(token2, "user-123")).toBe(true); }); }); });