How to Test Two-Factor Authentication on Web (Complete Guide)

Two‑factor authentication (2FA) has moved from an optional extra to a baseline security control for most web applications. When a login flow requires a second factor, attackers must compromise both th

May 07, 2026 · 19 min read · How-To Guides

Why Testing Two‑Factor Authentication Matters

Two‑factor authentication (2FA) has moved from an optional extra to a baseline security control for most web applications. When a login flow requires a second factor, attackers must compromise both the password and something the user possesses—or a biometric trait—to gain access. In production, a broken 2FA implementation can open the door to account takeover, credential stuffing, or even regulatory penalties if the failure exposes personal data.

Beyond security, 2FA touches usability. Users abandon flows that are confusing, slow, or inaccessible. A poorly designed OTP entry field, a missing fallback for users without phones, or an inaccessible error message can drive away legitimate traffic and increase support costs.

Testing 2FA therefore serves two goals: verify that the security guarantee holds under realistic conditions, and confirm that the experience works for the full spectrum of users who will encounter it in the wild. The following guide walks through a complete test matrix, manual and automated techniques, persona‑driven exploration, and production‑only edge cases that often slip past scripted suites.

Understanding Web‑Based 2FA Flows

Before designing tests, clarify which second‑factor mechanisms your application supports. Each type introduces distinct failure modes and test considerations.

Common 2FA Types on the Web

FactorTypical ImplementationUser InteractionCommon Failure Points
SMS OTPTwilio, AWS SNS, or custom gateway sends a 6‑digit code via textUser copies code from SMS into a form fieldCarrier delays, number reuse, SIM‑swap, missing fallback when SMS fails
Email OTPSMTP or transactional email service sends a code or magic linkUser checks inbox, copies code or clicks linkSpam filtering, latency, link expiration, email provider blocking
TOTP (Authenticator app)Server shares a secret via QR code; apps like Google Authenticator, Authy generate time‑based codesUser opens app, types current 6‑digit codeClock drift, secret leakage during QR scan, backup‑code handling
Push notificationServer pushes a challenge to a registered mobile app; user taps Approve/DenyUser responds in the appNetwork loss, app not installed, push token mismatch
Hardware token (U2F/WebAuthn)Browser performs a cryptographic challenge with a USB/NFC/Bluetooth deviceUser taps/button on deviceBrowser support, missing user gesture, transport errors
Backup codesPre‑generated list of one‑time use codes shown during enrollmentUser enters a code when primary factor unavailableCodes leaked, not regenerated after use, insufficient entropy

Each factor can be combined with others (e.g., password + TOTP + backup codes) or offered as alternatives (user chooses SMS or email). Your test matrix must cover the specific combination(s) your product ships.

Comprehensive Test Matrix

Below is a detailed matrix that separates test ideas into categories. Use it as a checklist when designing manual or automated suites. Each row includes a brief description, the factor(s) it targets, and the expected outcome.

CategoryTest IDDescriptionFactors CoveredExpected Result
Happy PathHP‑1Successful login with correct password and valid OTP from primary factorAllUser reaches post‑login page, session cookie set
HP‑2Login using backup code after losing primary deviceBackup codesAccess granted, backup code marked as used
HP‑3Login via push notification approval on registered devicePushSession established after user taps Approve
HP‑4Login with TOTP generated 30 seconds before expirationTOTPAccess granted; code still valid
HP‑5Login with email OTP after clicking magic link (no code entry)Email (link)Direct redirect to authenticated state
Error Paths – User InputEP‑1Entering incorrect OTP (wrong digits)AllError message shown, login remains blocked, retry counter increments
EP‑2Submitting empty OTP fieldAllValidation error, focus remains on OTP input
EP‑3Pasting OTP with extra whitespace or newlineAllSystem trims input or rejects with clear message
EP‑4Entering OTP after expiration (e.g., waiting 2 minutes for a 60‑second code)TOTP, SMS, EmailError indicating code expired, option to resend
EP‑5Using a previously used backup codeBackup codesError stating code already consumed
Error Paths – System BehaviorEP‑6Rate limiting after N failed OTP attempts (e.g., lockout for 15 min)AllFurther OTP submissions blocked, lockout timer displayed
EP‑7Server returns 429 Too Many Requests on OTP resendAllUser sees friendly message, resend button disabled temporarily
EP‑8Network failure during OTP delivery (simulate latency or dropout)SMS, Email, PushUser can request resend; no infinite spinner
EP‑9Server clock skew causing TOTP validation failureTOTPClear error about time mismatch, offer to resync
EP‑10Invalid QR code (corrupted or wrong secret) during enrollmentTOTPEnrollment fails, user prompted to retry or use manual entry
Edge Cases – Device & Account ChangesEC‑1Changing phone number and attempting SMS OTP with old numberSMSSystem sends OTP to new number only; old number receives nothing
EC‑2Adding a second authenticator app (multiple TOTP devices)TOTPBoth devices generate valid codes; revoking one does not affect the other
EC‑3Losing access to primary factor and using account recovery flow (email link + backup code)Email, Backup codesRecovery succeeds, forces re‑enrollment of 2FA
EC‑4Switching browsers mid‑session (e.g., login in Chrome, continue in Firefox)AllSession not shared; user prompted for 2FA again in second browser
EC‑5Using a private/incognito window after loginAllNo session persisted; re‑authentication required
AccessibilityAC‑1OTP field readable by screen readers (proper label, aria‑live for errors)AllAnnounced correctly, error messages announced without delay
AC‑2Sufficient color contrast for OTP input and error text (WCAG AA)AllContrast ratio ≥ 4.5:1
AC‑3Ability to complete 2FA using keyboard only (Tab to field, Enter to submit)AllNo mouse required; focus trap avoided
AC‑4Providing alternative OTP delivery for users with disabilities (e.g., voice call for SMS)SMSOption to receive voice call available and functional
AC‑5Error messages avoid reliance on color alone (e.g., icon + text)AllUsers with color blindness can discern error state
Security & PrivacySP‑1OTP not leaked in URL, referrer header, or logsAllNo OTP appears in network tokens, server logs masked
SP‑2OTP resend endpoint enforces per‑IP/user throttlingAllAbuse attempts blocked after threshold
SP‑3Backup codes displayed only once, then encrypted storageBackup codesCodes not recoverable after initial view
SP‑4QR code secret transmitted over TLS only, no cachingTOTPNo intermediate storage of secret
SP‑5Push notification payload contains no reusable secretPushReplay attack ineffective
SP‑6Session cookie marked Secure, HttpOnly, SameSite=Strict after 2FAAllCookie not accessible to JS, not sent on cross‑site requests
SP‑7No password‑reset link sent after successful 2FA login (to avoid credential leakage)AllPassword reset flow requires re‑authentication

The matrix above can be copied into a test‑management tool; each test ID becomes a traceable item for regression tracking.

Manual Testing Approach

Even with automation, a hands‑on pass catches nuances that scripts ignore—especially around timing, UI state, and human perception.

Preparation

  1. Create a test matrix spreadsheet with the IDs from the table above. Add columns for “Pass/Fail”, “Observations”, and “Bug ID”.
  2. Provision test accounts that have 2FA enabled but no existing session cookies. Use a password manager to store credentials securely.
  3. Set up factor simulators:

Execution Steps

  1. Login page – Enter username and password correctly. Verify that the OTP field appears only after successful primary credential validation (some apps show it up front, which can be a security issue).
  2. OTP delivery – Trigger the OTP send (often automatic after password). Note the latency; start a timer.
  3. Input OTP – Copy the code from the simulator and paste into the field. Observe whether the field auto‑trims spaces, whether the submit button enables only after a valid length entry, and whether any inline validation appears.
  4. Submit – Click the button or press Enter. Check for:
  1. Error paths – Repeat steps 1‑4 while injecting the specific fault from the matrix (wrong OTP, empty field, expired code, etc.). Verify that:
  1. Backup code flow – Log out, then attempt login with a backup code. Confirm the code is consumed and cannot be reused.
  2. Accessibility check – Navigate using only the keyboard. Run a screen reader (NVDA, VoiceOver) and confirm that:
  1. Security sniffing – With the browser’s devtools Network tab, ensure no OTP appears in request URLs, headers, or payloads. Check that any logging endpoints (if you have access) mask the OTP.

Reporting

When a test fails, capture:

Attach these to a bug ticket and link back to the test matrix ID for traceability.

Automated Testing Approaches

Automation shines for regression and for exercising the happy path at scale. The main challenge is handling the out‑of‑band nature of OTP delivery. Below are patterns for the most common web test frameworks.

General Strategy

  1. Decouple OTP retrieval from UI interaction – Have a separate helper that fetches the code from the simulated channel (email, SMS, etc.) and returns it to the test.
  2. Make the helper configurable – Switch between real services (for staging) and mocks (for CI) via environment variables.
  3. Keep tests deterministic – Use fixed seeds for TOTP (many libraries allow setting the clock) or pre‑generated backup codes.

Example: Cypress with Email OTP via Mailinator


// cypress/support/otpHelper.js
const imap = require('imap');
const { simpleParser } = require('mailparser');

function getMailinatorCode(email, password) {
  return new Cypress.Promise((resolve, reject) => {
    const imapConfig = {
      user: email,
      password,
      host: 'imap.mailinator.com',
      port: 993,
      tls: true,
      tlsOptions: { rejectUnauthorized: false }
    };

    const box = imap(imapConfig);
    box.once('ready', () => {
      box.openBox('INBOX', false, (err) => {
        if (err) return reject(err);
        box.search(['UNSEEN', ['SUBJECT', 'Your login code']], (err, results) => {
          if (err) return box.end().then(() => reject(err));
          if (results.length === 0) return box.end().then(() => reject('No matching email'));
          const fetch = box.fetch(results[0], { bodies: '' });
          fetch.on('message', (msg, seqno) => {
            let body = '';
            msg.on('body', (stream, info) => {
              stream.on('data', (chunk) => { body += chunk.toString('utf8'); });
            });
            msg.once('end', () => {
              simpleParser(body, (err, parsed) => {
                if (err) return box.end().then(() => reject(err));
                const code = parsed.text.match(/\b\d{6}\b/);
                box.end().then(() => resolve(code ? code[0] : null));
              });
            });
          });
        });
      });
    });
    box.once('error', (err) => reject(err));
    box.connect();
  });
}

// cypress/integration/2fa_login.spec.js
describe('Login with email OTP', () => {
  const TEST_USER = Cypress.env('TEST_USER');
  const TEST_PASS = Cypress.env('TEST_PASS');
  const EMAIL = Cypress.env('MAILINATOR_EMAIL');
  const EMAIL_PASS = Cypress.env('MAILINATOR_PASS');

  beforeEach(() => {
    cy.visit('/login');
    cy.get('#username').type(TEST_USER);
    cy.get('#password').type(TEST_PASS, { log: false });
    cy.get('#loginSubmit').click();
  });

  it('should succeed with OTP from email', () => {
    // wait for OTP email (max 30 s)
    cy.task('getOtpCode', { email: EMAIL, password: EMAIL_PASS, timeout: 30000 })
      .then((code) => {
        expect(code).to.be.a('string').and.have.lengthOf(6);
        cy.get('#otpCode').type(code);
        cy.get('#otpSubmit').click();
        cy.url().should('include', '/dashboard');
        cy.getCookie('session').should('exist');
      });
  });
});

Explanation

Example: Playwright with TOTP Clock Control


// tests/2fa-totp.test.js
const { test, expect } = require('@playwright/test');
const speakeasy = require('speakeasy');

test.describe('Login with TOTP', () => {
  const SECRET = 'JBSWY3DPEHPK3PXP'; // base32 secret for known test account
  const TEST_USER = process.env.TEST_USER;
  const TEST_PASS = process.env.TEST_PASS;

  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
    await page.fill('#username', TEST_USER);
    await page.fill('#password', TEST_PASS);
    await page.click('#loginSubmit');
  });

  test('should accept a valid TOTP', async ({ page }) => {
    // Freeze time to a known timestamp so OTP is predictable
    const token = speakeasy.totp({
      secret: SECRET,
      encoding: 'base32',
      step: 30,
      window: 0,
      time: Math.floor(Date.now() / 1000) * 1000 // align to 30‑second boundary
    });

    await page.fill('#otpCode', token);
    await page.click('#otpSubmit');

    await expect(page).toHaveURL(/.*\/dashboard/);
    const cookie = await page.context().cookies();
    expect(cookie.some(c => c.name === 'session' && c.secure && c.httpOnly)).toBeTruthy();
  });

  test('should reject an expired TOTP', async ({ page }) => {
    // Use a token from 2 minutes ago
    const past = Math.floor(Date.now() / 1000) - 120;
    const token = speakeasy.totp({
      secret: SECRET,
      encoding: 'base32',
      step: 30,
      window: 0,
      time: past * 1000
    });

    await page.fill('#otpCode', token);
    await page.click('#otpSubmit');

    const error = await page.locator('.otp-error');
    await expect(error).toBeVisible();
    await expect(error).toHaveText(/code is expired|invalid/);
  });
});

Explanation

Handling SMS with a Mock Gateway

Many teams run a local mock SMS service (e.g., using express and nexmo-mock) that writes incoming messages to a file or a REST endpoint. The test then polls that endpoint for the latest message.


// pseudo‑code for a helper
async function fetchSmtpOtp(phoneNumber) {
  const resp = await fetch(`http://localhost:4000/messages?to=${encodeURIComponent(phoneNumber)}`);
  const msgs = await resp.json();
  const latest = msgs.sort((a, b) => b.timestamp - a.timestamp)[0];
  const code = latest.body.match(/\b\d{6}\b/);
  return code ? code[0] : null;
}

Integrate this helper as a cy.task or Playwright request step, just like the email example.

Tool Comparison

FrameworkLanguageBuilt‑in OTP helpersEase of mocking external channelsTypical CI integrationComments
CypressJavaScript/TypeScriptNone (rely on cy.task)Good – can spin up mock servers or use email/SMS APIsCypress Dashboard, GitHub ActionsStrong UI debugging, time‑travel; requires Node for async helpers
PlaywrightJavaScript/TypeScript, Python, .NET, JavaNone (but easy to call external libs)Very good – built‑in request/context isolationPlaywright Test, GitHub ActionsMulti‑browser, auto‑wait, traces; works headless out of the box
SeleniumJava, C#, Python, Ruby, JSNoneModerate – need external libraries for mail/SMSSelenium Grid, DockerMature, but verbose; slower start‑up
TestCafeJavaScript/TypeScriptNoneGood – can use Node helpersTestCafe Studio, CINo WebDriver needed, automatic waiting

Pick the framework that matches your team’s existing test stack; the patterns above translate easily across them.

Persona‑Driven Autonomous Exploration

Scripted tests excel at verifying known paths, but they often miss surprises that arise from real‑world user behavior. Autonomous QA platforms that simulate diverse personas can uncover issues such as confusing error wording, unexpected UI states, or accessibility gaps that only appear under certain interaction styles.

How Autonomous Exploration Works

  1. Profile Creation – Each persona defines a set of tendencies:
  1. Exploration Engine – The platform drives a real browser (or mobile emulator) and, based on the persona’s profile, makes weighted decisions at each interaction point: which field to focus, whether to read helper text, how long to wait before submitting, whether to try an alternative OTP channel, etc.
  1. State Tracking – It records visited screens, DOM snapshots, and network calls. If it encounters a dead end (e.g., a button that does nothing) or a crash, it logs the exact sequence that led there.
  1. Learning Across Runs – Failed or blocked actions are stored; subsequent runs avoid repeating fruitless paths and focus on unexplored areas, increasing coverage over time.

Applying Persona Exploration to 2FA

PersonaWhat They Might Do DifferentlyPotential Bug Surface
CuriousAfter entering password, clicks the “Having trouble?” link, tries voice call OTP, then returns to SMS.Missing synchronization between OTP channels; voice call OTP not accepted after SMS attempt.
ImpatientSubmits the login form before the OTP field appears, then rapidly pastes a code and hits Enter multiple times.Race condition where OTP field is not yet attached to DOM, causing stale element, leading to silent failure.
NoviceLooks for a “Show code” button to reveal the OTP as they type (common in password fields).No affordance to reveal OTP, causing frustration and increased support tickets.
AdversarialSends thousands of OTP requests with varied phone numbers, attempts to guess 6‑digit codes via brute force.Insufficient rate limiting on OTP resend endpoint, enabling SMS pumping or DoS.
ElderlyUses a tablet with zoom enabled; the OTP input becomes obscured by the virtual keyboard.Layout breaks at 200 % zoom, making the field inaccessible.
AccessibilityNavigates with Tab, expects error messages to be announced instantly via aria-live.Error message inserted into a static
without live region, causing screen reader silence.
Power userUses Ctrl+V to paste OTP from password manager, expects the field to accept pasted content without extra spaces.Field trims leading/trailing spaces incorrectly, rejecting valid paste.
Backup‑code userAfter losing phone, tries to log in with a backup code, then immediately attempts another login with the same code.System does not mark backup code as used until after successful login, allowing reuse.

These scenarios are rarely captured in a deterministic script because they depend on timing, exploratory clicks, or persona‑specific tolerances. An autonomous platform that simulates these behaviors will surface them early, often before they reach production.

#### Mentioning SUSA (first organic reference)

SUSA’s autonomous agent can be pointed at a staging URL with 2FA enabled. By assigning it a mixture of the personas above, it will automatically:

The resulting report includes a list of unique screen states, any JavaScript errors, and a flow‑by‑flow verdict (PASS/FAIL). Teams can then prioritize fixes based on the severity and frequency of the issues observed across personas.

#### Second SUSA Reference (optional, still within limit)

If your team already uses SUSA for regression, you can configure it to remember which OTP delivery method succeeded for a given account during a prior run. On the next execution, the agent will prioritize that method while still occasionally trying alternatives to catch regressions in the fallback paths. This cross‑session learning reduces redundant exploration while maintaining coverage of edge cases.

Production‑Only Edge Cases

Even the most thorough pre‑release suite can miss issues that only manifest under real‑world traffic, infrastructure quirks, or user‑specific conditions. Below are common production‑only pitfalls for web 2FA and how to detect or mitigate them.

IssueWhy It Appears Only in ProdDetection / Mitigation
Carrier‑specific SMS delaysCertain regions experience filtering or greylisting that adds seconds to minutes of latency.Use real phone numbers from target markets in staging; monitor OTP delivery time SLO; set up alerts if median latency > 10 s.
SIM‑swap attacksAttacker convinces carrier to port victim’s number; OTP goes to attacker’s device.Implement secondary verification (e.g., email confirmation) when a new phone number is added; monitor for rapid number changes.
Email provider throttlingBulk OTP sends trigger spam filters, causing delayed or blocked delivery.Segment OTP traffic by domain; maintain good sender reputation; provide a “Resend via SMS” fallback when email fails.
Browser extension interferencePassword managers or security extensions may auto‑fill OTP fields incorrectly or block submission.Test with popular extensions (LastPass, 1Password, Bitdefender) enabled; provide a autocomplete="one-time-code" hint to help managers.
Timezone drift on user deviceUsers traveling across zones may have device clocks off by several minutes, breaking TOTP validation.Allow a larger verification window (e.g., ±2 minutes) while logging anomalies; prompt users to sync time if failures exceed threshold.
Concurrent sessionsUser logs in on two devices simultaneously; one session completes 2FA while the other is still waiting for OTP.Tie OTP validation to a specific session ID; invalidate pending OTPs when a new login begins.
Backup code leakage via logsMisconfigured logging might capture backup codes when users view them.Audit all loggers for PII; mask backup codes; use hash‑only storage after first view.
Network partitioning during pushMobile device loses data connection just after receiving push challenge; user approves but server never gets acknowledgment.Make push approval idempotent; allow retry within a short window; surface a “Didn’t receive response?” option.
Upgrade of authenticator appNew version changes the way secrets are stored (e.g., moves to encrypted vault) causing previously working TOTP to fail.Provide a “Re‑sync” flow that lets users re‑scan QR code without needing to disable/re‑enable 2FA.
Legal jurisdiction restrictionsSome countries prohibit storing OTPs or require data localization for SMS gateways.Ensure your OTP provider complies with local laws; have region‑specific fallback (e.g., local SMS gateway).
High‑traffic burst causing rate‑limit false positivesDuring a sale or product launch, legitimate users may hit OTP resend limits unintentionally.Dynamically adjust thresholds based on recent successful login rate; exempt known good IPs temporarily.

Monitoring these signals in production (via logs, metrics, and user‑feedback tickets) helps you catch regressions that unit or UI tests cannot anticipate.

Concise Checklist for QA Engineers

Copy this list into your test‑management tool or keep it as a reference before a release.

Run this checklist after any change to authentication, user‑settings, or notification services.

Closing Takeaways

Two‑factor authentication is more than a checkbox on a security compliance list; it is a dynamic interaction point that blends cryptography, delivery channels, UI design, and human behavior. Effective testing therefore needs:

  1. A comprehensive matrix that isolates happy paths, error conditions, accessibility, and security concerns.
  2. Manual exploratory sessions that catch timing‑sensitive bugs, confusing copy, and layout quirks only a human eye (or a persona‑driven bot) can spot.
  3. Automated regression that reliably verifies the core flow while delegating OTP retrieval to dedicated helpers or mocked services.
  4. Production awareness of carrier, email, device, and environmental factors that can turn a lab‑perfect flow into a real‑world failure.
  5. Persona‑driven autonomous testing—using tools like SUSA—to surface edge cases that scripted tests never consider, especially around alternative flows, accessibility, and misuse patterns.

By combining these approaches, you gain confidence that your 2FA implementation not only thwarts attackers but also serves every legitimate user with clarity, speed, and dignity. Treat 2FA as a living feature: revisit the matrix whenever you add a new factor, change a delivery vendor, or adjust security policies, and let your test suite evolve alongside the threats and user expectations it aims to protect.

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