Skip to main content
Glama
jwt-race-condition.test.ts16.3 kB
import { JWTService, JWTPayload, TokenPair } 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 { MockFactory, TestDataGenerator } from '../../utils/test-helpers'; import { fixtures } from '../../utils/fixtures'; import jwt from 'jsonwebtoken'; import crypto from 'crypto'; // Mock dependencies jest.mock('../../../src/config/config', () => ({ config: { env: 'test', jwt: { secret: 'test-jwt-secret-must-be-at-least-32-characters-long', accessExpiresIn: '15m', refreshExpiresIn: '7d', issuer: 'secure-mcp-server', audience: 'secure-mcp-client', }, }, })); jest.mock('../../../src/database/redis', () => ({ redis: { get: jest.fn(), set: jest.fn(), setex: jest.fn(), del: jest.fn(), incr: jest.fn(), expire: jest.fn(), ttl: jest.fn(), sadd: jest.fn(), srem: jest.fn(), smembers: jest.fn(), hgetall: jest.fn(), hget: jest.fn(), hset: jest.fn(), keys: jest.fn(), scard: jest.fn(), scan: jest.fn(), memory: jest.fn(), exists: jest.fn(), ping: jest.fn(() => Promise.resolve('PONG')), disconnect: jest.fn(() => Promise.resolve()), pipeline: jest.fn(() => ({ setex: jest.fn().mockReturnThis(), sadd: jest.fn().mockReturnThis(), srem: jest.fn().mockReturnThis(), del: jest.fn().mockReturnThis(), expire: jest.fn().mockReturnThis(), ttl: jest.fn().mockReturnThis(), exec: jest.fn().mockResolvedValue([]), })), }, })); jest.mock('../../../src/security/vault', () => ({ vault: { read: jest.fn(), write: jest.fn(), health: jest.fn(() => Promise.resolve({ status: 'ok' })), delete: jest.fn(), }, })); jest.mock('../../../src/utils/logger', () => ({ logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), trace: jest.fn(), child: jest.fn(() => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), trace: jest.fn(), })), }, })); // Mock the security modules jest.mock('../../../src/auth/distributed-lock', () => ({ distributedLock: { acquireLock: jest.fn(), releaseLock: jest.fn(), }, })); jest.mock('../../../src/auth/token-blacklist', () => ({ tokenBlacklist: { initialize: jest.fn(), isBlacklisted: jest.fn(), addToBlacklist: jest.fn(), blacklistAllUserTokens: jest.fn(), shutdown: jest.fn(), }, })); jest.mock('../../../src/auth/token-rate-limiter', () => ({ tokenRateLimiter: { checkValidationLimit: jest.fn(), checkRefreshLimit: jest.fn(), checkGenerationLimit: jest.fn(), checkRevocationLimit: jest.fn(), isUserBlocked: jest.fn(), }, })); describe('JWT Service - Race Condition Security Tests', () => { let jwtService: JWTService; let mockRedis: any; let mockDistributedLock: any; let mockTokenBlacklist: any; let mockTokenRateLimiter: any; beforeEach(() => { jwtService = new JWTService(); mockRedis = require('../../../src/database/redis').redis; mockDistributedLock = distributedLock; mockTokenBlacklist = tokenBlacklist; mockTokenRateLimiter = tokenRateLimiter; // Reset all mocks jest.clearAllMocks(); // Setup default mock implementations mockTokenRateLimiter.checkValidationLimit.mockResolvedValue({ allowed: true, remaining: 100 }); mockTokenRateLimiter.checkRefreshLimit.mockResolvedValue({ allowed: true, remaining: 10 }); mockTokenRateLimiter.checkGenerationLimit.mockResolvedValue({ allowed: true, remaining: 5 }); mockTokenRateLimiter.checkRevocationLimit.mockResolvedValue({ allowed: true, remaining: 20 }); mockTokenRateLimiter.isUserBlocked.mockResolvedValue(false); mockTokenBlacklist.initialize.mockResolvedValue(undefined); mockTokenBlacklist.isBlacklisted.mockResolvedValue(false); mockTokenBlacklist.addToBlacklist.mockResolvedValue(undefined); mockDistributedLock.acquireLock.mockResolvedValue({ key: 'test-lock', value: 'test-value', ttl: 5000, acquired: true }); mockDistributedLock.releaseLock.mockResolvedValue(true); }); describe('Race Condition Prevention', () => { beforeEach(async () => { await jwtService.initialize(); }); it('should prevent concurrent refresh token attacks', async () => { const userPayload = TestDataGenerator.generateUser(); // Generate initial token pair const originalTokenPair = await jwtService.generateTokenPair({ userId: userPayload.id, email: userPayload.email, roles: userPayload.roles, permissions: userPayload.permissions, sessionId: 'session-123', mfaVerified: false, }); // Mock token family data const familyData = { userId: userPayload.id, sessionId: 'session-123', createdAt: new Date().toISOString(), jti: jwt.decode(originalTokenPair.accessToken)?.jti }; mockRedis.get .mockResolvedValueOnce(JSON.stringify(familyData)) // First request - family exists .mockResolvedValueOnce(JSON.stringify(familyData)) // User data lookup .mockResolvedValueOnce(null); // Second request - family already used mockRedis.hgetall.mockResolvedValue({ email: userPayload.email, roles: JSON.stringify(userPayload.roles), permissions: JSON.stringify(userPayload.permissions), mfaVerified: 'false', }); // Simulate concurrent refresh attempts const refreshPromises = [ jwtService.refreshAccessToken(originalTokenPair.refreshToken), jwtService.refreshAccessToken(originalTokenPair.refreshToken) ]; // First should succeed, second should fail due to distributed lock const results = await Promise.allSettled(refreshPromises); // One should succeed, one should fail const successful = results.filter(r => r.status === 'fulfilled').length; const failed = results.filter(r => r.status === 'rejected').length; expect(successful).toBe(1); expect(failed).toBe(1); // Verify distributed lock was acquired expect(mockDistributedLock.acquireLock).toHaveBeenCalledWith( expect.stringMatching(/token_refresh:/), expect.objectContaining({ ttl: 10000, maxRetries: 5, retryDelay: 100 }) ); // Verify lock was released expect(mockDistributedLock.releaseLock).toHaveBeenCalled(); }); it('should detect token replay attacks', async () => { const userPayload = TestDataGenerator.generateUser(); const originalTokenPair = await jwtService.generateTokenPair({ userId: userPayload.id, email: userPayload.email, roles: userPayload.roles, permissions: userPayload.permissions, sessionId: 'session-123', mfaVerified: false, }); // Mock token family with different JTI (replay attack scenario) const familyData = { userId: userPayload.id, sessionId: 'session-123', createdAt: new Date().toISOString(), jti: 'different-jti' // This doesn't match the refresh token JTI }; mockRedis.get.mockResolvedValue(JSON.stringify(familyData)); // Should detect potential replay attack await expect( jwtService.refreshAccessToken(originalTokenPair.refreshToken) ).rejects.toThrow('Invalid refresh token - security violation detected'); // Verify token family is invalidated expect(mockTokenBlacklist.addToBlacklist).toHaveBeenCalledWith( 'different-jti', userPayload.id, expect.any(Date), 'potential_replay_attack' ); }); it('should handle high concurrency refresh attempts gracefully', async () => { const userPayload = TestDataGenerator.generateUser(); const originalTokenPair = await jwtService.generateTokenPair({ userId: userPayload.id, email: userPayload.email, roles: userPayload.roles, permissions: userPayload.permissions, sessionId: 'session-123', mfaVerified: false, }); // Mock token family data const familyData = { userId: userPayload.id, sessionId: 'session-123', createdAt: new Date().toISOString(), jti: jwt.decode(originalTokenPair.accessToken)?.jti }; mockRedis.get.mockResolvedValue(JSON.stringify(familyData)); mockRedis.hgetall.mockResolvedValue({ email: userPayload.email, roles: JSON.stringify(userPayload.roles), permissions: JSON.stringify(userPayload.permissions), mfaVerified: 'false', }); // Simulate many concurrent attempts const concurrentAttempts = 50; const refreshPromises = Array.from({ length: concurrentAttempts }, () => jwtService.refreshAccessToken(originalTokenPair.refreshToken) ); const results = await Promise.allSettled(refreshPromises); // Only one should succeed due to distributed locking const successful = results.filter(r => r.status === 'fulfilled').length; expect(successful).toBeLessThanOrEqual(1); // Verify distributed lock was used expect(mockDistributedLock.acquireLock).toHaveBeenCalled(); }); it('should prevent session hijacking through token validation', async () => { const userPayload = TestDataGenerator.generateUser(); const tokenPair = await jwtService.generateTokenPair({ userId: userPayload.id, email: userPayload.email, roles: userPayload.roles, permissions: userPayload.permissions, sessionId: 'session-123', mfaVerified: false, }); // Mock token as blacklisted (compromised) mockTokenBlacklist.isBlacklisted.mockResolvedValue(true); // Should reject blacklisted token await expect( jwtService.verifyAccessToken(tokenPair.accessToken) ).rejects.toThrow('Invalid or expired access token'); // Verify blacklist check was performed expect(mockTokenBlacklist.isBlacklisted).toHaveBeenCalledWith( expect.any(String) ); }); it('should enforce rate limits to prevent brute force attacks', async () => { const userPayload = TestDataGenerator.generateUser(); // Mock rate limit exceeded mockTokenRateLimiter.checkValidationLimit.mockResolvedValue({ allowed: false, remaining: 0 }); const tokenPair = await jwtService.generateTokenPair({ userId: userPayload.id, email: userPayload.email, roles: userPayload.roles, permissions: userPayload.permissions, sessionId: 'session-123', mfaVerified: false, }); // Should reject due to rate limiting await expect( jwtService.verifyAccessToken(tokenPair.accessToken) ).rejects.toThrow('Rate limit exceeded for token validation'); expect(mockTokenRateLimiter.checkValidationLimit).toHaveBeenCalled(); }); it('should block users after security violations', async () => { const userPayload = TestDataGenerator.generateUser(); // Mock user as blocked mockTokenRateLimiter.isUserBlocked.mockResolvedValue(true); // Should reject token generation for blocked user await expect( jwtService.generateTokenPair({ userId: userPayload.id, email: userPayload.email, roles: userPayload.roles, permissions: userPayload.permissions, sessionId: 'session-123', mfaVerified: false, }) ).rejects.toThrow('User is temporarily blocked from token operations'); }); }); describe('Atomic Operations', () => { beforeEach(async () => { await jwtService.initialize(); }); it('should handle Redis failures gracefully during token operations', async () => { const userPayload = TestDataGenerator.generateUser(); // Mock Redis pipeline failure mockRedis.pipeline.mockReturnValue({ setex: jest.fn().mockReturnThis(), sadd: jest.fn().mockReturnThis(), expire: jest.fn().mockReturnThis(), exec: jest.fn().mockRejectedValue(new Error('Redis connection failed')) }); await expect( jwtService.generateTokenPair({ userId: userPayload.id, email: userPayload.email, roles: userPayload.roles, permissions: userPayload.permissions, sessionId: 'session-123', mfaVerified: false, }) ).rejects.toThrow('Redis connection failed'); }); it('should ensure token family consistency during refresh', async () => { const userPayload = TestDataGenerator.generateUser(); const originalTokenPair = await jwtService.generateTokenPair({ userId: userPayload.id, email: userPayload.email, roles: userPayload.roles, permissions: userPayload.permissions, sessionId: 'session-123', mfaVerified: false, }); // Mock family data const familyData = { userId: userPayload.id, sessionId: 'session-123', createdAt: new Date().toISOString(), jti: jwt.decode(originalTokenPair.accessToken)?.jti }; mockRedis.get .mockResolvedValueOnce(JSON.stringify(familyData)) .mockResolvedValueOnce(JSON.stringify(familyData)); mockRedis.hgetall.mockResolvedValue({ email: userPayload.email, roles: JSON.stringify(userPayload.roles), permissions: JSON.stringify(userPayload.permissions), mfaVerified: 'false', }); const newTokenPair = await jwtService.refreshAccessToken(originalTokenPair.refreshToken); // Verify old tokens are blacklisted expect(mockTokenBlacklist.addToBlacklist).toHaveBeenCalledWith( expect.any(String), userPayload.id, expect.any(Date), 'token_refreshed' ); // Verify new token pair is generated expect(newTokenPair.accessToken).toBeDefined(); expect(newTokenPair.refreshToken).toBeDefined(); expect(newTokenPair.accessToken).not.toBe(originalTokenPair.accessToken); }); }); describe('Security Monitoring', () => { beforeEach(async () => { await jwtService.initialize(); }); it('should log security events for monitoring', async () => { const userPayload = TestDataGenerator.generateUser(); const mockLogger = require('../../../src/utils/logger').logger; await jwtService.generateTokenPair({ userId: userPayload.id, email: userPayload.email, roles: userPayload.roles, permissions: userPayload.permissions, sessionId: 'session-123', mfaVerified: false, }); // Verify security events are logged expect(mockLogger.info).toHaveBeenCalledWith( 'Token pair generated securely', expect.objectContaining({ userId: userPayload.id, sessionId: 'session-123' }) ); }); it('should track failed authentication attempts', async () => { const mockLogger = require('../../../src/utils/logger').logger; await expect( jwtService.verifyAccessToken('invalid-token') ).rejects.toThrow(); // Verify failed attempts are logged expect(mockLogger.warn).toHaveBeenCalledWith( 'Access token verification failed', expect.objectContaining({ error: expect.any(String) }) ); }); }); describe('Cleanup and Resource Management', () => { it('should properly shutdown and cleanup resources', async () => { await jwtService.initialize(); await jwtService.shutdown(); expect(mockTokenBlacklist.shutdown).toHaveBeenCalled(); }); it('should handle initialization failures gracefully', async () => { mockTokenBlacklist.initialize.mockRejectedValue(new Error('Initialization failed')); await expect(jwtService.initialize()).rejects.toThrow('Initialization failed'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/perfecxion-ai/secure-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server