#!/usr/bin/env ts-node
/**
* Security Validation Script for JWT Race Condition Fix
*
* This script validates that the critical JWT race condition vulnerability
* has been properly fixed and that all security controls are working.
*/
import { JWTService } from '../src/auth/jwt-service';
import { distributedLock } from '../src/auth/distributed-lock';
import { tokenBlacklist } from '../src/auth/token-blacklist';
import { tokenRateLimiter } from '../src/auth/token-rate-limiter';
import { initializeRedis, redis } from '../src/database/redis';
import { logger } from '../src/utils/logger';
import * as crypto from 'crypto';
interface ValidationResult {
testName: string;
status: 'PASS' | 'FAIL' | 'SKIP';
duration: number;
details: string;
securityImpact: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
}
interface PerformanceMetrics {
operation: string;
avgResponseTime: number;
maxResponseTime: number;
minResponseTime: number;
throughput: number;
errorRate: number;
}
class SecurityValidator {
private jwtService: JWTService;
private results: ValidationResult[] = [];
private performanceMetrics: PerformanceMetrics[] = [];
constructor() {
this.jwtService = new JWTService();
}
async initialize(): Promise<void> {
try {
await initializeRedis();
await this.jwtService.initialize();
logger.info('Security validator initialized');
} catch (error) {
logger.error('Failed to initialize security validator', { error });
throw error;
}
}
async runAllValidations(): Promise<{
results: ValidationResult[];
performanceMetrics: PerformanceMetrics[];
summary: {
totalTests: number;
passed: number;
failed: number;
criticalIssues: number;
};
}> {
logger.info('Starting comprehensive security validation');
const tests = [
{ name: 'Race Condition Prevention', fn: this.testRaceConditionPrevention.bind(this) },
{ name: 'Token Replay Attack Prevention', fn: this.testTokenReplayPrevention.bind(this) },
{ name: 'Concurrent Refresh Protection', fn: this.testConcurrentRefreshProtection.bind(this) },
{ name: 'Rate Limiting Effectiveness', fn: this.testRateLimitingEffectiveness.bind(this) },
{ name: 'Token Blacklisting Integrity', fn: this.testTokenBlacklistingIntegrity.bind(this) },
{ name: 'Distributed Lock Reliability', fn: this.testDistributedLockReliability.bind(this) },
{ name: 'High Concurrency Performance', fn: this.testHighConcurrencyPerformance.bind(this) },
{ name: 'Security Event Logging', fn: this.testSecurityEventLogging.bind(this) },
{ name: 'Session Management', fn: this.testSessionManagement.bind(this) },
{ name: 'Error Handling Security', fn: this.testErrorHandlingSecurity.bind(this) }
];
for (const test of tests) {
await this.runTest(test.name, test.fn);
}
const summary = this.generateSummary();
logger.info('Security validation completed', summary);
return {
results: this.results,
performanceMetrics: this.performanceMetrics,
summary
};
}
private async runTest(name: string, testFn: () => Promise<void>): Promise<void> {
const startTime = Date.now();
let status: 'PASS' | 'FAIL' | 'SKIP' = 'PASS';
let details = '';
let securityImpact: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' = 'LOW';
try {
logger.info(`Running test: ${name}`);
await testFn();
details = 'Test completed successfully';
} catch (error) {
status = 'FAIL';
details = error.message;
securityImpact = this.assessSecurityImpact(name, error);
logger.error(`Test failed: ${name}`, { error: error.message });
}
const duration = Date.now() - startTime;
this.results.push({
testName: name,
status,
duration,
details,
securityImpact
});
}
private assessSecurityImpact(testName: string, error: any): 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' {
if (testName.includes('Race Condition') || testName.includes('Token Replay')) {
return 'CRITICAL';
}
if (testName.includes('Concurrent') || testName.includes('Rate Limiting')) {
return 'HIGH';
}
if (testName.includes('Performance') || testName.includes('Logging')) {
return 'MEDIUM';
}
return 'LOW';
}
private async testRaceConditionPrevention(): Promise<void> {
const userId = 'test-user-race-condition';
const email = 'test@example.com';
// Generate initial token pair
const tokenPair = await this.jwtService.generateTokenPair({
userId,
email,
roles: ['user'],
permissions: ['read'],
sessionId: 'test-session',
mfaVerified: false
});
// Mock Redis data for refresh token
await redis.setex(`token_family:test-family`, 3600, JSON.stringify({
userId,
sessionId: 'test-session',
createdAt: new Date().toISOString(),
jti: 'test-jti'
}));
await redis.hset(`user:${userId}`, {
email,
roles: JSON.stringify(['user']),
permissions: JSON.stringify(['read']),
mfaVerified: 'false'
});
// Simulate concurrent refresh attempts
const concurrentAttempts = 10;
const refreshPromises = Array.from({ length: concurrentAttempts }, () =>
this.attemptTokenRefresh(tokenPair.refreshToken)
);
const results = await Promise.allSettled(refreshPromises);
const successful = results.filter(r => r.status === 'fulfilled').length;
// Should have at most 1 successful refresh due to distributed locking
if (successful > 1) {
throw new Error(`Race condition detected: ${successful} concurrent refreshes succeeded`);
}
logger.info('Race condition prevention test passed', { successful, total: concurrentAttempts });
}
private async attemptTokenRefresh(refreshToken: string): Promise<any> {
try {
return await this.jwtService.refreshAccessToken(refreshToken);
} catch (error) {
// Expected for most concurrent attempts
throw error;
}
}
private async testTokenReplayPrevention(): Promise<void> {
const userId = 'test-user-replay';
const tokenPair = await this.jwtService.generateTokenPair({
userId,
email: 'replay@example.com',
roles: ['user'],
permissions: ['read'],
sessionId: 'replay-session',
mfaVerified: false
});
// First use should succeed
await this.jwtService.verifyAccessToken(tokenPair.accessToken);
// Revoke the token
await this.jwtService.revokeToken((tokenPair.accessToken as any).jti, userId);
// Replay attempt should fail
try {
await this.jwtService.verifyAccessToken(tokenPair.accessToken);
throw new Error('Token replay attack succeeded - security violation');
} catch (error) {
if (!error.message.includes('Invalid or expired')) {
throw error;
}
}
logger.info('Token replay prevention test passed');
}
private async testConcurrentRefreshProtection(): Promise<void> {
// Test distributed lock effectiveness
const lockKey = 'test-concurrent-protection';
const concurrentLockAttempts = 20;
const lockPromises = Array.from({ length: concurrentLockAttempts }, () =>
distributedLock.acquireLock(lockKey, { ttl: 1000, maxRetries: 1 })
);
const lockResults = await Promise.allSettled(lockPromises);
const successfulLocks = lockResults.filter(r =>
r.status === 'fulfilled' && r.value !== null
).length;
// Only one lock should succeed
if (successfulLocks !== 1) {
throw new Error(`Distributed lock failed: ${successfulLocks} locks acquired simultaneously`);
}
logger.info('Concurrent refresh protection test passed');
}
private async testRateLimitingEffectiveness(): Promise<void> {
const userId = 'test-user-rate-limit';
// Test validation rate limiting
const validationAttempts = 150; // Should exceed limit
let rateLimitedCount = 0;
for (let i = 0; i < validationAttempts; i++) {
try {
const result = await tokenRateLimiter.checkValidationLimit(userId);
if (!result.allowed) {
rateLimitedCount++;
}
} catch (error) {
rateLimitedCount++;
}
}
// Should have significant rate limiting
if (rateLimitedCount < validationAttempts * 0.3) {
throw new Error(`Rate limiting ineffective: only ${rateLimitedCount} out of ${validationAttempts} were limited`);
}
logger.info('Rate limiting effectiveness test passed', { rateLimitedCount, total: validationAttempts });
}
private async testTokenBlacklistingIntegrity(): Promise<void> {
const userId = 'test-user-blacklist';
const tokenId = crypto.randomUUID();
const expiryDate = new Date(Date.now() + 3600000); // 1 hour
// Add token to blacklist
await tokenBlacklist.addToBlacklist(tokenId, userId, expiryDate, 'test_validation');
// Check if properly blacklisted
const isBlacklisted = await tokenBlacklist.isBlacklisted(tokenId);
if (!isBlacklisted) {
throw new Error('Token blacklisting failed - token not found in blacklist');
}
// Test user-level blacklisting
const hasBlacklistedTokens = await tokenBlacklist.hasBlacklistedTokens(userId);
if (!hasBlacklistedTokens) {
throw new Error('User-level blacklist check failed');
}
logger.info('Token blacklisting integrity test passed');
}
private async testDistributedLockReliability(): Promise<void> {
const lockKey = 'test-lock-reliability';
// Test basic lock acquisition and release
const lock = await distributedLock.acquireLock(lockKey, { ttl: 5000 });
if (!lock) {
throw new Error('Failed to acquire distributed lock');
}
// Test lock extension
const extended = await distributedLock.extendLock(lock, 10000);
if (!extended) {
throw new Error('Failed to extend distributed lock');
}
// Test lock validation
const isValid = await distributedLock.isLockValid(lock);
if (!isValid) {
throw new Error('Lock validation failed');
}
// Test lock release
const released = await distributedLock.releaseLock(lock);
if (!released) {
throw new Error('Failed to release distributed lock');
}
logger.info('Distributed lock reliability test passed');
}
private async testHighConcurrencyPerformance(): Promise<void> {
const userId = 'test-user-performance';
const tokenPair = await this.jwtService.generateTokenPair({
userId,
email: 'perf@example.com',
roles: ['user'],
permissions: ['read'],
sessionId: 'perf-session',
mfaVerified: false
});
// Mock successful validation
await redis.setex(`active_token:test-token`, 3600, JSON.stringify({
userId,
sessionId: 'perf-session',
issuedAt: new Date().toISOString()
}));
const concurrentValidations = 1000;
const startTime = Date.now();
// Perform concurrent validations
const validationPromises = Array.from({ length: concurrentValidations }, async () => {
const opStart = Date.now();
try {
await this.jwtService.verifyAccessToken(tokenPair.accessToken);
return Date.now() - opStart;
} catch (error) {
return Date.now() - opStart;
}
});
const responseTimes = await Promise.all(validationPromises);
const totalTime = Date.now() - startTime;
const avgResponseTime = responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length;
const maxResponseTime = Math.max(...responseTimes);
const minResponseTime = Math.min(...responseTimes);
const throughput = (concurrentValidations / totalTime) * 1000; // ops/sec
this.performanceMetrics.push({
operation: 'token_validation_concurrent',
avgResponseTime,
maxResponseTime,
minResponseTime,
throughput,
errorRate: 0
});
// Performance requirements
if (avgResponseTime > 50) {
throw new Error(`Performance degraded: average response time ${avgResponseTime}ms exceeds 50ms SLA`);
}
if (throughput < 100) {
throw new Error(`Performance degraded: throughput ${throughput} ops/sec below 100 ops/sec requirement`);
}
logger.info('High concurrency performance test passed', {
avgResponseTime,
maxResponseTime,
throughput
});
}
private async testSecurityEventLogging(): Promise<void> {
// This would typically check log aggregation system
// For now, we'll validate that security events are being generated
const userId = 'test-user-logging';
// Generate events that should be logged
await this.jwtService.generateTokenPair({
userId,
email: 'logging@example.com',
roles: ['user'],
permissions: ['read'],
sessionId: 'logging-session',
mfaVerified: false
});
// Simulate security violation
try {
await this.jwtService.verifyAccessToken('invalid-token');
} catch {
// Expected
}
// In a real implementation, you'd check log aggregation
logger.info('Security event logging test passed');
}
private async testSessionManagement(): Promise<void> {
const userId = 'test-user-session';
const sessionId = 'test-session-mgmt';
// Create session tokens
const tokenPair = await this.jwtService.generateTokenPair({
userId,
email: 'session@example.com',
roles: ['user'],
permissions: ['read'],
sessionId,
mfaVerified: false
});
// Verify session exists
const sessionTokens = await redis.smembers(`session_tokens:${sessionId}`);
if (sessionTokens.length === 0) {
throw new Error('Session tokens not properly tracked');
}
// Revoke session
await this.jwtService.revokeSession(sessionId);
// Verify session cleanup
const remainingTokens = await redis.smembers(`session_tokens:${sessionId}`);
if (remainingTokens.length > 0) {
throw new Error('Session tokens not properly cleaned up');
}
logger.info('Session management test passed');
}
private async testErrorHandlingSecurity(): Promise<void> {
// Test that errors don't leak sensitive information
try {
await this.jwtService.verifyAccessToken('malicious.token.attempt');
} catch (error) {
if (error.message.includes('secret') || error.message.includes('key')) {
throw new Error('Error message leaks sensitive information');
}
if (error.message !== 'Invalid or expired access token') {
throw new Error('Error message format inconsistent');
}
}
logger.info('Error handling security test passed');
}
private generateSummary() {
const totalTests = this.results.length;
const passed = this.results.filter(r => r.status === 'PASS').length;
const failed = this.results.filter(r => r.status === 'FAIL').length;
const criticalIssues = this.results.filter(r =>
r.status === 'FAIL' && r.securityImpact === 'CRITICAL'
).length;
return {
totalTests,
passed,
failed,
criticalIssues
};
}
async cleanup(): Promise<void> {
try {
await this.jwtService.shutdown();
// Additional cleanup if needed
logger.info('Security validator cleanup completed');
} catch (error) {
logger.error('Error during cleanup', { error });
}
}
}
// Main execution
async function main() {
const validator = new SecurityValidator();
try {
await validator.initialize();
const validationResults = await validator.runAllValidations();
// Print detailed results
console.log('\n=== SECURITY VALIDATION RESULTS ===\n');
validationResults.results.forEach(result => {
const status = result.status === 'PASS' ? '✅' : '❌';
console.log(`${status} ${result.testName}`);
console.log(` Duration: ${result.duration}ms`);
console.log(` Security Impact: ${result.securityImpact}`);
if (result.status === 'FAIL') {
console.log(` Error: ${result.details}`);
}
console.log('');
});
// Print performance metrics
console.log('\n=== PERFORMANCE METRICS ===\n');
validationResults.performanceMetrics.forEach(metric => {
console.log(`Operation: ${metric.operation}`);
console.log(` Avg Response Time: ${metric.avgResponseTime.toFixed(2)}ms`);
console.log(` Max Response Time: ${metric.maxResponseTime}ms`);
console.log(` Throughput: ${metric.throughput.toFixed(2)} ops/sec`);
console.log('');
});
// Print summary
console.log('\n=== SUMMARY ===\n');
console.log(`Total Tests: ${validationResults.summary.totalTests}`);
console.log(`Passed: ${validationResults.summary.passed}`);
console.log(`Failed: ${validationResults.summary.failed}`);
console.log(`Critical Issues: ${validationResults.summary.criticalIssues}`);
if (validationResults.summary.criticalIssues > 0) {
console.log('\n❌ CRITICAL SECURITY ISSUES DETECTED - DO NOT DEPLOY');
process.exit(1);
} else if (validationResults.summary.failed > 0) {
console.log('\n⚠️ SOME TESTS FAILED - REVIEW BEFORE DEPLOYMENT');
process.exit(1);
} else {
console.log('\n✅ ALL SECURITY VALIDATIONS PASSED - SAFE TO DEPLOY');
process.exit(0);
}
} catch (error) {
console.error('Security validation failed:', error);
process.exit(1);
} finally {
await validator.cleanup();
}
}
// Run the validation if this script is executed directly
if (require.main === module) {
main().catch(console.error);
}
export { SecurityValidator, ValidationResult, PerformanceMetrics };