How to Automate Two-Factor Authentication Testing (Step-by-Step)

How to Automate Two-Factor Authentication Testing (Step-by-Step)

June 25, 2026 · 15 min read · How-To Guides

How to Automate Two-Factor Authentication Testing (Step-by-Step)

When teams ask how to automate two-factor authentication testing, the immediate answer is that automation pays off when the login flow is a gatekeeper for critical functionality, when manual OTP entry creates repetitive overhead, and when regression risk is high due to frequent changes in authentication UI or backend token generation. Automating 2FA removes the human bottleneck of waiting for a code, enables parallel execution of security‑sensitive scenarios, and provides deterministic validation of error handling (expired codes, wrong codes, rate‑limiting). The following guide walks through a complete, production‑ready approach: deciding when to automate, picking a framework, engineering stable locators, synchronizing with OTP delivery, managing test data, integrating with CI, and reporting results. Each step includes concrete code snippets, a test‑matrix table, and a tool‑comparison table. The final section shows how an autonomous exploration platform can bootstrap the effort without writing a single script.

How to Automate Two-Factor Authentication Testing (Step-by-Step): When Automation Pays Off

Defining the ROI Threshold

Automation is justified when the cost of manual execution exceeds the engineering effort to build and maintain the test suite. For a typical SaaS product, a login‑protected checkout flow executed three times per release cycle by a QA engineer takes ~15 minutes per run (including OTP wait, manual entry, and verification). Over a monthly release cadence that amounts to 180 minutes of pure manual effort. If building a reliable 2FA automation suite takes ~8 hours initially and ~2 hours per sprint for maintenance, the break‑even point is reached after the fourth release. Beyond that, each cycle saves roughly 12 minutes of engineer time, which scales linearly with additional test cases (password reset, account recovery, MFA enrollment).

Risk Factors That Favor Automation

When to Keep Manual Checks

Exploratory testing of new authentication methods (e.g., biometric push, hardware tokens) still benefits from human observation because the interaction model may not be fully captured by scripts. Likewise, usability aspects such as error‑message clarity or screen‑reader compatibility are better evaluated manually. Use automation for the deterministic “happy‑path” and negative‑path checks; reserve manual sessions for edge‑case discovery and UX validation.

How to Automate Two-Factor Authentication Testing (Step-by-Step): Choosing a Test Framework

Language and Ecosystem Considerations

Select a framework that matches your team’s existing test stack to reduce context switching. If your regression suite is primarily Java‑based with TestNG, Selenium WebDriver remains a natural fit. For JavaScript/TypeScript shops, Playwright or Cypress offer built‑in auto‑waiting and easier debugging. Python teams often gravitate toward Selenium with pytest, especially when leveraging libraries like pyotp for TOTP generation.

Support for OTP Retrieval Mechanisms

A framework must allow either:

  1. Direct OTP injection (bypassing the delivery channel) for speed and reliability, or
  2. Real‑world channel simulation (SMS, email, authenticator app) when you need to validate end‑to‑end delivery.

Web‑focused tools like Playwright provide page.route to intercept network calls and mock OTP APIs. Mobile tools like Appium enable reading SMS from Android emulators via adb shell content query --uri content://sms/inbox. Choose a framework that offers at least one of these mechanisms out of the box or via a lightweight plugin.

Parallel Execution and Isolation

Tests that involve OTP timing are sensitive to shared state (e.g., a single test mailbox). Frameworks that support isolated browser contexts or device snapshots per worker (Playwright’s browserContext, Selenium’s ThreadLocal WebDriver, Appium’s session‑per‑test) prevent cross‑talk. Verify that your CI runner can launch enough parallel workers without exhausting OTP rate limits.

Community and Maintenance

Check the frequency of releases, availability of community‑maintained helpers for 2FA (e.g., playwright-extra with recaptcha plugin, selenium‑wire for request interception), and quality of documentation. A framework with an active Slack/Discord channel reduces debugging time when OTP‑related flakiness appears.

Quick Comparison Table

FrameworkLanguageWeb SupportMobile SupportBuilt‑in OTP MockParallel ContextsTypical Setup Time
Selenium WebDriverJava, C#, Python, JSvia Appium❌ (requires external mock)✅ (ThreadLocal/Grid)Medium
PlaywrightJS/TS, Python, .NET, Java❌ (experimental)✅ (route interception)✅ (browserContext)Low
CypressJS/TS✅ (cy.request stub)❌ (single tab, but can run multiple instances)Low
AppiumJava, JS, Python, C#✅ (Android/iOS)❌ (requires SMS/Android API)✅ (session per test)Medium‑High
SUSA Autonomous AgentNo code (Python CLI)✅ (web)✅ (Android)✅ (auto‑generated OTP hooks)✅ (cross‑session learning)Very Low

The table shows that for pure web 2FA, Playwright offers the lowest friction because you can intercept the OTP API call and feed a static code instantly. If you need to test native authenticator apps or SMS gateways, Appium remains the go‑to, albeit with a heavier setup.

How to Automate Two-Factor Authentication Testing (Step-by-Step): Setting Up the Test Environment

Isolating Authentication Secrets

Never store real OTP seeds or backup codes in version control. Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, or even GitHub Actions encrypted secrets) to inject the base32 secret for TOTP generation at runtime. For SMS‑based 2FA, provision a dedicated test phone number or use a virtual SIM service (Twilio Test Credentials, Vonage Sandbox) that allows you to programmatically retrieve inbound messages.

Mocking OTP Delivery Channels

#### Email OTP

Set up a disposable mailbox via an API such as MailSlurp or Mailosaur. After triggering the OTP send, poll the mailbox API for the latest message and extract the code with a regular expression. Example in Python:


import re, time
from mailslurp_client import MailSlurpClient

def get_email_otp(api_key, inbox_id, timeout=30):
    client = MailSlurpClient(api_key)
    start = time.time()
    while time.time() - start < timeout:
        emails = client.get_emails(inbox_id, wait_for=1, count=1)
        if emails:
            body = emails[0].body
            match = re.search(r'\b(\d{6})\b', body)
            if match:
                return match.group(1)
        time.sleep(2)
    raise TimeoutError("OTP not received")

#### SMS OTP

With Twilio’s test credentials, you can list inbound messages for a test number:


from twilio.rest import Client
import os, time

def get_sms_otp(account_sid, auth_token, phone_number, timeout=30):
    client = Client(account_sid, auth_token)
    start = time.time()
    while time.time() - start < timeout:
        messages = client.messages.list(to=phone_number, limit=1)
        if messages:
            body = messages[0].body
            # assume OTP is a 6‑digit number
            match = re.search(r'\b(\d{6})\b', body)
            if match:
                return match.group(1)
        time.sleep(2)
    raise TimeoutError("SMS OTP not received")

#### TOTP (Authenticator App)

Generate the code locally using the shared secret:


import pyotp, time

def get_totp(secret):
    totp = pyotp.TOTP(secret)
    return totp.now()

Provisioning Test Accounts

Create a script that registers a new user via the API, enrolls them in 2FA (SMS, email, or TOTP), and returns the credentials plus the secret. Store the result in a temporary fixture that is torn down after each test suite run. Example using requests and a hypothetical /signup endpoint:


def create_test_user(base_url, api_key):
    resp = requests.post(
        f"{base_url}/signup",
        json={"email": f"test_{uuid4()}@example.com", "password": "StrongPass!123"},
        headers={"Authorization": f"Bearer {api_key}"},
    )
    resp.raise_for_status()
    data = resp.json()
    # enroll 2FA via API (if available)
    enroll = requests.post(
        f"{base_url}/users/{data['id']}/2fa/enroll",
        json={"method": "totp"},
        headers={"Authorization": f"Bearer {api_key}"},
    )
    enroll.raise_for_status()
    secret = enroll.json()["secret"]
    return {
        "email": data["email"],
        "password": data["password"],
        "totp_secret": secret,
        "user_id": data["id"],
    }

Teardown Strategy

After each test iteration, delete the test user, revoke any issued tokens, and clear the OTP mailbox/SMS inbox. This prevents credential leakage and ensures that subsequent runs start from a clean slate. Implement teardown in a fixture’s finally block or using pytest’s yield pattern.

How to Automate Two-Factor Authentication Testing (Step-by-Step): Designing Stable Locators for 2FA Screens

Avoiding Brittle Identifiers

Do not rely on auto‑generated IDs, positional indexes, or text that may change with copy updates. Instead, work with developers to add stable data-test attributes (or aria-label where appropriate) to every interactive element on the 2FA screen:


<input data-test="otp-input" type="text" inputmode="numeric" aria-label="One‑time code">
<button data-test="verify-button">Verify</button>
<span data-test="error-message" class="hidden"></span>

If modifying the source is not feasible, use a combination of role‑based selectors and nearby static text. For example, in Playwright:


await page.get_by_role("textbox", name=re.compile("code", re.I)).fill(otp)
await page.get_by_role("button", name=re.compile("verify", re.I)).click()

Handling Dynamic Iframes

Some providers embed the OTP entry field inside an iframe (common with third‑party authenticator widgets). Switch to the frame before interacting:


frame = page.frame_locator("iframe[title='Secure OTP Widget']")
await frame.locator('input[data-test="otp-input"]').fill(otp)
await frame.locator('button[data-test="verify-button"]').click()

Dealing with OTP Expiration UI

Many apps display a countdown timer (“Code expires in 00:45”). Rather than asserting on the exact remaining time, verify that the timer element exists and that its text matches the pattern \d{2}:\d{2}. This makes the test resilient to minor timing variations.

Example Locator Library (Python + Selenium)


from selenium.webdriver.common.by import By

class TwoFactorPage:
    OTP_INPUT = (By.CSS_SELECTOR, 'input[data-test="otp-input"]')
    VERIFY_BTN = (By.CSS_SELECTOR, 'button[data-test="verify-button"]')
    ERROR_MSG = (By.CSS_SELECTOR, 'span[data-test="error-message"]')
    COUNTDOWN   = (By.CSS_SELECTOR, 'div[data-test="countdown-timer"]')

    def __init__(self, driver):
        self.driver = driver

    def enter_otp(self, code):
        self.driver.find_element(*self.OTP_INPUT).send_keys(code)

    def click_verify(self):
        self.driver.find_element(*self.VERIFY_BTN).click()

    def get_error(self):
        return self.driver.find_element(*self.ERROR_MSG).text

    def get_countdown(self):
        return self.driver.find_element(*self.COUNTDOWN).text

Encapsulating locators in a page object reduces duplication and simplifies updates when the UI changes.

How to Automate Two-Factor Authentication Testing (Step-by-Step): Handling Waits and Synchronization

Explicit Waits Over Sleep

Hard‑coded time.sleep calls are the primary source of flakiness. Use framework‑provided explicit waits that poll for a condition to become true. In Selenium/WebDriver:


from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 15)
wait.until(EC.visibility_of_element_located(TwoFactorPage.OTP_INPUT))

In Playwright, the auto‑waiting mechanism covers most actions, but you still need to wait for network‑based OTP arrival:


await page.wait_for_function(
    """() => {
        const msg = document.querySelector('div[data-test="otp-received"]');
        return msg && msg.innerText.length === 6;
    }""",
    timeout=15000
)

Polling for OTP Arrival

When you simulate the delivery channel (e.g., checking a mailbox API), wrap the retrieval in a retry loop with exponential backoff. This prevents the test from failing because the OTP arrived a few hundred milliseconds later than expected.


def wait_for_otp(fetch_func, timeout=20, interval=1.5):
    end = time.time() + timeout
    while time.time() < end:
        code = fetch_func()
        if code and re.fullmatch(r'\d{6}', code):
            return code
        time.sleep(interval)
    raise TimeoutError("OTP not retrieved within timeout")

Handling Rate‑Limiting and Retries

Some OTP services throttle after a handful of requests per minute. Design your test to:

Verifying Timeout Behavior

To assert that an expired OTP is rejected, you can either:

  1. Wait for the OTP to naturally expire (not recommended for speed), or
  2. Manipulate the system clock via a test‑only API endpoint that sets the OTP validity window to a few seconds, then attempt verification after the window passes.

# pseudo‑API call to shorten TOTP window
requests.post(f"{base_url}/test/set-totp-window", json={"seconds": 5})
# after 6 seconds, submit code → expect failure

How to Automate Two-Factor Authentication Testing (Step-by-Step): Data Setup, Teardown, and Isolation

Fixture Lifecycle

Structure your test suite so that each test receives a fresh user account, a clean OTP channel, and an isolated browser context. In pytest with Playwright:


import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="function")
def browser_context():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context()
        yield context
        context.close()
        browser.close()

@pytest.fixture
def test_user(browser_context):
    user = create_test_user(API_BASE, os.getenv("API_KEY"))
    yield user
    # teardown
    requests.delete(f"{API_BASE}/users/{user['id']}",
                    headers={"Authorization": f"Bearer {os.getenv('API_KEY')}"})

Parallel Isolation

When running with pytest -n auto, each worker gets its own fixture instance, guaranteeing that OTP mailboxes and SMS inboxes are not shared. If you rely on a shared external service (e.g., a real Twilio number), incorporate a worker‑ID suffix into the test phone number or email address to keep streams separate.

Cleaning OTP Histories

After retrieving an OTP, immediately delete the message from the mailbox or mark it as read to prevent the next poll from picking up a stale code. Most mailbox APIs provide a delete_message endpoint.


client.delete_email(email_id)

Token and Session Cleanup

Even after a successful login, the test may leave behind an active session cookie or JWT. Call the logout endpoint or clear the browser context’s storage to avoid leaking credentials into subsequent tests.


await context.clear_cookies()
await context.storage_state(path=None)  # clears localStorage, sessionStorage

How to Automate Two-Factor Authentication Testing (Step-by-Step): Running Tests in CI/CD Pipelines

Integrating with Popular CI Systems

Managing Secrets Securely

Never hard‑code API keys, Twilio SIDs, or mailbox tokens in the repository. Use the CI provider’s secret store and export them as environment variables at job start. In the test code, read via os.getenv. For added safety, rotate these secrets quarterly and audit access logs.

Controlling Parallelism and Resource Limits

Determine the maximum number of concurrent OTP requests your provider allows. If the limit is 20 SMS/min, configure your test runner to launch no more than 4 workers (assuming each worker performs up to 5 OTP‑heavy tests). Most CI systems let you set jobs: (GitHub Actions) or parallel: (GitLab CI) to enforce this.

Artifact Collection for Debugging

Capture screenshots, page source, and console logs on failure. In Playwright:


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    if rep.when == "call" and rep.failed:
        page = item.funcargs.get("page")
        if page:
            page.screenshot(path=f"failure-{item.name}.png")
            with open(f"failure-{item.name}.html", "w") as f:
                f.write(page.content())

These artifacts are uploaded as CI build artifacts, enabling rapid triage without rerunning the entire suite.

Reporting and Trend Analysis

Publish JUnit‑style XML reports (pytest --junitxml=results.xml) and let your CI ingest them. Over time, track:

Grafana or Datadog dashboards can visualize these metrics, alerting when flake exceeds a threshold (e.g., 5%).

How to Automate Two-Factor Authentication Testing (Step-by-Step): Reporting and Flake Analysis

Structuring Test Results

Each test case should emit a clear PASS/FAIL verdict plus optional metadata:

A JSON line per test makes downstream processing trivial:


{
  "test_id": "TFA-001-SMS-Valid",
  "channel": "sms",
  "outcome": "PASS",
  "duration_ms": 1240,
  "otp_latency_ms": 820,
  "git_sha": "a3f9c2e",
  "timestamp": "2025-09-25T14:32:10Z"
}

Aggregate these lines into a daily summary using a simple Python script or a log‑processing tool like jq.

Detecting Flaky Tests

Run each test twice in the same pipeline (or use a rerun plugin) and compare outcomes. If a test passes on the first attempt but fails on the second (or vice‑versa), flag it as flaky. Store the flake count per test ID and raise an issue when the count exceeds a threshold over a sliding window (e.g., 3 failures in the last 10 runs).


# pseudo‑code for flake detection
if outcome_history[-5:].count("PASS") != 5 and outcome_history[-5:].count("FAIL") != 5:
    flake_score = outcome_history[-5 - outcome_history[-5:].count("PASS", "FAIL")].count("FAIL") / 5
    if flake_score > 0.4:
        raise FlakeDetected(test_id)

Visualizing Flake Trends

Plot a cumulative flake percentage over time. A rising trend indicates either test instability (locator issues, wait timeouts) or a flaky third‑party OTP provider. Correlate spikes with deployments or provider status pages to isolate root cause.

Generating Actionable Reports

Create a markdown report that lists:

Attach this report as a comment on the pull request or publish it to an internal wiki.

How to Automate Two-Factor Authentication Testing (Step-by-Step): Leveraging Autonomous Exploration to Bootstrap 2FA Tests

What Autonomous Exploration Offers

Platforms like SUSA can crawl an application without pre‑written scripts, discovering every reachable screen, input field, and flow. When the crawler encounters a 2FA challenge, it records:

This metadata becomes a baseline test spec that a QA engineer can convert into a coded test in minutes rather than hours.

From Exploration to Executable Script

SUSA’s CLI can export the discovered flow as a Playwright test skeleton:


susatest export --format playwright --output tests/tfa_login.spec.js

The generated file contains:

Reducing Initial Effort

A team that previously spent two days writing login‑2FA tests for three channels (SMS, email, TOTP) can now:

  1. Run SUSA against a staging build (≈15 minutes).
  2. Review the exported specs, adjust the OTP helper, and commit.
  3. Achieve full coverage with <30 minutes of engineer time.

Maintaining the Baseline

As the app evolves, re‑run the explorer on each release candidate. The tool produces a diff report highlighting:

Integrate this diff step into your CI pipeline as a gate: if the explorer reports a missing OTP input, the build fails, prompting immediate attention.

When to Still Write Manual Scripts

Autonomous exploration excels at covering the “happy‑path” and common error paths. It may not capture:

In those cases, supplement the auto‑generated tests with hand‑crafted scripts that exercise the edge cases.

How to Automate Two-Factor Authentication Testing (Step-by‑Step): Checklist for Reliable 2FA Automation

✅ ItemWhy It MattersHow to Verify
Stable locators (data‑test/aria‑label)Prevents breakage on UI tweaksRun a locator audit: grep -r "data-test" coverage > 90%
Explicit waits for OTP arrivalEliminates sleep‑based flakeEnsure no time.sleep or await page.waitForTimeout in test code
Isolated test data per workerAvoids cross‑test OTP collisionsRun tests with -n 4 and confirm no duplicate OTP errors
Secure secret injectionKeeps real seeds out of repoVerify CI logs never contain raw base32 secrets
OTP channel mock or APIGuarantees deterministic timingConfirm test completes within expected latency SLA (< 5 s)
Negative‑path assertions (wrong code, expired code)Validates error handlingInclude at least one invalid‑code test per channel
Post‑login state checkConfirms true authentication successAssert presence of a user‑specific element after verify
Teardown of OTP messagesPrevents stale code reuseAfter each test, delete retrieved email/SMS
Parallel execution respecting rate limitsAvoids provider throttlingMonitor provider response codes; no 429s
CI artifact collection on failureSpeeds root‑cause analysisScreenshots and logs attached to failed job
Flake detection and trackingMaintains trust in suiteFlake rate < 2% over last 20 runs
Documentation of OTP helperEnables onboardingHelper functions have docstrings and example usage
Version‑controlled test specsAllows review and rollbackAll test files in repo, PRs required for changes

Run this checklist as part of your Definition of Done for any new 2FA‑related feature.

How to Automate Two-Factor Authentication Testing (Step‑by‑Step): Closing Takeaways

Automating two‑factor authentication testing transforms a painful, manual bottleneck into a reliable, repeatable gatekeeper for security‑critical paths. Start by quantifying the ROI: if your team spends more than a few minutes per release on manual OTP entry, invest in a framework that lets you inject or retrieve OTPs programmatically. Choose a tool that matches your language stack, offers strong support for isolated contexts, and provides mechanisms to mock or intercept the OTP delivery channel. Build your tests around stable locators—prefer data‑test attributes or role‑based selectors—and replace every time.sleep with explicit waits that poll for the OTP or the UI state it drives. Isolate each test with fresh accounts, disposable mailbox or SIM numbers, and rigorous teardown that removes OTP messages and session tokens. In CI, guard secrets with the provider’s vault, limit parallelism to stay within OTP‑provider rate limits, and publish screenshots, logs, and JUnit reports on failure to accelerate debugging. Leverage autonomous exploration platforms like SUSA to generate an initial test suite from a simple crawl, then refine the exported specs with your own OTP helper and negative‑path checks. Finally, institutionalize a lightweight checklist and flake‑monitoring process so the test suite stays trustworthy as the application evolves. By following these steps, you’ll turn 2FA from a testing afterthought into a continuously validated assurance that your authentication gates remain solid under every release.

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free