Skip to main content
Glama
middleware.test.ts26.3 kB
import { AuthMiddleware } from '../../../src/auth/middleware'; import { JWTService } from '../../../src/auth/jwt-service'; import { MFAService } from '../../../src/auth/mfa-service'; import { MockFactory, TestDataGenerator } from '../../utils/test-helpers'; import { fixtures } from '../../utils/fixtures'; // Mock dependencies jest.mock('../../../src/config/config', () => ({ config: { env: 'test', session: { timeout: 3600000, // 1 hour }, 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/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 JWTService for socket authentication tests jest.mock('../../../src/auth/jwt-service', () => ({ JWTService: jest.fn().mockImplementation(() => ({ initialize: jest.fn(), isValidTokenFormat: jest.fn(), verifyAccessToken: jest.fn(), extractTokenFromHeader: jest.fn(), })), })); // Import after mocks are set up import { authenticateSocket } from '../../../src/auth/middleware'; describe('AuthMiddleware', () => { let authMiddleware: AuthMiddleware; let mockJWTService: jest.Mocked<JWTService>; let mockMFAService: jest.Mocked<MFAService>; let mockRedis: any; let mockLogger: any; beforeEach(() => { // Use fake timers to avoid timeout issues jest.useFakeTimers(); mockRedis = require('../../../src/database/redis').redis; mockLogger = require('../../../src/utils/logger').logger; // Mock JWT service mockJWTService = { extractTokenFromHeader: jest.fn(), isValidTokenFormat: jest.fn(), verifyAccessToken: jest.fn(), initialize: jest.fn(), } as any; // Mock MFA service mockMFAService = { isMFAEnabled: jest.fn(), } as any; authMiddleware = new AuthMiddleware(mockJWTService, mockMFAService); jest.clearAllMocks(); }); afterEach(() => { jest.useRealTimers(); }); describe('authenticate middleware', () => { it('should authenticate valid JWT token', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.headers.authorization = 'Bearer valid-jwt-token'; req.ip = '192.168.1.100'; const mockPayload = fixtures.jwt.validPayload; const mockSession = fixtures.sessions.validSession; mockJWTService.extractTokenFromHeader.mockReturnValue('valid-jwt-token'); mockJWTService.isValidTokenFormat.mockReturnValue(true); mockJWTService.verifyAccessToken.mockResolvedValue(mockPayload as any); mockRedis.get.mockResolvedValue(JSON.stringify(mockSession)); await authMiddleware.authenticate(req, res, next); expect(req.user).toBeDefined(); expect(req.user!.id).toBe(mockPayload.sub); expect(req.user!.email).toBe(mockPayload.email); expect(req.session).toBeDefined(); expect(next).toHaveBeenCalledWith(); }); it('should reject request without authorization header', async () => { const { req, res, next } = MockFactory.createMockExpress(); // No authorization header await authMiddleware.authenticate(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Authorization header required' }); expect(next).not.toHaveBeenCalled(); }); it('should reject invalid authorization header format', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.headers.authorization = 'Basic invalid-format'; mockJWTService.extractTokenFromHeader.mockReturnValue(null); await authMiddleware.authenticate(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid authorization header format' }); }); it('should reject invalid token format', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.headers.authorization = 'Bearer invalid-token'; mockJWTService.extractTokenFromHeader.mockReturnValue('invalid-token'); mockJWTService.isValidTokenFormat.mockReturnValue(false); await authMiddleware.authenticate(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid token format' }); }); it('should reject expired or invalid JWT', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.headers.authorization = 'Bearer expired-jwt-token'; mockJWTService.extractTokenFromHeader.mockReturnValue('expired-jwt-token'); mockJWTService.isValidTokenFormat.mockReturnValue(true); mockJWTService.verifyAccessToken.mockRejectedValue(new Error('Token expired')); await authMiddleware.authenticate(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Authentication failed' }); expect(mockLogger.warn).toHaveBeenCalledWith( 'Authentication failed', expect.objectContaining({ error: 'Token expired', }) ); }); it('should reject request with non-existent session', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.headers.authorization = 'Bearer valid-jwt-token'; const mockPayload = fixtures.jwt.validPayload; mockJWTService.extractTokenFromHeader.mockReturnValue('valid-jwt-token'); mockJWTService.isValidTokenFormat.mockReturnValue(true); mockJWTService.verifyAccessToken.mockResolvedValue(mockPayload as any); mockRedis.get.mockResolvedValue(null); // Session not found await authMiddleware.authenticate(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Session not found or expired' }); }); it('should update session activity on successful authentication', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.headers.authorization = 'Bearer valid-jwt-token'; req.ip = '192.168.1.100'; req.get = jest.fn().mockReturnValue('Mozilla/5.0 Test Browser'); const mockPayload = fixtures.jwt.validPayload; const mockSession = fixtures.sessions.validSession; mockJWTService.extractTokenFromHeader.mockReturnValue('valid-jwt-token'); mockJWTService.isValidTokenFormat.mockReturnValue(true); mockJWTService.verifyAccessToken.mockResolvedValue(mockPayload as any); mockRedis.get.mockResolvedValue(JSON.stringify(mockSession)); await authMiddleware.authenticate(req, res, next); expect(mockRedis.setex).toHaveBeenCalledWith( `session:${mockSession.id}`, 3600, // 1 hour in seconds expect.stringContaining(mockSession.userId) ); }); }); describe('requireMFA middleware', () => { it('should allow requests when MFA is not enabled', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = fixtures.users.validUser as any; mockMFAService.isMFAEnabled.mockResolvedValue(false); await authMiddleware.requireMFA(req, res, next); expect(next).toHaveBeenCalledWith(); }); it('should allow requests when MFA is verified', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.validUser, mfaVerified: true } as any; mockMFAService.isMFAEnabled.mockResolvedValue(true); await authMiddleware.requireMFA(req, res, next); expect(next).toHaveBeenCalledWith(); }); it('should reject requests when MFA is required but not verified', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.validUser, mfaVerified: false } as any; mockMFAService.isMFAEnabled.mockResolvedValue(true); await authMiddleware.requireMFA(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'MFA verification required', mfaRequired: true, }); }); it('should reject requests without user authentication', async () => { const { req, res, next } = MockFactory.createMockExpress(); // No req.user await authMiddleware.requireMFA(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' }); }); }); describe('requireRoles middleware', () => { it('should allow access with matching role', () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.validUser, roles: ['user', 'editor'] } as any; const middleware = authMiddleware.requireRoles('editor'); middleware(req, res, next); expect(next).toHaveBeenCalledWith(); }); it('should allow access with admin role (bypasses specific role check)', () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.adminUser, roles: ['admin'] } as any; const middleware = authMiddleware.requireRoles('superuser'); middleware(req, res, next); expect(next).toHaveBeenCalledWith(); }); it('should reject access without required role', () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.validUser, roles: ['user'] } as any; const middleware = authMiddleware.requireRoles('editor'); middleware(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Insufficient permissions' }); expect(mockLogger.warn).toHaveBeenCalledWith( 'Access denied - insufficient roles', expect.objectContaining({ requiredRoles: ['editor'], userRoles: ['user'], }) ); }); it('should handle multiple required roles', () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.validUser, roles: ['user', 'editor'] } as any; const middleware = authMiddleware.requireRoles('editor', 'moderator'); middleware(req, res, next); expect(next).toHaveBeenCalledWith(); }); it('should reject unauthenticated requests', () => { const { req, res, next } = MockFactory.createMockExpress(); // No req.user const middleware = authMiddleware.requireRoles('user'); middleware(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' }); }); }); describe('requirePermissions middleware', () => { it('should allow access with exact permission', () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.validUser, permissions: ['read', 'write'] } as any; const middleware = authMiddleware.requirePermissions('write'); middleware(req, res, next); expect(next).toHaveBeenCalledWith(); }); it('should allow access with wildcard permission', () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.validUser, permissions: ['*'] } as any; const middleware = authMiddleware.requirePermissions('anything'); middleware(req, res, next); expect(next).toHaveBeenCalledWith(); }); it('should allow access with prefix wildcard permission', () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.validUser, permissions: ['admin:*'] } as any; const middleware = authMiddleware.requirePermissions('admin:users:create'); middleware(req, res, next); expect(next).toHaveBeenCalledWith(); }); it('should reject access without required permission', () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.validUser, permissions: ['read'] } as any; const middleware = authMiddleware.requirePermissions('write'); middleware(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Insufficient permissions' }); expect(mockLogger.warn).toHaveBeenCalledWith( 'Access denied - insufficient permissions', expect.objectContaining({ requiredPermissions: ['write'], userPermissions: ['read'], }) ); }); it('should handle multiple required permissions (OR logic)', () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = { ...fixtures.users.validUser, permissions: ['read'] } as any; const middleware = authMiddleware.requirePermissions('write', 'read', 'delete'); middleware(req, res, next); expect(next).toHaveBeenCalledWith(); // Should pass because user has 'read' }); }); describe('optionalAuth middleware', () => { it('should set user when valid token is provided', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.headers.authorization = 'Bearer valid-jwt-token'; const mockPayload = fixtures.jwt.validPayload; const mockSession = fixtures.sessions.validSession; mockJWTService.extractTokenFromHeader.mockReturnValue('valid-jwt-token'); mockJWTService.verifyAccessToken.mockResolvedValue(mockPayload as any); mockRedis.get.mockResolvedValue(JSON.stringify(mockSession)); await authMiddleware.optionalAuth(req, res, next); expect(req.user).toBeDefined(); expect(next).toHaveBeenCalledWith(); }); it('should continue without user when no token is provided', async () => { const { req, res, next } = MockFactory.createMockExpress(); // No authorization header await authMiddleware.optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalledWith(); }); it('should continue without user when invalid token is provided', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.headers.authorization = 'Bearer invalid-token'; mockJWTService.extractTokenFromHeader.mockReturnValue('invalid-token'); mockJWTService.verifyAccessToken.mockRejectedValue(new Error('Invalid token')); await authMiddleware.optionalAuth(req, res, next); expect(req.user).toBeNull(); expect(next).toHaveBeenCalledWith(); expect(mockLogger.debug).toHaveBeenCalledWith( 'Optional authentication failed', expect.objectContaining({ error: 'Invalid token', }) ); }); }); describe('userRateLimit middleware', () => { it('should allow requests within rate limit', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = fixtures.users.validUser as any; mockRedis.incr.mockResolvedValue(5); mockRedis.expire.mockResolvedValue(1); const middleware = authMiddleware.userRateLimit(100, 60000); await middleware(req, res, next); expect(res.set).toHaveBeenCalledWith({ 'X-RateLimit-Limit': '100', 'X-RateLimit-Remaining': '95', }); expect(next).toHaveBeenCalledWith(); }); it('should reject requests exceeding rate limit', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = fixtures.users.validUser as any; mockRedis.incr.mockResolvedValue(101); mockRedis.ttl.mockResolvedValue(30); const middleware = authMiddleware.userRateLimit(100, 60000); await middleware(req, res, next); expect(res.status).toHaveBeenCalledWith(429); expect(res.json).toHaveBeenCalledWith({ error: 'Too many requests', retryAfter: 30, }); expect(mockLogger.warn).toHaveBeenCalledWith( 'Rate limit exceeded', expect.objectContaining({ current: 101, max: 100, }) ); }); it('should use IP address for unauthenticated requests', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.ip = '192.168.1.100'; // No req.user mockRedis.incr.mockResolvedValue(5); const middleware = authMiddleware.userRateLimit(100, 60000); await middleware(req, res, next); expect(mockRedis.incr).toHaveBeenCalledWith('rate_limit:192.168.1.100'); expect(next).toHaveBeenCalledWith(); }); it('should handle Redis errors gracefully', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.user = fixtures.users.validUser as any; mockRedis.incr.mockRejectedValue(new Error('Redis error')); const middleware = authMiddleware.userRateLimit(100, 60000); await middleware(req, res, next); expect(mockLogger.error).toHaveBeenCalledWith( 'Rate limiting error', expect.objectContaining({ error: expect.any(Error), }) ); expect(next).toHaveBeenCalledWith(); // Should continue despite error }); }); describe('checkSessionTimeout middleware', () => { it('should allow requests with active session', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.session = { ...fixtures.sessions.validSession, lastActivity: new Date(Date.now() - 1800000), // 30 minutes ago } as any; await authMiddleware.checkSessionTimeout(req, res, next); expect(next).toHaveBeenCalledWith(); }); it('should reject requests with expired session', async () => { const { req, res, next } = MockFactory.createMockExpress(); req.session = { ...fixtures.sessions.validSession, lastActivity: new Date(Date.now() - 7200000), // 2 hours ago } as any; req.user = fixtures.users.validUser as any; await authMiddleware.checkSessionTimeout(req, res, next); expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ error: 'Session expired', sessionExpired: true, }); expect(mockRedis.del).toHaveBeenCalledWith(`session:${req.session.id}`); }); it('should continue when no session exists', async () => { const { req, res, next } = MockFactory.createMockExpress(); // No req.session await authMiddleware.checkSessionTimeout(req, res, next); expect(next).toHaveBeenCalledWith(); }); }); describe('ipWhitelist middleware', () => { it('should allow requests from whitelisted IP', () => { const { req, res, next } = MockFactory.createMockExpress(); req.ip = '192.168.1.100'; const middleware = authMiddleware.ipWhitelist(['192.168.1.100', '10.0.0.1']); middleware(req, res, next); expect(next).toHaveBeenCalledWith(); }); it('should reject requests from non-whitelisted IP', () => { const { req, res, next } = MockFactory.createMockExpress(); req.ip = '192.168.1.200'; req.user = fixtures.users.validUser as any; const middleware = authMiddleware.ipWhitelist(['192.168.1.100', '10.0.0.1']); middleware(req, res, next); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Access denied from this IP address' }); expect(mockLogger.warn).toHaveBeenCalledWith( 'Access denied - IP not whitelisted', expect.objectContaining({ clientIP: '192.168.1.200', allowedIPs: ['192.168.1.100', '10.0.0.1'], }) ); }); }); describe('session management methods', () => { it('should create new session', async () => { const userId = 'user-123'; const ipAddress = '192.168.1.100'; const userAgent = 'Mozilla/5.0 Test'; const session = await authMiddleware.createSession(userId, ipAddress, userAgent); expect(session.userId).toBe(userId); expect(session.ipAddress).toBe(ipAddress); expect(session.userAgent).toBe(userAgent); expect(session.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); expect(mockRedis.setex).toHaveBeenCalledWith( `session:${session.id}`, 3600, // 1 hour expect.stringContaining(userId) ); expect(mockRedis.sadd).toHaveBeenCalledWith( `user_sessions:${userId}`, session.id ); }); it('should get existing session', async () => { const sessionId = 'session-123'; const sessionData = fixtures.sessions.validSession; mockRedis.get.mockResolvedValue(JSON.stringify(sessionData)); const session = await authMiddleware.getSession(sessionId); expect(session).toBeDefined(); expect(session!.id).toBe(sessionData.id); expect(session!.userId).toBe(sessionData.userId); }); it('should return null for non-existent session', async () => { const sessionId = 'non-existent'; mockRedis.get.mockResolvedValue(null); const session = await authMiddleware.getSession(sessionId); expect(session).toBeNull(); }); it('should delete session', async () => { const sessionId = 'session-123'; const sessionData = fixtures.sessions.validSession; mockRedis.get.mockResolvedValue(JSON.stringify(sessionData)); await authMiddleware.deleteSession(sessionId); expect(mockRedis.del).toHaveBeenCalledWith(`session:${sessionId}`); expect(mockRedis.srem).toHaveBeenCalledWith( `user_sessions:${sessionData.userId}`, sessionId ); }); it('should delete all user sessions', async () => { const userId = 'user-123'; const sessionIds = ['session-1', 'session-2', 'session-3']; mockRedis.smembers.mockResolvedValue(sessionIds); await authMiddleware.deleteAllUserSessions(userId); sessionIds.forEach(sessionId => { expect(mockRedis.del).toHaveBeenCalledWith(`session:${sessionId}`); }); expect(mockRedis.del).toHaveBeenCalledWith(`user_sessions:${userId}`); }); }); }); describe('authenticateSocket', () => { let mockSocket: any; let validJWTToken: string; let mockJWTServiceInstance: any; beforeEach(() => { // Create a valid JWT token format (3 base64url parts) validJWTToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; mockSocket = MockFactory.createMockSocket(); mockSocket.handshake = { auth: { token: validJWTToken }, headers: {}, }; // Get the mocked instance methods const MockedJWTService = jest.mocked(JWTService); mockJWTServiceInstance = { initialize: jest.fn(), isValidTokenFormat: jest.fn(), verifyAccessToken: jest.fn(), }; MockedJWTService.mockImplementation(() => mockJWTServiceInstance); jest.clearAllMocks(); }); it('should authenticate socket with valid token in auth', async () => { const mockPayload = fixtures.jwt.validPayload; mockJWTServiceInstance.isValidTokenFormat.mockReturnValue(true); mockJWTServiceInstance.verifyAccessToken.mockResolvedValue(mockPayload); const user = await authenticateSocket(mockSocket); expect(user.id).toBe(mockPayload.sub); expect(user.email).toBe(mockPayload.email); }); it('should authenticate socket with token in headers', async () => { const mockPayload = fixtures.jwt.validPayload; mockSocket.handshake.auth = {}; mockSocket.handshake.headers.authorization = `Bearer ${validJWTToken}`; mockJWTServiceInstance.isValidTokenFormat.mockReturnValue(true); mockJWTServiceInstance.verifyAccessToken.mockResolvedValue(mockPayload); const user = await authenticateSocket(mockSocket); expect(user.id).toBe(mockPayload.sub); }); it('should reject socket without token', async () => { mockSocket.handshake.auth = {}; mockSocket.handshake.headers = {}; await expect(authenticateSocket(mockSocket)) .rejects.toThrow('No authentication token provided'); }); it('should reject socket with invalid token format', async () => { mockSocket.handshake.auth.token = 'invalid-token'; mockJWTServiceInstance.isValidTokenFormat.mockReturnValue(false); await expect(authenticateSocket(mockSocket)) .rejects.toThrow('Invalid token format'); }); it('should reject socket with invalid token', async () => { mockSocket.handshake.auth.token = validJWTToken; mockJWTServiceInstance.isValidTokenFormat.mockReturnValue(true); mockJWTServiceInstance.verifyAccessToken.mockRejectedValue(new Error('Token expired')); await expect(authenticateSocket(mockSocket)) .rejects.toThrow('Token expired'); }); });

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