← Back to Blog

Automate 2FA Testing Securely

September 17, 2024

2FAMFAsecurity testingtest automation

Testing 2FA/MFA: Practical Approaches Without Compromising Security

Testing two-factor authentication (2FA) and multi-factor authentication (MFA) is tricky: you need automated tests, but you can't compromise security. Here are proven strategies for different test levels.

The Challenge

How do you automate login flows that require:

  • Time-based one-time passwords (TOTP)
  • SMS codes
  • Email verification links
  • Push notification approvals

Without either:

  • Manually entering codes during test runs
  • Creating security backdoors in production

Five Testing Strategies

1. Test Environment Backdoors (Most Common)

Create bypass mechanisms that only work in non-production environments:

# Environment-specific bypass
if os.getenv('ENV') == 'test' and request.headers.get('X-Test-Bypass') == 'true':
    return authenticate_without_2fa(user)

# Master test code (non-prod only)
if code == '000000' and not is_production():
    return validate_success()

Use when: You need quick E2E test execution without external dependencies.

2. Programmatic TOTP Generation (Recommended)

Generate valid time-based codes during test execution:

import pyotp

# Store test account TOTP secret in test config
totp = pyotp.TOTP('JBSWY3DPEHPK3PXP')
current_code = totp.now()

# Use in login flow
login(username='test@example.com', password='pass', mfa_code=current_code)

Use when: Testing against real 2FA implementation without manual intervention.

3. SMS/Email Code Interception

For SMS or email-based verification:

Option A: Test services

  • Twilio test credentials: Don't actually send SMS, return predictable codes
  • Mailtrap/Mailinator: Automated email verification code extraction

Option B: Test endpoint

# Test-only API endpoint (disabled in production)
@app.route('/test/last-verification-code', methods=['GET'])
def get_last_code():
    if not is_test_environment():
        abort(404)
    return jsonify({'code': get_last_sent_code_for_user(request.args['email'])})

Use when: Testing SMS/email delivery flow is critical to your test scenarios.

4. Mock the 2FA Service

In lower environments, stub external providers:

# Mock Twilio in integration tests
@patch('twilio.rest.Client.messages.create')
def test_2fa_flow(mock_sms):
    mock_sms.return_value.sid = 'test-message-id'

    result = send_2fa_code(phone='+15551234567')

    assert result.success
    mock_sms.assert_called_once()

Use when: You want to test error handling without external service costs.

5. Session Token Injection

Skip authentication entirely for tests that don't focus on login:

# Generate valid session directly
def create_authenticated_session(user_id):
    token = jwt.encode({'user_id': user_id, 'exp': ...}, SECRET_KEY)
    return token

# In E2E tests
token = create_authenticated_session(test_user_id)
browser.set_cookie('session_token', token)
browser.goto('/dashboard')  # Already authenticated

Use when: Authentication isn't what you're testing—you just need an authenticated state.

Strategy by Test Level

Test Level Recommended Approach
Unit Tests Mock 2FA verification entirely
Integration Tests Programmatic TOTP generation
E2E Tests Test backdoors or TOTP generation
Security/Pentest Real 2FA (no shortcuts)

Example: Complete E2E Test with TOTP

import pyotp
from playwright.sync_api import sync_playwright

def test_login_with_2fa():
    # Test account configuration
    test_email = 'test@example.com'
    test_password = 'SecurePassword123!'
    totp_secret = 'JBSWY3DPEHPK3PXP'  # Stored in test config

    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()

        # Step 1: Login with username/password
        page.goto('https://app.example.com/login')
        page.fill('#email', test_email)
        page.fill('#password', test_password)
        page.click('#login-button')

        # Step 2: Generate and enter current TOTP code
        totp = pyotp.TOTP(totp_secret)
        current_code = totp.now()

        page.fill('#mfa-code', current_code)
        page.click('#verify-button')

        # Step 3: Assert successful login
        assert page.url == 'https://app.example.com/dashboard'

        browser.close()

Security Best Practices

Never compromise production:

  • ✅ Environment checks for all bypass mechanisms
  • ✅ Separate test accounts with known TOTP secrets
  • ✅ Feature flags that cannot be enabled in production
  • ✅ Code reviews specifically for auth-related changes

Configuration example:

# config/test.yml
security:
  two_factor:
    allow_bypass: true
    test_master_code: "000000"

# config/production.yml
security:
  two_factor:
    allow_bypass: false  # Hardcoded, not overridable

Common Mistakes to Avoid

Hardcoded bypass in production code without environment checks

# DON'T DO THIS
if code == '000000':
    return True  # Backdoor always active!

Environment-gated bypass

if code == '000000' and os.getenv('ENV') in ['test', 'dev']:
    return True

Using production 2FA credentials in tests

  • Burns through SMS credits
  • Rate limiting issues
  • Slower test execution

Test-specific accounts with known TOTP secrets or mock services

Quick Decision Guide

Use programmatic TOTP generation if:

  • You're using TOTP/authenticator apps
  • You need realistic test coverage
  • External services aren't involved

Use test backdoors if:

  • Speed is critical (E2E tests)
  • 2FA isn't the focus of your test
  • You're testing across multiple environments

Use mocks if:

  • Testing unit/integration level
  • External service costs are a concern
  • You need to test error scenarios

Conclusion

Testing 2FA/MFA doesn't have to be painful. The key is choosing the right approach for your test level:

  • Unit/Integration: Mock or generate codes programmatically
  • E2E: TOTP generation or environment-specific bypasses
  • Security: Real 2FA with no shortcuts

The golden rule: Never create a backdoor that could work in production.


How do you handle 2FA/MFA in your test automation? Share your approach on LinkedIn.