@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
371 lines (288 loc) • 12.2 kB
text/typescript
/**
* 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();
});
});
});