import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from './plugins.js'; import { createTestServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../helpers/smtp.client.js'; tap.test('CSEC-08: should handle authentication fallback securely', async (tools) => { const testId = 'CSEC-08-authentication-fallback'; console.log(`\n${testId}: Testing authentication fallback mechanisms...`); let scenarioCount = 0; // Scenario 1: Multiple authentication methods await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing multiple authentication methods`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 auth.example.com ESMTP\r\n'); let authMethod = ''; let authStep = 0; socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-auth.example.com\r\n'); socket.write('250-AUTH CRAM-MD5 PLAIN LOGIN\r\n'); socket.write('250 OK\r\n'); } else if (command.startsWith('AUTH CRAM-MD5')) { authMethod = 'CRAM-MD5'; authStep = 1; // Send challenge const challenge = Buffer.from(`<${Date.now()}.${Math.random()}@auth.example.com>`).toString('base64'); socket.write(`334 ${challenge}\r\n`); } else if (command.startsWith('AUTH PLAIN')) { authMethod = 'PLAIN'; if (command.length > 11) { // Credentials included const credentials = Buffer.from(command.substring(11), 'base64').toString(); console.log(` [Server] PLAIN auth attempt with immediate credentials`); socket.write('235 2.7.0 Authentication successful\r\n'); } else { // Request credentials socket.write('334\r\n'); } } else if (command.startsWith('AUTH LOGIN')) { authMethod = 'LOGIN'; authStep = 1; socket.write('334 VXNlcm5hbWU6\r\n'); // Username: } else if (authMethod === 'CRAM-MD5' && authStep === 1) { // Verify CRAM-MD5 response console.log(` [Server] CRAM-MD5 response received`); socket.write('235 2.7.0 Authentication successful\r\n'); authMethod = ''; authStep = 0; } else if (authMethod === 'PLAIN' && !command.startsWith('AUTH')) { // PLAIN credentials const credentials = Buffer.from(command, 'base64').toString(); console.log(` [Server] PLAIN credentials received`); socket.write('235 2.7.0 Authentication successful\r\n'); authMethod = ''; } else if (authMethod === 'LOGIN' && authStep === 1) { // Username console.log(` [Server] LOGIN username received`); authStep = 2; socket.write('334 UGFzc3dvcmQ6\r\n'); // Password: } else if (authMethod === 'LOGIN' && authStep === 2) { // Password console.log(` [Server] LOGIN password received`); socket.write('235 2.7.0 Authentication successful\r\n'); authMethod = ''; authStep = 0; } else if (command.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (command === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 OK\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, auth: { user: 'testuser', pass: 'testpass' } }); const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Multi-auth test', text: 'Testing multiple authentication methods' }); const result = await smtpClient.sendMail(email); console.log(' Authentication successful'); expect(result).toBeDefined(); expect(result.messageId).toBeDefined(); await testServer.server.close(); })(); // Scenario 2: Authentication method downgrade prevention await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing auth method downgrade prevention`); let attemptCount = 0; const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 secure.example.com ESMTP\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { attemptCount++; if (attemptCount === 1) { // First attempt: offer secure methods socket.write('250-secure.example.com\r\n'); socket.write('250-AUTH CRAM-MD5 SCRAM-SHA-256\r\n'); socket.write('250 OK\r\n'); } else { // Attacker attempt: offer weaker methods socket.write('250-secure.example.com\r\n'); socket.write('250-AUTH PLAIN LOGIN\r\n'); socket.write('250 OK\r\n'); } } else if (command.startsWith('AUTH CRAM-MD5')) { // Simulate failure to force fallback attempt socket.write('535 5.7.8 Authentication failed\r\n'); } else if (command.startsWith('AUTH PLAIN') || command.startsWith('AUTH LOGIN')) { console.log(' [Server] Warning: Client using weak auth method'); socket.write('535 5.7.8 Weak authentication method not allowed\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, auth: { user: 'testuser', pass: 'testpass' }, authMethod: 'CRAM-MD5' // Prefer secure method }); const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Downgrade prevention test', text: 'Testing authentication downgrade prevention' }); try { await smtpClient.sendMail(email); console.log(' Unexpected: Authentication succeeded'); } catch (error) { console.log(` Expected: Auth failed - ${error.message}`); expect(error.message).toContain('Authentication failed'); } await testServer.server.close(); })(); // Scenario 3: OAuth2 fallback await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing OAuth2 authentication fallback`); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 oauth.example.com ESMTP\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command.substring(0, 50)}...`); if (command.startsWith('EHLO')) { socket.write('250-oauth.example.com\r\n'); socket.write('250-AUTH XOAUTH2 PLAIN LOGIN\r\n'); socket.write('250 OK\r\n'); } else if (command.startsWith('AUTH XOAUTH2')) { // Check OAuth2 token const token = command.substring(13); if (token.includes('expired')) { console.log(' [Server] OAuth2 token expired'); socket.write('334 eyJzdGF0dXMiOiI0MDEiLCJzY2hlbWVzIjoiYmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==\r\n'); } else { console.log(' [Server] OAuth2 authentication successful'); socket.write('235 2.7.0 Authentication successful\r\n'); } } else if (command.startsWith('AUTH PLAIN')) { // Fallback to PLAIN auth console.log(' [Server] Fallback to PLAIN auth'); if (command.length > 11) { socket.write('235 2.7.0 Authentication successful\r\n'); } else { socket.write('334\r\n'); } } else if (command === '') { // Empty line after failed XOAUTH2 socket.write('535 5.7.8 Authentication failed\r\n'); } else if (!command.startsWith('AUTH') && command.length > 20) { // PLAIN credentials socket.write('235 2.7.0 Authentication successful\r\n'); } else if (command.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (command === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 OK\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); // Test with OAuth2 token const oauthClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, auth: { type: 'oauth2', user: 'user@example.com', accessToken: 'valid-oauth-token' } }); const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'OAuth2 test', text: 'Testing OAuth2 authentication' }); try { const result = await oauthClient.sendMail(email); console.log(' OAuth2 authentication successful'); expect(result).toBeDefined(); } catch (error) { console.log(` OAuth2 failed, testing fallback...`); // Test fallback to password auth const fallbackClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, auth: { user: 'testuser', pass: 'testpass' } }); const result = await fallbackClient.sendMail(email); console.log(' Fallback authentication successful'); expect(result).toBeDefined(); } await testServer.server.close(); })(); // Scenario 4: Authentication retry with different credentials await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing auth retry with different credentials`); let authAttempts = 0; const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 retry.example.com ESMTP\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); if (command.startsWith('EHLO')) { socket.write('250-retry.example.com\r\n'); socket.write('250-AUTH PLAIN LOGIN\r\n'); socket.write('250 OK\r\n'); } else if (command.startsWith('AUTH')) { authAttempts++; console.log(` [Server] Auth attempt ${authAttempts}`); if (authAttempts <= 2) { // Fail first attempts socket.write('535 5.7.8 Authentication failed\r\n'); } else { // Success on third attempt socket.write('235 2.7.0 Authentication successful\r\n'); } } else if (command.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (command === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 OK\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); // Test multiple auth attempts const credentials = [ { user: 'wronguser', pass: 'wrongpass' }, { user: 'testuser', pass: 'wrongpass' }, { user: 'testuser', pass: 'testpass' } ]; let successfulAuth = false; for (const cred of credentials) { try { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, auth: cred }); const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Retry test', text: 'Testing authentication retry' }); const result = await smtpClient.sendMail(email); console.log(` Auth succeeded with user: ${cred.user}`); successfulAuth = true; expect(result).toBeDefined(); break; } catch (error) { console.log(` Auth failed with user: ${cred.user}`); } } expect(successfulAuth).toBe(true); expect(authAttempts).toBe(3); await testServer.server.close(); })(); // Scenario 5: Secure authentication over insecure connection await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing secure auth over insecure connection`); const testServer = await createTestServer({ secure: false, // Plain text connection onConnection: async (socket) => { console.log(' [Server] Client connected (insecure)'); socket.write('220 insecure.example.com ESMTP\r\n'); let tlsStarted = false; socket.on('data', (data) => { const command = data.toString().trim(); console.log(` [Server] Received: ${command}`); if (command.startsWith('EHLO')) { socket.write('250-insecure.example.com\r\n'); socket.write('250-STARTTLS\r\n'); if (tlsStarted) { socket.write('250-AUTH PLAIN LOGIN\r\n'); } socket.write('250 OK\r\n'); } else if (command === 'STARTTLS') { socket.write('220 2.0.0 Ready to start TLS\r\n'); tlsStarted = true; // In real scenario, would upgrade to TLS here } else if (command.startsWith('AUTH') && !tlsStarted) { console.log(' [Server] Rejecting auth over insecure connection'); socket.write('530 5.7.0 Must issue a STARTTLS command first\r\n'); } else if (command.startsWith('AUTH') && tlsStarted) { console.log(' [Server] Accepting auth over TLS'); socket.write('235 2.7.0 Authentication successful\r\n'); } else if (command.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (command === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 OK\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); // Try auth without TLS (should fail) const insecureClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, ignoreTLS: true, // Don't use STARTTLS auth: { user: 'testuser', pass: 'testpass' } }); const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Secure auth test', text: 'Testing secure authentication requirements' }); try { await insecureClient.sendMail(email); console.log(' Unexpected: Auth succeeded without TLS'); } catch (error) { console.log(' Expected: Auth rejected without TLS'); expect(error.message).toContain('STARTTLS'); } // Try with STARTTLS const secureClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, requireTLS: true, auth: { user: 'testuser', pass: 'testpass' } }); try { const result = await secureClient.sendMail(email); console.log(' Auth succeeded with STARTTLS'); // Note: In real test, STARTTLS would actually upgrade the connection } catch (error) { console.log(' STARTTLS not fully implemented in test'); } await testServer.server.close(); })(); // Scenario 6: Authentication mechanism negotiation await (async () => { scenarioCount++; console.log(`\nScenario ${scenarioCount}: Testing auth mechanism negotiation`); const supportedMechanisms = new Map([ ['SCRAM-SHA-256', { priority: 1, supported: true }], ['CRAM-MD5', { priority: 2, supported: true }], ['PLAIN', { priority: 3, supported: true }], ['LOGIN', { priority: 4, supported: true }] ]); const testServer = await createTestServer({ onConnection: async (socket) => { console.log(' [Server] Client connected'); socket.write('220 negotiate.example.com ESMTP\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); if (command.startsWith('EHLO')) { socket.write('250-negotiate.example.com\r\n'); const authMechs = Array.from(supportedMechanisms.entries()) .filter(([_, info]) => info.supported) .map(([mech, _]) => mech) .join(' '); socket.write(`250-AUTH ${authMechs}\r\n`); socket.write('250 OK\r\n'); } else if (command.startsWith('AUTH ')) { const mechanism = command.split(' ')[1]; console.log(` [Server] Client selected: ${mechanism}`); const mechInfo = supportedMechanisms.get(mechanism); if (mechInfo && mechInfo.supported) { console.log(` [Server] Priority: ${mechInfo.priority}`); socket.write('235 2.7.0 Authentication successful\r\n'); } else { socket.write('504 5.5.4 Unrecognized authentication type\r\n'); } } else if (command.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (command.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (command === 'DATA') { socket.write('354 Start mail input\r\n'); } else if (command === '.') { socket.write('250 OK\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); } }); const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, auth: { user: 'testuser', pass: 'testpass' } // Client will negotiate best available mechanism }); const email = new plugins.smartmail.Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Negotiation test', text: 'Testing authentication mechanism negotiation' }); const result = await smtpClient.sendMail(email); console.log(' Authentication negotiation successful'); expect(result).toBeDefined(); expect(result.messageId).toBeDefined(); await testServer.server.close(); })(); console.log(`\n${testId}: All ${scenarioCount} authentication fallback scenarios tested ✓`); });