/** * Security Features Test Suite * * Tests for CSRF protection, token encryption, and error handling */ import { generateState, validateState, generateSessionId, } from '../../src/utils/state-utils'; import { encryptToken, decryptToken, validateEncryptionSecret, } from '../../src/utils/encryption-utils'; import { createOAuthError, handleProviderError, OAuthErrorCodes, } from '../../src/utils/error-utils'; import { validateProvider, validateResponseType, validateRedirectUri, checkOAuthErrorParam, } from '../../src/utils/validation-utils'; describe('Security Features', () => { describe('CSRF Protection - State Parameter', () => { it('should generate a 64-character hex state parameter', () => { const state = generateState(); expect(state).toBeDefined(); expect(typeof state).toBe('string'); expect(state.length).toBe(64); // 32 bytes = 64 hex chars expect(/^[0-9a-f]{64}$/.test(state)).toBe(true); }); it('should generate unique state parameters', () => { const state1 = generateState(); const state2 = generateState(); expect(state1).not.toBe(state2); }); it('should validate matching state parameters', () => { const state = generateState(); const isValid = validateState(state, state); expect(isValid).toBe(true); }); it('should reject invalid state parameters', () => { const state1 = generateState(); const state2 = generateState(); const isValid = validateState(state1, state2); expect(isValid).toBe(false); }); it('should reject empty state parameters', () => { const state = generateState(); expect(validateState('', state)).toBe(false); expect(validateState(state, '')).toBe(false); expect(validateState('', '')).toBe(false); }); it('should reject tampered state parameters', () => { const state = generateState(); const tamperedState = state.substring(0, state.length - 1) + 'x'; const isValid = validateState(tamperedState, state); expect(isValid).toBe(false); }); it('should generate session IDs for OAuth flow tracking', () => { const sessionId = generateSessionId(); expect(sessionId).toBeDefined(); expect(typeof sessionId).toBe('string'); expect(sessionId.length).toBe(32); // 16 bytes = 32 hex chars }); }); describe('Token Encryption', () => { const testSecret = 'this-is-a-test-secret-that-is-at-least-32-characters-long'; const testToken = 'github_pat_123456789abcdefghijklmnopqrstuvwxyz'; it('should encrypt and decrypt tokens correctly', () => { const encrypted = encryptToken(testToken, testSecret); const decrypted = decryptToken(encrypted, testSecret); expect(decrypted).toBe(testToken); }); it('should produce different encrypted outputs for same token', () => { const encrypted1 = encryptToken(testToken, testSecret); const encrypted2 = encryptToken(testToken, testSecret); expect(encrypted1).not.toBe(encrypted2); const decrypted1 = decryptToken(encrypted1, testSecret); const decrypted2 = decryptToken(encrypted2, testSecret); expect(decrypted1).toBe(testToken); expect(decrypted2).toBe(testToken); }); it('should fail to decrypt with wrong secret', () => { const encrypted = encryptToken(testToken, testSecret); const wrongSecret = 'wrong-secret-that-is-also-at-least-32-characters-long'; expect(() => { decryptToken(encrypted, wrongSecret); }).toThrow(); }); it('should fail to decrypt tampered tokens', () => { const encrypted = encryptToken(testToken, testSecret); const parts = encrypted.split(':'); parts[3] = parts[3].substring(0, parts[3].length - 2) + 'ff'; const tampered = parts.join(':'); expect(() => { decryptToken(tampered, testSecret); }).toThrow(); }); it('should reject encryption with short secret', () => { expect(() => { encryptToken(testToken, 'short'); }).toThrowError(/at least 32 characters/); }); it('should validate encryption secret requirements', () => { expect(() => validateEncryptionSecret('short')).toThrow(); expect(() => validateEncryptionSecret('')).toThrow(); expect(validateEncryptionSecret(testSecret)).toBe(true); }); }); describe('Error Handling', () => { it('should create standardized OAuth errors', () => { const error = createOAuthError( OAuthErrorCodes.ACCESS_DENIED, 'User denied access', { extra: 'details' } ); expect(error.code).toBe(OAuthErrorCodes.ACCESS_DENIED); expect(error.message).toBe('User denied access'); expect(error.details.extra).toBe('details'); }); it('should handle access_denied error from provider', () => { const providerError = { error: 'access_denied' }; const oauthError = handleProviderError(providerError); expect(oauthError.code).toBe(OAuthErrorCodes.ACCESS_DENIED); expect(oauthError.message).toContain('denied access'); }); it('should handle invalid_grant error from provider', () => { const providerError = { error: 'invalid_grant' }; const oauthError = handleProviderError(providerError); expect(oauthError.code).toBe(OAuthErrorCodes.INVALID_GRANT); expect(oauthError.message).toContain('expired'); }); it('should handle network errors', () => { const networkError = { code: 'ENOTFOUND' }; const oauthError = handleProviderError(networkError); expect(oauthError.code).toBe(OAuthErrorCodes.NETWORK_ERROR); expect(oauthError.message).toContain('connect'); }); it('should handle JWT generation errors', () => { const jwtError = { message: 'JWT generation failed' }; const oauthError = handleProviderError(jwtError); expect(oauthError.code).toBe(OAuthErrorCodes.JWT_GENERATION_FAILED); expect(oauthError.message).toContain('authentication token'); }); it('should provide generic error for unknown errors', () => { const unknownError = { message: 'Something weird happened' }; const oauthError = handleProviderError(unknownError); expect(oauthError.code).toBe(OAuthErrorCodes.SERVER_ERROR); expect(oauthError.message).toContain('unexpected'); }); }); describe('Input Validation', () => { it('should validate supported providers', () => { expect(validateProvider('github')).toBe('github'); expect(validateProvider('google')).toBe('google'); }); it('should reject unsupported providers', () => { expect(() => validateProvider('facebook')).toThrow(); expect(() => validateProvider('')).toThrow(); }); it('should validate response_type parameter', () => { expect(validateResponseType('json')).toBe('json'); expect(validateResponseType(undefined)).toBeUndefined(); }); it('should reject invalid response_type', () => { expect(() => validateResponseType('xml')).toThrow(); }); it('should validate redirect URIs', () => { expect(validateRedirectUri('https://example.com/callback')).toBe(true); expect(validateRedirectUri('http://localhost:3000/callback')).toBe(true); expect(validateRedirectUri(undefined)).toBe(true); }); it('should reject invalid redirect URIs', () => { expect(() => validateRedirectUri('ftp://example.com')).toThrow(); expect(() => validateRedirectUri('not-a-url')).toThrow(); }); it('should detect OAuth error parameters from provider', () => { expect(() => checkOAuthErrorParam('access_denied', 'User denied')).toThrow(); expect(() => checkOAuthErrorParam('server_error', 'Server error')).toThrow(); }); it('should not throw when no error parameter present', () => { expect(() => checkOAuthErrorParam(undefined, undefined)).not.toThrow(); }); }); });