UNPKG

@ordojs/security

Version:

Security package for OrdoJS with XSS, CSRF, and injection protection

371 lines (288 loc) 12.2 kB
/** * CSRF Manager Tests * Comprehensive tests for CSRF protection functionality */ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { CSRFManager } from './csrf-manager'; import { CSRFConfig, CSRFRequest } from './types'; describe('CSRFManager', () => { let csrfManager: CSRFManager; let config: CSRFConfig; beforeEach(() => { config = { secret: 'test-secret-key-for-csrf-protection', tokenExpiry: 60 * 60 * 1000, // 1 hour cookieName: '__csrf-token', headerName: 'X-CSRF-Token', fieldName: '_csrf', secureCookie: true, httpOnlyCookie: true, sameSite: 'strict' }; csrfManager = new CSRFManager(config); }); afterEach(() => { csrfManager.destroy(); }); describe('Token Generation', () => { it('should generate a valid CSRF token', () => { const sessionId = 'test-session-123'; const token = csrfManager.generateToken(sessionId); expect(token).toBeDefined(); expect(token.value).toBeTruthy(); expect(token.sessionId).toBe(sessionId); expect(token.expiresAt).toBeGreaterThan(Date.now()); }); it('should generate unique tokens for the same session', () => { const sessionId = 'test-session-123'; const token1 = csrfManager.generateToken(sessionId); const token2 = csrfManager.generateToken(sessionId); expect(token1.value).not.toBe(token2.value); expect(token1.sessionId).toBe(token2.sessionId); }); it('should generate different tokens for different sessions', () => { const token1 = csrfManager.generateToken('session-1'); const token2 = csrfManager.generateToken('session-2'); expect(token1.value).not.toBe(token2.value); expect(token1.sessionId).not.toBe(token2.sessionId); }); }); describe('Token Validation', () => { it('should validate a correct token', () => { const sessionId = 'test-session-123'; const token = csrfManager.generateToken(sessionId); const result = csrfManager.validateToken(token.value, sessionId); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); it('should reject token with wrong session ID', () => { const sessionId = 'test-session-123'; const token = csrfManager.generateToken(sessionId); const result = csrfManager.validateToken(token.value, 'wrong-session'); expect(result.valid).toBe(false); expect(result.error).toBeTruthy(); }); it('should reject malformed tokens', () => { const result = csrfManager.validateToken('invalid-token', 'session-123'); expect(result.valid).toBe(false); expect(result.error).toBeTruthy(); }); it('should reject expired tokens', () => { // Create manager with very short expiry const shortConfig = { ...config, tokenExpiry: 1 }; // 1ms const shortManager = new CSRFManager(shortConfig); const sessionId = 'test-session-123'; const token = shortManager.generateToken(sessionId); // Wait for token to expire return new Promise(resolve => { setTimeout(() => { const result = shortManager.validateToken(token.value, sessionId); expect(result.valid).toBe(false); expect(result.expired).toBe(true); shortManager.destroy(); resolve(undefined); }, 10); }); }); }); describe('Double-Submit Cookie Pattern', () => { it('should set up double-submit protection', () => { const sessionId = 'test-session-123'; const response = csrfManager.setupDoubleSubmitProtection(sessionId); expect(response.headers).toBeDefined(); expect(response.headers['X-CSRF-Token']).toBeTruthy(); expect(response.cookies).toHaveLength(1); const cookie = response.cookies[0]; expect(cookie.name).toBe('__csrf-token'); expect(cookie.value).toBeTruthy(); expect(cookie.options.httpOnly).toBe(true); expect(cookie.options.secure).toBe(true); expect(cookie.options.sameSite).toBe('strict'); }); it('should validate matching double-submit tokens', () => { const sessionId = 'test-session-123'; const response = csrfManager.setupDoubleSubmitProtection(sessionId); const cookieToken = response.cookies[0].value; const headerToken = response.headers['X-CSRF-Token']; const request: CSRFRequest = { headers: { 'X-CSRF-Token': headerToken }, cookies: { '__csrf-token': cookieToken } }; const result = csrfManager.validateDoubleSubmit(request); expect(result.valid).toBe(true); }); it('should reject mismatched double-submit tokens', () => { const sessionId = 'test-session-123'; const response1 = csrfManager.setupDoubleSubmitProtection(sessionId); const response2 = csrfManager.setupDoubleSubmitProtection(sessionId); const request: CSRFRequest = { headers: { 'X-CSRF-Token': response1.headers['X-CSRF-Token'] }, cookies: { '__csrf-token': response2.cookies[0].value } }; const result = csrfManager.validateDoubleSubmit(request); expect(result.valid).toBe(false); expect(result.error).toBeTruthy(); }); it('should reject missing double-submit tokens', () => { const request: CSRFRequest = { headers: {}, cookies: {} }; const result = csrfManager.validateDoubleSubmit(request); expect(result.valid).toBe(false); expect(result.error).toBe('Missing CSRF tokens'); }); }); describe('Request Validation', () => { it('should validate request with session-based token in header', () => { const sessionId = 'test-session-123'; const token = csrfManager.generateToken(sessionId); const request: CSRFRequest = { headers: { 'X-CSRF-Token': token.value }, sessionId }; const result = csrfManager.validateRequest(request); expect(result.valid).toBe(true); }); it('should validate request with session-based token in form field', () => { const sessionId = 'test-session-123'; const token = csrfManager.generateToken(sessionId); const request: CSRFRequest = { headers: {}, body: { _csrf: token.value }, sessionId }; const result = csrfManager.validateRequest(request); expect(result.valid).toBe(true); }); it('should validate request with double-submit pattern when no session ID', () => { const sessionId = 'test-session-123'; const response = csrfManager.setupDoubleSubmitProtection(sessionId); const request: CSRFRequest = { headers: { 'X-CSRF-Token': response.headers['X-CSRF-Token'] }, cookies: { '__csrf-token': response.cookies[0].value } }; const result = csrfManager.validateRequest(request); expect(result.valid).toBe(true); }); it('should reject request without CSRF token', () => { const request: CSRFRequest = { headers: {}, sessionId: 'test-session-123' }; const result = csrfManager.validateRequest(request); expect(result.valid).toBe(false); expect(result.error).toBe('CSRF token not found in request'); }); }); describe('Form Field Generation', () => { it('should generate HTML form field', () => { const sessionId = 'test-session-123'; const formField = csrfManager.generateFormField(sessionId); expect(formField).toContain('type="hidden"'); expect(formField).toContain('name="_csrf"'); expect(formField).toContain('value='); expect(formField).toMatch(/<input[^>]*>/); }); it('should generate form field with valid token', () => { const sessionId = 'test-session-123'; const formField = csrfManager.generateFormField(sessionId); // Extract token value from HTML const match = formField.match(/value="([^"]+)"/); expect(match).toBeTruthy(); if (match) { const tokenValue = match[1]; const result = csrfManager.validateToken(tokenValue, sessionId); expect(result.valid).toBe(true); } }); }); describe('Client Script Generation', () => { it('should generate client-side JavaScript', () => { const script = csrfManager.generateClientScript('test-session'); expect(script).toContain('csrfConfig'); expect(script).toContain('X-CSRF-Token'); expect(script).toContain('__csrf-token'); expect(script).toContain('_csrf'); expect(script).toContain('XMLHttpRequest'); expect(script).toContain('fetch'); }); it('should include configuration in generated script', () => { const script = csrfManager.generateClientScript(); expect(script).toContain('"headerName":"X-CSRF-Token"'); expect(script).toContain('"fieldName":"_csrf"'); expect(script).toContain('"cookieName":"__csrf-token"'); }); }); describe('Token Consumption', () => { it('should consume token successfully', () => { const sessionId = 'test-session-123'; const token = csrfManager.generateToken(sessionId); const consumed = csrfManager.consumeToken(sessionId, token.value); expect(consumed).toBe(true); // Token should no longer be valid after consumption const result = csrfManager.validateToken(token.value, sessionId); expect(result.valid).toBe(false); }); it('should return false for non-existent token', () => { const consumed = csrfManager.consumeToken('session-123', 'non-existent-token'); expect(consumed).toBe(false); }); }); describe('Session Management', () => { it('should remove session and all tokens', () => { const sessionId = 'test-session-123'; const token1 = csrfManager.generateToken(sessionId); const token2 = csrfManager.generateToken(sessionId); const removed = csrfManager.removeSession(sessionId); expect(removed).toBe(true); // Tokens should no longer be valid const result1 = csrfManager.validateToken(token1.value, sessionId); const result2 = csrfManager.validateToken(token2.value, sessionId); expect(result1.valid).toBe(false); expect(result2.valid).toBe(false); }); it('should return false when removing non-existent session', () => { const removed = csrfManager.removeSession('non-existent-session'); expect(removed).toBe(false); }); }); describe('Statistics', () => { it('should provide session statistics', () => { const sessionId1 = 'session-1'; const sessionId2 = 'session-2'; csrfManager.generateToken(sessionId1); csrfManager.generateToken(sessionId1); csrfManager.generateToken(sessionId2); const stats = csrfManager.getStats(); expect(stats.totalSessions).toBe(2); expect(stats.totalTokens).toBe(3); expect(stats.activeSessions).toBe(2); }); }); describe('Configuration', () => { it('should return configuration', () => { const returnedConfig = csrfManager.getConfig(); expect(returnedConfig.secret).toBe(config.secret); expect(returnedConfig.tokenExpiry).toBe(config.tokenExpiry); expect(returnedConfig.cookieName).toBe(config.cookieName); expect(returnedConfig.headerName).toBe(config.headerName); expect(returnedConfig.fieldName).toBe(config.fieldName); }); it('should use default values for optional config', () => { const minimalConfig: CSRFConfig = { secret: 'test-secret' }; const manager = new CSRFManager(minimalConfig); const returnedConfig = manager.getConfig(); expect(returnedConfig.tokenExpiry).toBe(60 * 60 * 1000); // 1 hour default expect(returnedConfig.cookieName).toBe('__csrf-token'); expect(returnedConfig.headerName).toBe('X-CSRF-Token'); expect(returnedConfig.fieldName).toBe('_csrf'); expect(returnedConfig.secureCookie).toBe(true); expect(returnedConfig.httpOnlyCookie).toBe(true); expect(returnedConfig.sameSite).toBe('strict'); manager.destroy(); }); }); });