/** * @jest-environment node * Comprehensive tests for auth-manager.js * Tests JWT generation/validation, API key management, and security boundaries */ const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const AuthManager = require('../auth-manager'); const credentialManager = require('../credential-manager'); // Mock credential manager jest.mock('../credential-manager'); jest.mock('../logger-utils', () => ({ safeLog: jest.fn() })); describe('AuthManager', () => { let authManager; beforeEach(() => { authManager = new AuthManager(); jest.clearAllMocks(); credentialManager.save.mockResolvedValue(true); credentialManager.get.mockResolvedValue(null); credentialManager.delete.mockResolvedValue(true); credentialManager.list.mockResolvedValue([]); }); describe('JWT Generation', () => { test('should generate valid JWT token', async () => { const payload = { sub: 'user123', role: 'admin' }; const token = await authManager.generateJWT(payload); expect(token).toBeDefined(); expect(typeof token).toBe('string'); expect(token.split('.')).toHaveLength(3); // JWT has 3 parts }); test('should include required claims in JWT', async () => { const payload = { sub: 'user123', role: 'admin' }; const token = await authManager.generateJWT(payload); const decoded = jwt.decode(token); expect(decoded.sub).toBe('user123'); expect(decoded.role).toBe('admin'); expect(decoded.iat).toBeDefined(); expect(decoded.exp).toBeDefined(); expect(decoded.scope).toEqual(['read', 'write']); // default scopes }); test('should respect custom expiration time', async () => { const payload = { sub: 'user123' }; const token = await authManager.generateJWT(payload, '1h'); const decoded = jwt.decode(token); const expectedExp = decoded.iat + 3600; // 1 hour = 3600 seconds expect(decoded.exp).toBeCloseTo(expectedExp, -1); // Allow 1 sec tolerance }); test('should include custom scopes', async () => { const payload = { sub: 'user123', scope: ['read'] }; const token = await authManager.generateJWT(payload); const decoded = jwt.decode(token); expect(decoded.scope).toEqual(['read']); }); test('should reject JWT generation without sub claim', async () => { const payload = { role: 'admin' }; // Missing sub await expect(authManager.generateJWT(payload)) .rejects.toThrow('JWT payload must include "sub"'); }); test('should reject JWT generation with null payload', async () => { await expect(authManager.generateJWT(null)) .rejects.toThrow(); }); }); describe('JWT Verification', () => { test('should verify valid JWT token', async () => { const payload = { sub: 'user123', role: 'admin' }; const token = await authManager.generateJWT(payload); const verified = await authManager.verifyJWT(token); expect(verified).toBeDefined(); expect(verified.userId).toBe('user123'); expect(verified.scope).toEqual(['read', 'write']); expect(verified.iat).toBeDefined(); expect(verified.exp).toBeDefined(); }); test('should reject invalid JWT token', async () => { const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid.signature'; const verified = await authManager.verifyJWT(invalidToken); expect(verified).toBeNull(); }); test('should reject malformed JWT token', async () => { const verified = await authManager.verifyJWT('not-a-jwt-token'); expect(verified).toBeNull(); }); test('should reject expired JWT token', async () => { const payload = { sub: 'user123' }; const token = await authManager.generateJWT(payload, '-1s'); // Already expired // Wait a tiny bit to ensure expiration await new Promise(resolve => setTimeout(resolve, 100)); const verified = await authManager.verifyJWT(token); expect(verified).toBeNull(); }); test('should reject JWT with wrong signature', async () => { const payload = { sub: 'user123' }; const wrongSecret = 'wrong-secret-key'; const token = jwt.sign(payload, wrongSecret); const verified = await authManager.verifyJWT(token); expect(verified).toBeNull(); }); test('should handle empty token gracefully', async () => { const verified = await authManager.verifyJWT(''); expect(verified).toBeNull(); }); test('should handle null token gracefully', async () => { const verified = await authManager.verifyJWT(null); expect(verified).toBeNull(); }); }); describe('API Key Generation', () => { test('should generate valid API key', async () => { const result = await authManager.generateAPIKey('test-key'); expect(result).toBeDefined(); expect(result.key).toMatch(/^dk_[a-f0-9]{32}_[a-f0-9]{64}$/); expect(result.id).toMatch(/^[a-f0-9]{32}$/); expect(result.name).toBe('test-key'); expect(result.scopes).toEqual(['read', 'write']); expect(result.createdAt).toBeDefined(); }); test('should generate unique API keys', async () => { const key1 = await authManager.generateAPIKey('key1'); const key2 = await authManager.generateAPIKey('key2'); expect(key1.key).not.toBe(key2.key); expect(key1.id).not.toBe(key2.id); }); test('should accept custom scopes', async () => { const result = await authManager.generateAPIKey('readonly-key', ['read']); expect(result.scopes).toEqual(['read']); }); test('should store API key in credential manager', async () => { await authManager.generateAPIKey('test-key'); expect(credentialManager.save).toHaveBeenCalledWith( expect.stringMatching(/^auth\.apikey\./), expect.objectContaining({ keySecret: expect.any(String) }) ); }); test('should store API key metadata', async () => { await authManager.generateAPIKey('test-key', ['read']); expect(credentialManager.save).toHaveBeenCalledWith( expect.stringMatching(/^auth\.metadata\./), expect.objectContaining({ name: 'test-key', scopes: ['read'], createdAt: expect.any(String) }) ); }); test('should reject API key generation without name', async () => { await expect(authManager.generateAPIKey('')) .rejects.toThrow('API key name is required'); await expect(authManager.generateAPIKey(null)) .rejects.toThrow('API key name is required'); await expect(authManager.generateAPIKey(undefined)) .rejects.toThrow('API key name is required'); }); test('should reject non-string name', async () => { await expect(authManager.generateAPIKey(123)) .rejects.toThrow('API key name is required'); await expect(authManager.generateAPIKey({})) .rejects.toThrow('API key name is required'); }); }); describe('API Key Validation', () => { test('should validate correct API key', async () => { const { key, id } = await authManager.generateAPIKey('test-key'); // Mock credential manager to return the stored key credentialManager.get.mockResolvedValueOnce({ keySecret: key.split('_')[2] }); credentialManager.get.mockResolvedValueOnce({ name: 'test-key', scopes: ['read', 'write'], createdAt: new Date().toISOString() }); const validated = await authManager.validateAPIKey(key); expect(validated).toBeDefined(); expect(validated.valid).toBe(true); expect(validated.keyId).toBe(id); expect(validated.scopes).toEqual(['read', 'write']); }); test('should reject malformed API key', async () => { const validated = await authManager.validateAPIKey('not-an-api-key'); expect(validated.valid).toBe(false); }); test('should reject API key with wrong prefix', async () => { const validated = await authManager.validateAPIKey('sk_abc123_def456'); expect(validated.valid).toBe(false); }); test('should reject non-existent API key', async () => { const fakeKey = 'dk_' + crypto.randomBytes(16).toString('hex') + '_' + crypto.randomBytes(32).toString('hex'); credentialManager.get.mockResolvedValue(null); // Key doesn't exist const validated = await authManager.validateAPIKey(fakeKey); expect(validated.valid).toBe(false); }); test('should reject revoked API key', async () => { const { key } = await authManager.generateAPIKey('test-key'); credentialManager.get.mockResolvedValueOnce({ keySecret: key.split('_')[2], revoked: true // Key is revoked }); const validated = await authManager.validateAPIKey(key); expect(validated.valid).toBe(false); }); test('should handle null key gracefully', async () => { const validated = await authManager.validateAPIKey(null); expect(validated.valid).toBe(false); }); test('should handle empty key gracefully', async () => { const validated = await authManager.validateAPIKey(''); expect(validated.valid).toBe(false); }); }); describe('API Key Revocation', () => { test('should revoke API key', async () => { const { id } = await authManager.generateAPIKey('test-key'); credentialManager.get.mockResolvedValue({ keySecret: 'test-secret' }); const revoked = await authManager.revokeAPIKey(id); expect(revoked).toBe(true); expect(credentialManager.save).toHaveBeenCalledWith( `auth.apikey.${id}`, expect.objectContaining({ revoked: true, revokedAt: expect.any(String) }) ); }); test('should reject revoking non-existent key', async () => { credentialManager.get.mockResolvedValue(null); await expect(authManager.revokeAPIKey('nonexistent')) .rejects.toThrow(); }); }); describe('API Key Listing', () => { test('should list all API keys with metadata', async () => { credentialManager.list.mockResolvedValue([ 'auth.metadata.key1', 'auth.metadata.key2' ]); credentialManager.get.mockResolvedValueOnce({ name: 'Key 1', scopes: ['read'], createdAt: '2026-01-01T00:00:00Z' }); credentialManager.get.mockResolvedValueOnce({ name: 'Key 2', scopes: ['read', 'write'], createdAt: '2026-01-02T00:00:00Z' }); const keys = await authManager.listAPIKeys(); expect(keys).toHaveLength(2); expect(keys[0].name).toBe('Key 1'); expect(keys[1].name).toBe('Key 2'); }); test('should return empty array when no keys exist', async () => { credentialManager.list.mockResolvedValue([]); const keys = await authManager.listAPIKeys(); expect(keys).toEqual([]); }); }); describe('Security Boundaries', () => { test('should not log sensitive token data', async () => { const payload = { sub: 'user123' }; const token = await authManager.generateJWT(payload); // Logger should never be called with the actual token const { safeLog } = require('../logger-utils'); const calls = safeLog.mock.calls.flat(); expect(calls.some(arg => String(arg).includes(token))).toBe(false); }); test('should not log API key secrets', async () => { const { key } = await authManager.generateAPIKey('test-key'); const { safeLog } = require('../logger-utils'); const calls = safeLog.mock.calls.flat(); expect(calls.some(arg => String(arg).includes(key))).toBe(false); }); test('should generate cryptographically secure API keys', async () => { const key1 = await authManager.generateAPIKey('key1'); const key2 = await authManager.generateAPIKey('key2'); // Keys should be unrelated (not sequential) expect(parseInt(key1.id, 16)).not.toBe(parseInt(key2.id, 16) + 1); expect(parseInt(key1.id, 16)).not.toBe(parseInt(key2.id, 16) - 1); }); }); });