Two-Factor Authentication Testing Checklist (2026)

Two-Factor Authentication Testing Checklist (2026) provides a comprehensive, actionable list for verifying that your 2FA implementation works correctly under normal, error, and edge conditions. Use th

March 02, 2026 · 16 min read · Testing Checklists

Two-Factor Authentication Testing Checklist (2026) provides a comprehensive, actionable list for verifying that your 2FA implementation works correctly under normal, error, and edge conditions. Use this guide as a living reference that you can bookmark, adapt to your tech stack, and integrate into CI pipelines. The checklist is organized by functional area, each item includes a clear pass criterion, a real‑world example, and notes on how manual and automated approaches differ. At the end you’ll find a quick‑reference matrix and a short section on how an autonomous explorer such as SUSA can exercise most of these points in a single pass.

Two-Factor Authentication Testing Checklist (2026): Happy Path Scenarios

Primary Flow with SMS OTP

Description – User enters valid credentials, receives an SMS OTP, inputs the code, and gains access.

Pass Criteria – OTP arrives within the configured delivery window (typically 30‑60 seconds), the code is accepted on first attempt, and the session is established without additional prompts.

Manual Steps – 1) Log in with username/password. 2) Wait for SMS. 3) Enter OTP. 4) Verify landing page.

Automated Approach – Use a mock SMS gateway (e.g., Twilio test credentials) that forwards the OTP to a webhook your test harness reads. In Playwright:


const otp = await page.waitForResponse(r => r.url().includes('/sms-webhook') && r.request().method() === 'POST')
  .then(res => res.json())
  .then(json => json.code);
await page.fill('#otp-input', otp);
await page.click('#verify-button');
await expect(page).toHaveURL(/^\/dashboard/);

Notes – Ensure the OTP is time‑based (TOTP) or event‑based (HOTP) as appropriate and that the verification endpoint enforces expiration.

Primary Flow with Authenticator App (TOTP)

Description – After username/password, the user opens a TOTP app (Google Authenticator, Authy, etc.) and enters the 6‑digit code.

Pass Criteria – Code validates within the current 30‑second window, no network call is required for verification, and login succeeds.

Manual Steps – 1) Complete first factor. 2) Generate code in authenticator app. 3) Enter code. 4) Confirm access.

Automated Approach – Use a library such as otplib to generate the expected TOTP from the shared secret stored in your test fixture. Example in Java (JUnit + Selenium):


String secret = Base32.decode("JBSWY3DPEHPK3PXP");
String otp = new GoogleAuthenticator().generateTotp(secret);
driver.findElement(By.id("otp")).sendKeys(otp);
driver.findElement(By.id("verify")).click();
assertTrue(driver.getCurrentUrl().contains("/home"));

Notes – Verify that the secret is never logged or exposed in test output.

Primary Flow with Push Notification

Description – After credentials, a push request is sent to the user’s registered device; approving the push completes authentication.

Pass Criteria – Push arrives within 5 seconds, tapping “Approve” in the authentication app logs the user in, and denying the push results in a clear failure message.

Manual Steps – 1) Enter credentials. 2) Check device for push. 3) Tap approve/deny. 4) Observe outcome.

Automated Approach – If you control the push provider, expose a test API that returns a pre‑approved token. In an Appium test for Android:


// Simulate push approval via backend
RestAssured.given()
  .header("Authorization", "Bearer test-token")
  .post("https://auth.example.com/v2/push/approve")
  .then()
  .statusCode(200);
// Then verify the web session
AndroidElement welcome = driver.findElement(By.id("welcome_msg"));
assertEquals(welcome.getText(), "Welcome back!");

Notes – Test both approve and deny paths; ensure the deny path shows a user‑friendly error and does not leave a hanging session.

Primary Flow with Hardware Token (U2F/WebAuthn)

Description – After password, the user inserts a USB/NFC/BLE token and performs a gesture (tap).

Pass Criteria – The browser completes the credential assertion ceremony, returns a valid signature, and the server validates it.

Manual Steps – 1) Login with password. 2) Insert token. 3) Touch token. 4) Confirm login.

Automated Approach – Use WebDriverBiDi or Chrome DevTools Protocol to inject a virtual authenticator. In Playwright:


await page.context().addInitScript(() => {
  navigator.credentials.get = () => Promise.resolve({
    id: "test-cred",
    rawId: base64url.encode(new Uint8Array([1,2,3])),
    response: { authenticatorData: new ArrayBuffer(0), signature: new ArrayBuffer(0) },
    type: "public-key"
  });
});
await page.fill("#username", "user");
await page.fill("#password", "pw");
await page.click("#login");
await page.waitForURL(/^\/app/);

Notes – Verify that the relying party ID matches your domain and that the challenge is freshly generated per login.

Multi‑Device Enrollment

Description – User registers a second 2FA method (e.g., adds authenticator app after already having SMS).

Pass Criteria – Both methods are usable for login, and removing one does not affect the other.

Manual Steps – 1) Enroll first method. 2) Log in, navigate to 2FA settings, add second method. 3) Log out and log in using each method separately.

Automated Approach – Store each enrollment’s secret or credential ID in a test vault; after enrollment, run two parallel login flows using each credential set.

Notes – Test that the UI prevents enrollment of the same method twice (e.g., cannot add a second SMS number if already present).

Two-Factor Authentication Testing Checklist (2026): Error Handling and Failure Cases

OTP Delivery Failure

Description – SMS gateway returns an error or the OTP never arrives.

Pass Criteria – The system shows a clear “Didn’t receive the code?” link, allows resend after a configurable cooldown (e.g., 30 seconds), and does not expose gateway raw errors to the user.

Manual Steps – 1) Trigger login. 2) Simulate gateway failure (e.g., block outbound SMS via network throttling). 3) Verify UI behavior. 4) Attempt resend after cooldown.

Automated Approach – In a test environment, mock the SMS API to return HTTP 500 on the first request and HTTP 200 on the second. Assert that the resend button is disabled until the cooldown expires.


await page.route('**/sms/send', async route => {
  const req = route.request();
  if (req.headers()['x-test-attempt'] === '1') {
    await route.fulfill({ status: 500, body: JSON.stringify({error: 'internal'}) });
  } else {
    await route.fulfill({ status: 200, body: JSON.stringify({code: '123456'}) });
  }
});

Notes – Log the failure internally for ops alerting but keep the user message generic.

Invalid OTP Entry

Description – User types an incorrect code.

Pass Criteria – After each invalid attempt, the remaining attempts counter decrements; after the configured max (typically 5), the account is locked or a secondary recovery flow is triggered.

Manual Steps – 1) Enter wrong OTP four times. 2) Verify remaining attempts shown. 3) Enter wrong OTP a fifth time. 4) Confirm lockout message and that further attempts are rejected.

Automated Approach – Loop through invalid OTPs, checking the response body for the attempts remaining field. After max attempts, assert that the endpoint returns HTTP 423 (Locked) or redirects to recovery.


for i in range(5):
    r = client.post('/verify-otp', json={'otp': '000000'})
    assert r.json()['attempts_left'] == 4 - i
assert r.status_code == 423

Notes – Ensure lockout does not leak whether the username exists (use same response timing for valid/invalid OTP).

Expired OTP

Description – User delays entering the code past its validity period.

Pass Criteria – The system rejects the OTP with a message like “Code has expired; please request a new one” and does not count the attempt against the lockout counter.

Manual Steps – 1) Request OTP. 2) Wait beyond TTL (e.g., 70 seconds for a 60‑second TTL). 3) Submit OTP. 4) Verify expiration error.

Automated Approach – Freeze the system clock using a library like timewarp (Java) or loose (JavaScript) to simulate passage of time, then submit the OTP and assert the error code.


const { clock } = require('loose');
clock.install();
clock.tick(70000); // 70 seconds
const res = await request.post('/verify-otp').send({otp: '123456'});
expect(res.body.error).toBe('expired');

Notes – Verify that a new OTP can be requested immediately after expiration without hitting a rate limit.

Push Notification Timeout / No Response

Description – Push is sent but the user does not respond within the allotted time (e.g., 60 seconds).

Pass Criteria – The authentication request fails with a clear “No response received; try again” message and the session returns to the OTP/password prompt.

Manual Steps – 1) Trigger login with push. 2) Do not interact with the push notification. 3) Wait for timeout. 4) Verify error state.

Automated Approach – Mock the push service to never send an acknowledgment. After the client‑side timeout, assert that the authentication endpoint returns HTTP 408 and the UI shows the retry button.


await page.waitForTimeout(65000); // exceed client timeout
const errorMsg = await page.innerText('.auth-error');
expect(errorMsg).toContain('No response');

Notes – Ensure that a timed‑out push does not leave a half‑authenticated session on the server side.

Hardware Token Not Detected

Description – User inserts a U2F token but the browser does not detect it (e.g., missing drivers, disabled WebAuthn).

Pass Criteria – The browser shows a platform‑specific error (“Security key not found”) and offers to retry or fall back to another 2FA method.

Manual Steps – 1) Login to password step. 2) Remove or disable the token. 3) Attempt authentication. 4) Confirm fallback UI.

Automated Approach – In Chrome DevTools Protocol, disable the virtual authenticator before the assertion call.


await page.context().overridePermissions(page.url(), []); // remove authenticator permission
await page.click('#login-with-token');
const msg = await page.innerText('.error');
expect(msg).toMatch(/not found/i);

Notes – Verify that falling back to SMS or TOTP still works and that the fallback is logged for audit.

Network Interruption During OTP Submission

Description – The user’s connection drops after sending the OTP but before receiving the server response.

Pass Criteria – The client retries the request automatically (if implemented) or shows a “Connection lost; please try again” message without creating a duplicate authentication attempt on the server.

Manual Steps – 1) Submit OTP. 2) Immediately disconnect the test device from Wi‑Fi. 3) Observe client behavior. 4) Reconnect and verify that only one verification attempt was processed.

Automated Approach – Use a tool like toxiproxy to drop the TCP connection after the client sends the request. Assert that the server logs a single request and the client shows a retry UI.


toxiproxy create otp-proxy -l localhost:8080 -u upstream:8080
toxiproxy toxic add otp-proxy --type timeout --attribute timeout=0
# run test, then remove toxic

Notes – Ensure idempotency on the verification endpoint (e.g., using a nonce or request ID) so retries do not cause state corruption.

Two-Factor Authentication Testing Checklist (2026): Accessibility and Inclusive Design

Screen Reader Compatibility – OTP Input

Description – Users relying on VoiceOver, TalkBack, or NVDA must be able to hear labels, error messages, and live regions.

Pass Criteria – The OTP field has an associated or aria-label, live region updates announce “Invalid code” or “Code accepted”, and focus moves appropriately after submission.

Manual Steps – 1) Enable screen reader. 2) Navigate to OTP field. 3) Hear label. 4) Enter invalid code. 5) Verify error announcement. 6) Enter valid code. 7) Verify success announcement and focus shift.

Automated Approach – Use axe-core with the aria-label rule and a custom test that checks for aria-live="assertive" on the error container. In Jest + axe:


const results = await axe.run(page, { rules: { 'label': { enabled: true } } });
expect(results.violations).toHaveLength(0);

Notes – Test on iOS VoiceOver and Android TalkBack; ensure that the OTP characters are not spoken individually unless the user prefers that mode.

Keyboard Navigation – Resend Link

Description – Users who cannot use a mouse must be able to trigger OTP resend via keyboard.

Pass Criteria – The “Resend code” link is focusable, operable with Enter/Space, and announces its purpose to assistive tech.

Manual Steps – 1) Tab to the resend link. 2) Verify visible focus indicator. 3) Press Enter. 4) Confirm a new OTP is sent.

Automated Approach – In Playwright, assert that the element is focusable and triggers a network request on keyboard.press('Enter').


await page.keyboard.press('Tab'); // move to resend link
await page.keyboard.press('Enter');
await page.waitForResponse(r => r.url().includes('/sms/resend'));

Notes – Ensure the link is not hidden behind a modal that traps focus without an escape mechanism.

Touch Target Size – Push Approve/Deny Buttons

Description – On mobile, the push notification’s approve and deny buttons must meet WCAG 2.2 minimum target size (24 × 24 dp).

Pass Criteria – Both buttons are at least 24 dp, have sufficient spacing (≥8 dp) from other interactive elements, and are operable with a single finger.

Manual Steps – 1) Receive a push notification. 2) Use a ruler tool or accessibility scanner to measure button dimensions. 3) Attempt to tap the button with a finger offset to confirm tolerance.

Automated Approach – Use Android’s UIAutomator to retrieve button bounds and assert width/height ≥ 24.


Rect approve = new Rect();
approveButton.getBoundsInScreen(approve);
assertTrue(approveButton.width() >= 24 && approveButton.height() >= 24);

Notes – Test both portrait and landscape orientations; ensure that system UI overlays (e.g., notch) do not reduce effective target size.

Color Contrast – Error Text

Description – Error messages must meet a contrast ratio of at least 4.5:1 against the background for normal text.

Pass Criteria – All error text (invalid OTP, expired, network error) passes the contrast check.

Manual Steps – 1) Trigger each error state. 2) Use a contrast analyzer (e.g., WebAIM) to verify ratio.

Automated Approach – Run axe with the color-contrast rule; assert zero violations.


const { violations } = await axe.run(page, { rules: { 'color-contrast': { enabled: true } } });
expect(violations).toHaveLength(0);

Notes – Also test in high‑contrast mode or forced colors browser setting.

Language Localization – OTP Prompt

Description – The OTP request message should be translatable and retain proper layout in right‑to‑left (RTL) languages.

Pass Criteria – Prompt text appears correctly translated, input field aligns to the right in RTL, and no truncation occurs.

Manual Steps – 1) Switch locale to Arabic (ar-SA). 2) Initiate 2FA flow. 3) Verify prompt reads “أدخل رمز التحقق” and that the input field is right‑aligned. 4) Enter a valid OTP.

Automated Approach – Use i18n test harness to assert that the rendered string matches the translation file and that the computed style direction is rtl.


expect(page.locator('#otp-prompt')).toHaveText(/أدخل رمز التحقق/i);
expect(await page.evaluate(() => getComputedStyle(document.querySelector('#otp-input')).direction)).toBe('rtl');

Notes – Verify that placeholder text does not get cut off when the language uses longer words (e.g., German).

Performance and Load Considerations for 2FA

Latency Budget – End‑to‑End Authentication

Description – The total time from credential submission to authenticated session should stay within product SLA (e.g., <2 seconds for SMS, <1 second for TOTP/push).

Pass Criteria – 95th‑percentile latency measured under typical load meets the SLA.

Manual Steps – 1) Use a stopwatch or browser devtools network timing. 2) Record time from POST /login (credentials) to successful redirect after 2FA. 3) Repeat 20 times, compute p95.

Automated Approach – In k6 script, measure http_req_duration for the verification endpoint and assert thresholds.


export let options = {
  thresholds: {
    http_req_duration: ['p(95)<2000'] // ms
  }
};
export default function () {
  const res = http.post('https://auth.example.com/login', JSON.stringify({username: 'user', password: 'pw'}));
  const otp = extractOtpFromResponse(res); // mock or pre‑known
  const verify = http.post('https://auth.example.com/verify-otp', JSON.stringify({otp}));
  check(verify, {'verify status': (r) => r.status === 200});
}

Notes – Include time for OTP generation/delivery in the measurement; if using a real SMS gateway, factor in carrier latency.

Throughput Under Load – Concurrent 2FA Requests

Description – System must handle expected peak concurrent authentications without degrading success rate.

Pass Criteria – With N concurrent users (e.g., 500), error rate < 0.5 % and average latency stays within SLA.

Manual Steps – 1) Use a load generator (Locust, JMeter) to spawn N virtual users each performing a full 2FA flow. 2) Monitor success/failure counts and latency. 3) Increase N until SLA breached.

Automated Approach – Define a Locust task set that logs in, requests OTP, verifies, and logs out. Run with --headless -u 500 -r 20 --run-time 5m.


class TwoFaUser(HttpUser):
    wait_time = between(1, 3)

    @task
    def complete_2fa(self):
        resp = self.client.post("/login", json={"username": "user", "password": "pw"})
        otp = self.extract_otp(resp)  # stub
        self.client.post("/verify-otp", json={"otp": otp})

Notes – Ensure that rate‑limiting on OTP sending is configured per‑account, not globally, to avoid false throttling spikes.

Resource Consumption – Memory & CPU per Authentication

Description – Each 2FA verification should not cause unbounded memory growth or CPU spikes that affect other services.

Pass Criteria – Average additional RAM < 5 MB and CPU < 5 % of a single core per verification under steady load.

Manual Steps – 1) Profile the verification service with a tool like perf or async-profiler. 2) Capture metrics over a 10‑minute window with a constant request rate. 3) Compute per‑request averages.

Automated Approach – In a CI job, run a short benchmark using wrk and collect pidstat output; assert that the delta stays within bounds.


wrk -t4 -c200 -d30s https://auth.example.com/verify-otp &
# collect pidstat during the window

Notes – Pay special attention to cryptographic operations (e.g., WebAuthn signature verification) and ensure they are offloaded to hardware or use constant‑time algorithms.

Cache Efficiency – OTP Nonce Replay Prevention

Description – Nonces or state tokens used to prevent OTP replay should be stored efficiently (e.g., in Redis with TTL).

Pass Criteria – Hit rate > 95 % for nonce lookups, memory usage stays predictable, and expired entries are evicted without blocking.

Manual Steps – 1) Generate a burst of OTP requests. 2) Monitor Redis keyspace_hits and keyspace_misses. 3) Verify that TTL deletion occurs after OTP expiry + safety margin.

Automated Approach – Use a Redis exporter in Prometheus and alert if evicted_keys_per_second spikes beyond a threshold.


- alert: HighOTPNonceEvictions
  expr: rate(redis_keyspace_evictions_total[5m]) > 100
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "High OTP nonce eviction rate"

Notes – Ensure that the nonce storage is sharded if you expect millions of concurrent authentications.

Security and Privacy Validation

OTP Entropy and Length

Description – The OTP must be sufficiently random and of adequate length to resist brute‑force.

Pass Criteria – OTP is at least 6 digits (10⁶ possibilities) for time‑based, or 8 digits for event‑based, and generated using a CSPRNG.

Manual Steps – 1) Request 10 000 OTPs from the service. 2) Perform statistical tests (frequency, runs) using a tool like dieharder. 3) Verify no obvious bias.

Automated Approach – In a unit test, mock the random source to return a known sequence and assert that the output matches the expected OTP after applying the HMAC‑based algorithm.


import otpauth
secret = bases32.decode('JBSWY3DPEHPK3PXP')
otp = otpauth.totp(secret, interval=30)
assert len(otp) == 6 and otp.isdigit()

Notes – Ensure that the secret is stored encrypted at rest and never transmitted in clear text.

Resistance to Replay Attacks

Description – Captured OTP or push approval token must not be reusable after successful use or after expiry.

Pass Criteria – Replaying a previously valid OTP results in rejection with a clear error; same for a used push nonce.

Manual Steps – 1) Perform a successful login and capture the OTP request payload. 2) Immediately resend the identical payload. 3) Verify rejection. 4) Wait until OTP expires and retry – should also be rejected (expired, not replay).

Automated Approach – In a test, store the verification request body, send it twice via the test client, and assert 400/401 on the second attempt.


const first = await agent.post('/verify-otp').send({otp: '123456'});
expect(first.status).toBe(200);
const second = await agent.post('/verify-otp').send({otp: '123456'});
expect(second.status).toBe(400);
expect(second.body.error).toMatch(/already used|invalid/);

Notes – Ensure that the server uses a one‑time nonce or a timestamp window that prevents reuse even if the attacker re‑sends within the same window.

Rate Limiting & Account Enumeration Protection

Description – Brute‑force attempts on OTP should be throttled, and error messages must not reveal whether a username exists.

Pass Criteria – After M failed OTP attempts (configurable, e.g., 10), further attempts return HTTP 429 with a generic message; success and failure responses have identical timing and body shape.

Manual Steps – 1) Attempt login with a known username and wrong OTP repeatedly. 2) Observe when throttling kicks in. 3) Repeat with an unknown username; verify same response.

Automated Approach – Use a script that sends OTP verification requests in a loop, measuring response time and status code. Assert that after the threshold, status is 429 and that the response body does not contain the username.


for i in {1..15}; do
  curl -s -w "%{http_code} %{time_total}\n" -X POST https://auth.example.com/verify-otp \
    -d '{"username":"alice","otp":"000000"}' -H "Content-Type: application/json"
done

Notes – Apply the same rate limit to resend endpoints to prevent OTP flooding.

Push Notification Privacy – No Personal Data in Payload

Description – The push notification sent to the user's device must not contain the username, email, or any PII in plain text.

Pass Criteria – The push payload contains only an opaque transaction ID; any user‑identifying data is encrypted or omitted.

Manual Steps – 1) Trigger a push‑based 2FA. 2) Use a device logcat or console to capture the push payload. 3) Verify absence of PII.

Automated Approach – Mock the push provider to forward the payload to a test endpoint; assert that the JSON does not contain fields like email, phone, or username.


expect(pushPayload).not.toHaveProperty('email');
expect(pushPayload).not.toHaveProperty('username');
expect(pushPayload).toHaveProperty('txnId');

Notes – Ensure that the transaction ID is cryptographically random and tied to a server‑side session that maps back to the user only after successful authentication.

WebAuthn Credential Protection – User Presence & Verification

Description – For WebAuthn, the authenticator must require user presence (touch) and optionally user verification (PIN/biometrics).

Pass Criteria – Registration and assertion ceremonies fail if the user does not provide the required gesture; the server rejects responses lacking the UP (User Present) flag or UV (User Verified) flag when required.

Manual Steps – 1) Attempt to register a credential without touching the token. 2) Verify error. 3) Repeat with touch but without PIN (if UV required). 4) Confirm failure. 5) Finally perform correct gesture and verify success.

Automated Approach – Use the virtual authenticator API to set userVerified to false and userPresent to false, then call navigator.credentials.get. Assert that the promise rejects with NotAllowedError.


await page.context().addInitScript(() => {
  navigator.credentials.get = () => Promise.reject(new Error('NotAllowedError'));
});
await page.click('#register-webauthn');
const msg = await page.innerText('.error');
expect(msg).toContain('presence');

Notes – Test both platform authenticators (Touch ID, Windows Hello) and roaming USB/NFC tokens.

Edge/Boundary Conditions and Race Conditions

Clock Skew Impact on TOTP Validation

Description – If the server’s system clock differs significantly from the client’s, valid OTPs may be rejected.

Pass Criteria – The server accepts OTPs generated within a configurable window (usually ±1 interval) and rejects those outside.

Manual Steps – 1) Change the server clock by +30 seconds (using date -s or NTP offset). 2) Generate an OTP on a correctly timed client device. 3) Submit OTP. 4) Verify acceptance/rejection according to window.

Automated Approach – In a Docker compose test, offset the auth service container’s clock via CAP_SYS_TIME and date. Use a test script to send OTPs and assert the expected outcome.


docker run --cap-add=SYS_TIME -e TZ=UTC auth-service date -s '+30 seconds'
# then run OTP verification tests

Notes – Document the maximum tolerated skew in your runbooks; consider using NTP with strict synchronization.

Simultaneous Enrollment and Authentication

Description – A user attempts to enroll a new 2FA method while an existing authentication session is active in another tab.

Pass Criteria – Enrollment succeeds, the new method is immediately usable for subsequent logins, and the existing session remains valid until its natural expiry.

Manual Steps – 1) Log in in Tab A. 2) Open Tab B, navigate to 2FA settings, add a new authenticator app. 3) In Tab A, perform a sensitive action that requires re‑authentication (e.g., change password). 4) Verify that the new method can be used for the step‑up authentication.

Automated Approach – Use two Playwright contexts sharing the same storage state; perform enrollment in context B, then trigger a step‑up in context A and assert that the new OTP is accepted.


const ctxA = await browser.newContext({ storageState: 'state.json' });
const ctxB = await browser.newContext({ storageState: 'state.json' });
// enroll in

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