dcrouter/test/suite/smtpclient_security/test.csec-02.oauth2-authentication.ts

448 lines
14 KiB
TypeScript
Raw Normal View History

2025-05-24 16:19:19 +00:00
import { tap, expect } from '@git.zone/tstest/tapbundle';
2025-05-25 19:02:18 +00:00
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
2025-05-24 16:19:19 +00:00
2025-05-25 19:02:18 +00:00
let testServer: ITestServer;
2025-05-24 16:19:19 +00:00
tap.test('setup test SMTP server', async () => {
2025-05-25 19:02:18 +00:00
testServer = await startTestServer({
port: 2562,
tlsEnabled: false,
authRequired: true
2025-05-24 16:19:19 +00:00
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CSEC-02: Check OAuth2 support', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Check EHLO response for OAuth support
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
console.log('Checking OAuth2 support in EHLO response...');
const supportsXOAuth2 = ehloResponse.includes('XOAUTH2');
const supportsOAuthBearer = ehloResponse.includes('OAUTHBEARER');
console.log(`XOAUTH2 supported: ${supportsXOAuth2}`);
console.log(`OAUTHBEARER supported: ${supportsOAuthBearer}`);
if (!supportsXOAuth2 && !supportsOAuthBearer) {
console.log('Server does not advertise OAuth2 support');
}
await smtpClient.close();
});
tap.test('CSEC-02: XOAUTH2 authentication flow', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Create XOAUTH2 string
// Format: base64("user=" + user + "^Aauth=Bearer " + token + "^A^A")
const user = 'user@example.com';
const accessToken = 'mock-oauth2-access-token';
const authString = `user=${user}\x01auth=Bearer ${accessToken}\x01\x01`;
const base64Auth = Buffer.from(authString).toString('base64');
console.log('\nAttempting XOAUTH2 authentication...');
console.log(`User: ${user}`);
console.log(`Token: ${accessToken.substring(0, 10)}...`);
try {
const authResponse = await smtpClient.sendCommand(`AUTH XOAUTH2 ${base64Auth}`);
if (authResponse.startsWith('235')) {
console.log('XOAUTH2 authentication successful');
expect(authResponse).toInclude('235');
} else if (authResponse.startsWith('334')) {
// Server wants more data or error response
console.log('Server response:', authResponse);
// Send empty response to get error details
const errorResponse = await smtpClient.sendCommand('');
console.log('Error details:', errorResponse);
} else {
console.log('Authentication failed:', authResponse);
}
} catch (error) {
console.log('XOAUTH2 not supported or failed:', error.message);
}
await smtpClient.close();
});
tap.test('CSEC-02: OAUTHBEARER authentication flow', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Create OAUTHBEARER string (RFC 7628)
// Format: n,a=user@example.com,^Ahost=server.example.com^Aport=587^Aauth=Bearer token^A^A
const user = 'user@example.com';
const accessToken = 'mock-oauthbearer-access-token';
const authString = `n,a=${user},\x01host=${testServer.hostname}\x01port=${testServer.port}\x01auth=Bearer ${accessToken}\x01\x01`;
const base64Auth = Buffer.from(authString).toString('base64');
console.log('\nAttempting OAUTHBEARER authentication...');
console.log(`User: ${user}`);
console.log(`Host: ${testServer.hostname}`);
console.log(`Port: ${testServer.port}`);
try {
const authResponse = await smtpClient.sendCommand(`AUTH OAUTHBEARER ${base64Auth}`);
if (authResponse.startsWith('235')) {
console.log('OAUTHBEARER authentication successful');
expect(authResponse).toInclude('235');
} else if (authResponse.startsWith('334')) {
// Server wants more data or error response
console.log('Server challenge:', authResponse);
// Decode challenge if present
const challenge = authResponse.substring(4).trim();
if (challenge) {
const decodedChallenge = Buffer.from(challenge, 'base64').toString();
console.log('Decoded challenge:', decodedChallenge);
}
// Send empty response to cancel
await smtpClient.sendCommand('*');
} else {
console.log('Authentication failed:', authResponse);
}
} catch (error) {
console.log('OAUTHBEARER not supported or failed:', error.message);
}
await smtpClient.close();
});
tap.test('CSEC-02: OAuth2 with client configuration', async () => {
// Test client with OAuth2 configuration
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
auth: {
type: 'oauth2',
user: 'oauth.user@example.com',
clientId: 'client-id-12345',
clientSecret: 'client-secret-67890',
accessToken: 'access-token-abcdef',
refreshToken: 'refresh-token-ghijkl',
expires: Date.now() + 3600000 // 1 hour from now
},
connectionTimeout: 5000,
debug: true
});
try {
await smtpClient.connect();
// Check if client handles OAuth2 auth automatically
const authenticated = await smtpClient.isAuthenticated();
console.log('OAuth2 auto-authentication:', authenticated ? 'Success' : 'Failed');
if (authenticated) {
// Try to send a test email
const result = await smtpClient.verify();
console.log('Connection verified:', result);
}
} catch (error) {
console.log('OAuth2 configuration test:', error.message);
// Expected if server doesn't support OAuth2
}
await smtpClient.close();
});
tap.test('CSEC-02: OAuth2 token refresh simulation', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Simulate expired token scenario
const user = 'user@example.com';
const expiredToken = 'expired-access-token';
const authString = `user=${user}\x01auth=Bearer ${expiredToken}\x01\x01`;
const base64Auth = Buffer.from(authString).toString('base64');
console.log('\nSimulating expired token scenario...');
try {
const authResponse = await smtpClient.sendCommand(`AUTH XOAUTH2 ${base64Auth}`);
if (authResponse.startsWith('334')) {
// Server returns error, decode it
const errorBase64 = authResponse.substring(4).trim();
if (errorBase64) {
const errorJson = Buffer.from(errorBase64, 'base64').toString();
console.log('OAuth2 error response:', errorJson);
try {
const error = JSON.parse(errorJson);
if (error.status === '401') {
console.log('Token expired or invalid - would trigger refresh');
// Simulate token refresh
const newToken = 'refreshed-access-token';
const newAuthString = `user=${user}\x01auth=Bearer ${newToken}\x01\x01`;
const newBase64Auth = Buffer.from(newAuthString).toString('base64');
// Cancel current auth
await smtpClient.sendCommand('*');
// Try again with new token
console.log('Retrying with refreshed token...');
const retryResponse = await smtpClient.sendCommand(`AUTH XOAUTH2 ${newBase64Auth}`);
console.log('Retry response:', retryResponse);
}
} catch (e) {
console.log('Error response not JSON:', errorJson);
}
}
}
} catch (error) {
console.log('Token refresh simulation error:', error.message);
}
await smtpClient.close();
});
tap.test('CSEC-02: OAuth2 scope validation', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test different OAuth2 scopes
const testScopes = [
{ scope: 'https://mail.google.com/', desc: 'Gmail full access' },
{ scope: 'https://outlook.office.com/SMTP.Send', desc: 'Outlook send-only' },
{ scope: 'email', desc: 'Generic email scope' }
];
for (const test of testScopes) {
console.log(`\nTesting OAuth2 with scope: ${test.desc}`);
const user = 'user@example.com';
const token = `token-with-scope-${test.scope.replace(/[^a-z]/gi, '')}`;
// Include scope in auth string (non-standard, for testing)
const authString = `user=${user}\x01auth=Bearer ${token}\x01scope=${test.scope}\x01\x01`;
const base64Auth = Buffer.from(authString).toString('base64');
try {
const response = await smtpClient.sendCommand(`AUTH XOAUTH2 ${base64Auth}`);
console.log(`Response for ${test.desc}: ${response.substring(0, 50)}...`);
if (response.startsWith('334') || response.startsWith('535')) {
// Cancel auth attempt
await smtpClient.sendCommand('*');
}
} catch (error) {
console.log(`Error for ${test.desc}: ${error.message}`);
}
}
await smtpClient.close();
});
tap.test('CSEC-02: OAuth2 provider-specific formats', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test provider-specific OAuth2 formats
const providers = [
{
name: 'Google',
format: (user: string, token: string) =>
`user=${user}\x01auth=Bearer ${token}\x01\x01`
},
{
name: 'Microsoft',
format: (user: string, token: string) =>
`user=${user}\x01auth=Bearer ${token}\x01\x01`
},
{
name: 'Yahoo',
format: (user: string, token: string) =>
`user=${user}\x01auth=Bearer ${token}\x01\x01`
}
];
for (const provider of providers) {
console.log(`\nTesting ${provider.name} OAuth2 format...`);
const user = `test@${provider.name.toLowerCase()}.com`;
const token = `${provider.name.toLowerCase()}-oauth-token`;
const authString = provider.format(user, token);
const base64Auth = Buffer.from(authString).toString('base64');
try {
const response = await smtpClient.sendCommand(`AUTH XOAUTH2 ${base64Auth}`);
console.log(`${provider.name} response: ${response.substring(0, 30)}...`);
if (!response.startsWith('235')) {
// Cancel if not successful
await smtpClient.sendCommand('*');
}
} catch (error) {
console.log(`${provider.name} error: ${error.message}`);
}
}
await smtpClient.close();
});
tap.test('CSEC-02: OAuth2 security considerations', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
console.log('\nOAuth2 Security Considerations:');
// Check if connection is encrypted
const connectionInfo = smtpClient.getConnectionInfo();
console.log(`Connection encrypted: ${connectionInfo?.secure || false}`);
if (!connectionInfo?.secure) {
console.log('WARNING: OAuth2 over unencrypted connection is insecure!');
}
// Check STARTTLS availability
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
const supportsStartTLS = ehloResponse.includes('STARTTLS');
if (supportsStartTLS && !connectionInfo?.secure) {
console.log('STARTTLS available - upgrading connection...');
try {
const starttlsResponse = await smtpClient.sendCommand('STARTTLS');
if (starttlsResponse.startsWith('220')) {
console.log('Connection upgraded to TLS');
// In real implementation, TLS handshake would happen here
}
} catch (error) {
console.log('STARTTLS failed:', error.message);
}
}
// Test token exposure in logs
const sensitiveToken = 'super-secret-oauth-token-12345';
const safeLogToken = sensitiveToken.substring(0, 10) + '...';
console.log(`Token handling - shown as: ${safeLogToken}`);
await smtpClient.close();
});
tap.test('CSEC-02: OAuth2 error handling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test various OAuth2 error scenarios
const errorScenarios = [
{
name: 'Invalid token format',
authString: 'invalid-base64-!@#$'
},
{
name: 'Empty token',
authString: Buffer.from('user=test@example.com\x01auth=Bearer \x01\x01').toString('base64')
},
{
name: 'Missing user',
authString: Buffer.from('auth=Bearer token123\x01\x01').toString('base64')
},
{
name: 'Malformed structure',
authString: Buffer.from('user=test@example.com auth=Bearer token').toString('base64')
}
];
for (const scenario of errorScenarios) {
console.log(`\nTesting: ${scenario.name}`);
try {
const response = await smtpClient.sendCommand(`AUTH XOAUTH2 ${scenario.authString}`);
console.log(`Response: ${response}`);
if (response.startsWith('334') || response.startsWith('501') || response.startsWith('535')) {
// Expected error responses
await smtpClient.sendCommand('*'); // Cancel
}
} catch (error) {
console.log(`Error (expected): ${error.message}`);
}
}
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
2025-05-25 19:02:18 +00:00
await stopTestServer(testServer);
2025-05-24 16:19:19 +00:00
}
});
2025-05-25 19:05:43 +00:00
export default tap.start();