OTP Verification Testing Checklist (2026)
Otp Verification Testing Checklist (2026) provides a concrete, step‑by‑step matrix that lets engineers validate every aspect of a one‑time password flow from entry to expiration.
Otp Verification Testing Checklist (2026) provides a concrete, step‑by‑step matrix that lets engineers validate every aspect of a one‑time password flow from entry to expiration.
Use this guide as a reference you can bookmark, copy into a test‑case management tool, or feed into an autonomous explorer such as SUSA to obtain immediate pass/fail verdicts without writing scripts.
Otp Verification Testing Checklist (2026) – Scope and Purpose
The checklist groups verification activities into seven logical areas: happy path, error handling, edge/boundary cases, accessibility, security/privacy, performance, and release readiness. Each area contains discrete, observable items with defined pass criteria, enabling both manual execution and automated assertion. By treating the OTP flow as a state machine (request → delivery → entry → validation → post‑validation actions), the checklist ensures that transitions, timeouts, and fallback paths are exercised under realistic conditions.
Why a checklist in 2026?
Modern applications embed OTP in multi‑factor authentication (MFA), password‑less login, transaction confirmation, and account recovery. Attack surfaces have expanded to include SIM‑swap, phishing‑resistant codes, and time‑synchronization attacks. Regulatory regimes (e.g., PSD2 SCA, NIST 800‑63B) now require documented test evidence for rate limiting, entropy, and replay protection. A checklist that captures these concerns gives auditors a traceable artifact and helps QA teams catch regressions introduced by library updates or UI redesigns.
How to use the checklist
- Select the relevant area based on the feature under test (e.g., login vs. payment confirmation).
- Execute each item and record the result (PASS/FAIL/N/A).
- Attach evidence (screenshots, logs, network traces) for failed items.
- Automate repeatable items using the code snippets provided later.
- Review trends across releases to spot drift in OTP reliability.
Otp Verification Testing Checklist (2026) – Happy Path Test Cases
Happy‑path validation confirms that a legitimate user can request, receive, and submit an OTP without obstruction. The following table lists the core items, pass criteria, and a real‑world example for each.
| # | Test Item | Pass Criteria | Example |
|---|---|---|---|
| 1 | OTP request button is enabled after valid input (e.g., phone number) | UI element becomes clickable; no validation error shown | Entering "+1 555 123 4567" enables "Send Code". |
| 2 | Request triggers backend API with correct payload | POST /api/v1/otp/request includes phone, purpose, timestamp; returns 200 with session_id | {"phone":"+15551234567","purpose":"login"} → 200 {session_id:"abc123"}. |
| 3 | Delivery channel (SMS, email, push) delivers code within expected latency | Code arrives ≤ 10 s for SMS, ≤ 5 s for email/push; no duplicate messages | SMS received in 3.2 s; body: "Your login code is 483921". |
| 4 | OTP entry field accepts exactly the numeric length defined (usually 4‑6 digits) | Field rejects non‑numeric input; accepts leading zeros; max length enforced | Typing "001234" succeeds; "12a45" blocked. |
| 5 | Submit button becomes active only after required OTP length entered | Button disabled until 6 digits present; enables on 6th digit | After 5th digit, button grey; after 6th, green. |
| 6 | Validation API returns success and proceeds to next step | POST /api/v1/otp/verify with session_id and code returns 200 and redirect or token | Response includes JWT; UI navigates to dashboard. |
| 7 | Post‑validation state is clean (no residual OTP UI) | OTP request button resets to initial state; any timers cleared | After successful login, screen shows logged‑in profile, no OTP fields. |
| 8 | Rate‑limit headers are present in response (informational) | X-RateLimit-Limit, X-RateLimit-Remaining returned; values sane | Header fields: limit=5, remaining=4`. |
Automation tip – a minimal Python request flow:
import requests, time
BASE = "https://api.example.com"
phone = "+15551234567"
purpose = "login"
# 1. request OTP
r = requests.post(f"{BASE}/api/v1/otp/request",
json={"phone": phone, "purpose": purpose})
assert r.status_code == 200
session = r.json()["session_id"]
# 2. simulate receipt (in test env we can fetch via admin endpoint)
code = requests.get(f"{BASE}/admin/otp/{session}").json()["code"]
# 3. verify
v = requests.post(f"{BASE}/api/v1/otp/verify",
json={"session_id": session, "code": code})
assert v.status_code == 200
assert "token" in v.json()
Running this script against a staging endpoint yields a PASS/FAIL for items 1‑6, 8; items 7 and the UI‑specific checks require a browser or mobile driver.
Otp Verification Testing Checklist (2026) – Error Handling and Validation
Error handling ensures the system degrades gracefully when users supply malformed data, when delivery fails, or when the OTP expires. Each item must produce a clear, user‑friendly message and leave the application in a recoverable state.
| # | Test Item | Pass Criteria | Example |
|---|---|---|---|
| 9 | Invalid phone number format (missing country code, letters) | Inline validation shows “Enter a valid international number”; request button stays disabled | Input "abcdef" → error toast. |
| 10 | Backend returns 400 on request (e.g., blacklisted number) | UI displays “Unable to send code. Try another number.”; no infinite spinner | Mock 400 → toast appears after 2 s. |
| 11 | Delivery channel failure (SMS gateway timeout) | System shows “We couldn’t send the code. Check your network or try email.” after ≤ 15 s timeout | Simulated gateway latency 20 s → fallback message. |
| 12 | User requests a new code before the previous one expires | New request allowed; previous code invalidated; UI shows “Code resent.” | Resend after 30 s → old code rejected. |
| 13 | OTP entry field accepts non‑numeric characters (should reject) | Field rejects input; shows “Only numbers allowed”; cursor stays | Typing "12a4" → field stays "12". |
| 14 | OTP length too short (e.g., 3 digits when 6 required) | Submit remains disabled; helper text “Enter 6‑digit code”. | After 3 digits, button grey. |
| 15 | OTP length too long (more than allowed) | Extra characters blocked or truncated; no crash | Typing 7 digits → only first 6 kept. |
| 16 | Submitting wrong OTP (incorrect code) | API returns 401/422; UI shows “Incorrect code. Try again.”; retry counter increments | After 3 fails, “Too many attempts – wait 2 min”. |
| 17 | Submitting OTP after expiry (server‑side TTL) | API returns 410 or 400 with “Code expired”; UI shows “Code has expired. Request a new one.” | Code requested at T‑0, submitted at T+70 s (TTL=60 s) → expiry msg. |
| 18 | Repeated rapid requests (rate‑limit hit) | Backend returns 429; UI shows “Too many requests. Wait X seconds.”; respects Retry-After header | 5 requests in 5 s → 429 with Retry-After:30. |
| 19 | Network loss during verification | UI shows offline banner; verification request is retried automatically with exponential backoff; user can cancel | Airplane mode → banner, retry after 5,10,20 s. |
| 20 | Server returns 500 on verify | UI shows generic “Something went wrong. Please try again later.”; logs capture stack trace for devs | Mock 500 → toast + error log entry. |
Pass criteria nuance – For items 9‑11, the UI must not expose raw HTTP status codes or stack traces. For items 16‑18, the system must enforce a monotonic increase in lockout time (e.g., 30 s → 2 min → 15 min) to deter brute force.
Automation example – simulating rate limit with locust or a simple loop:
import requests, time
BASE = "https://api.example.com"
session = "fixed-session-for-test"
for i in range(8):
r = requests.post(f"{BASE}/api/v1/otp/verify",
json={"session_id": session, "code": "000000"})
print(i+1, r.status_code, r.headers.get("Retry-After"))
if r.status_code == 429:
wait = int(r.headers.get("Retry-After", "5"))
time.sleep(wait)
break
time.sleep(0.5)
The loop should observe a 429 after the fifth attempt and then respect the Retry-After header.
Otp Verification Testing Checklist (2026) – Edge and Boundary Cases
Edge cases uncover assumptions about data types, time zones, leap seconds, and unusual user behavior. Treat each as a separate test scenario; document any deviation from the spec.
| # | Test Item | Pass Criteria | Example |
|---|---|---|---|
| 21 | OTP request with international number containing spaces or hyphens | Backend normalizes (strips spaces/hyphens) and processes correctly | Input "+1 (555) 123-4567" → treated as "+15551234567". |
| 22 | OTP request with maximum allowed phone length (e.g., 15 digits) | Accepted; no truncation error | "+999999999999999" processed. |
| 23 | OTP request with minimum allowed length (e.g., 3 digits after '+') | Rejected with validation error | "+12" → error. |
| 24 | OTP entry field respects device timezone mismatch (user sets device clock off by ± 2 h) | Server validates based on UTC timestamp embedded in request; rejects if drift > allowed skew (usually ± 5 min) | Device clock +2 h → request timestamp off → 400. |
| 25 | OTP request during leap second insertion (UTC 23:59:60) | System uses monotonic or UTC‑based TTL; no crash or incorrect expiry | Simulated leap second → TTL still 60 s from request. |
| 26 | Very large OTP value (e.g., 12‑digit code) when backend expects 6 | Input truncated or rejected; UI shows “Code must be 6 digits”. | Typing 123456789012 → field shows 123456 (or error). |
| 27 | Leading zeros in OTP (e.g., "001234") | Preserved and validated correctly; not stripped by numeric conversion | Backend receives string "001234". |
| 28 | Pasting OTP from clipboard that includes whitespace | UI trims whitespace before validation; accepts if core digits match length | Clipboard " 483921 " → accepted. |
| 29 | OTP request while another OTP flow is active for same user (different purpose) | Allowed if purposes are distinct; each session isolated | Login OTP and transaction OTP coexist. |
| 30 | OTP request after user logs out mid‑flow | Server invalidates existing session_id; new request creates fresh session | Logout after request → verify with old session_id returns 401. |
| 31 | OTP delivery via email with HTML email client that strips | Plain‑text fallback present; code still readable | Email contains both plain and HTML; user sees code. |
| 32 | OTP delivery via push notification on device with notification channel silenced | App receives silent push; UI shows in‑app badge or toast to inform user | Silent push → app displays “New code available”. |
| 33 | OTP request with VPN causing IP geolocation mismatch (fraud check) | If fraud scoring blocks request, UI shows “Request blocked due to suspicious location.”; offers alternative verification | VPN to blocked country → 403 with message. |
| 34 | OTP request when user has disabled notifications (OS level) | App falls back to in‑app polling or provides manual “Resend” option; no silent failure | Notification disabled → banner “Enable notifications for faster codes”. |
| 35 | OTP request with device in battery‑saver mode that delays background fetch | OTP still delivered via foreground channel (SMS) or user prompted to open app; timeout extended accordingly | Battery saver → SMS still arrives; app wait time increased to 20 s. |
Pass criteria notes – Items 24‑25 require server‑side clock skew tolerance configuration; document the allowed skew (e.g., ± 3 min) and test both sides. Items 31‑32 verify that delivery channels have graceful fall‑backs. Items 33‑35 test real‑world constraints that often escape unit tests.
Automation snippet – testing clock skew with pytest and freezegun:
from freezegun import freeze_time
import requests, time
BASE = "https://api.example.com"
def test_otp_clock_skew():
# request at real time t0
r = requests.post(f"{BASE}/api/v1/otp/request",
json={"phone":"+15551234567","purpose":"login"})
session = r.json()["session_id"]
# freeze time 6 minutes later (outside 5‑min skew)
with freeze_time(time.time() + 360):
v = requests.post(f"{BASE}/api/v1/otp/verify",
json={"session_id":session,"code":"123456"})
assert v.status_code == 400
assert "expired" in v.json()["error"].lower()
Otp Verification Testing Checklist (2026) – Accessibility Checks (WCAG)
Accessibility ensures that users relying on screen readers, keyboard navigation, or high‑contrast modes can complete OTP verification. Map each WCAG success criterion to a concrete UI check.
| # | WCAG Ref | Test Item | Pass Criteria | Example |
|---|---|---|---|---|
| 36 | 1.3.1 Info and Relationships | Label associated with OTP input via or ARIA-label | Screen reader announces “Enter the 6‑digit code you received”. | Label present; reading order correct. |
| 37 | 1.4.3 Contrast (Minimum) | Text and background contrast ratio ≥ 4.5:1 for normal text | OTP field placeholder and error text meet ratio. | Dark gray placeholder on white passes. |
| 38 | 2.1.1 Keyboard | All interactive elements (Send Code, Resend, Verify) reachable via Tab | No mouse‑only gestures; visible focus indicator. | Tab order: phone → send → otp input → verify. |
| 39 | 2.1.2 No Keyboard Trap | Focus can move away from OTP modal using Esc or Tab | Closing modal with Esc returns focus to triggering button. | Press Esc → focus returns to login button. |
| 40 | 2.2.1 Timing Adjustable | User can extend OTP expiry via a visible “Need more time?” control (if timed) | Control extends TTL by at least 30 s; announced to SR. | Button appears after 40 s; extends to 90 s. |
| 41 | 2.2.2 Pause, Stop, Hide | Any auto‑advancing carousel or countdown can be paused | Countdown pause button works; SR announces state. | Pause button stops 10‑s resend timer. |
| 42 | 2.4.1 Bypass Blocks | Skip link to main content after OTP screen (if modal) | “Skip to dashboard” link present and functional. | Skip link jumps to main page. |
| 43 | 2.4.7 Focus Visible | Custom OTP input shows clear focus outline (≥ 2 px solid) | No reliance on default browser outline removed without replacement. | Custom input shows blue 2 px outline on focus. |
| 44 | 3.2.1 On Focus | Changing OTP input does not trigger unexpected context change | No auto‑submit on focus; only on explicit Verify action. | Typing does not submit; button needed. |
| 45 | 3.2.2 On Input | Entering a valid OTP does not change context until verification | No navigation or modal change before submit. | UI stays on same screen until verify pressed. |
| 46 | 3.3.1 Error Identification | Error messages are associated with the input via aria-describedby | SR reads error when input invalid. | Error text linked via ID. |
| 47 | 3.3.2 Labels or Instructions | Instructions (e.g., “Code expires in 60 s”) provided near field | Text visible and readable by SR. | Helper text below field. |
| 48 | 4.1.2 Name, Role, Value | Custom OTP component exposes correct role (textbox) and value | SR reports role “edit text”, value updates as typed. | Inspect accessibility tree. |
Pass criteria details – For items 36‑38, use automated tools like axe-core or pa11y in CI; they will flag missing labels or contrast failures. Items 40‑41 require manual verification of timing controls, but you can also assert that a “extend time” button exists and that clicking it changes a backend TTL value (exposed via a test endpoint).
Automation example – using Playwright to check label association:
from playwright.sync_api import expect, sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://app.example.com/login")
# fill phone, click send
page.fill('input[name="phone"]', "+15551234567")
page.click('button:has-text("Send Code")')
# locate OTP input and verify label
otp_input = page.locator('input[name="otp"]')
expect(otp_input).to_have_attribute("aria-label", re.compile("6.*digit", re.IGNORECASE))
# check contrast via axe
page.add_script_tag(url="https://unpkg.com/axe-core@4.7.2/axe.min.js")
results = page.evaluate("""() => axe.run()""")
assert all(v["impact"] != "critical" for v in results["violations"])
browser.close()
Otp Verification Testing Checklist (2026) – Security and Privacy Considerations
Security testing validates that the OTP mechanism resists replay, leakage, and inference attacks. Privacy checks confirm that personal data (phone number, email) is not unnecessarily logged or exposed.
| # | Test Item | Pass Criteria | Example |
|---|---|---|---|
| 49 | OTP entropy ≥ 6 bits per digit (i.e., uniformly random 0‑9) | Statistical test over ≥ 10 000 generated codes shows chi‑square p > 0.05 | Collect codes from test endpoint; run scipy.stats.chisquare. |
| 50 | OTP never returned in plaintext inbound request logs or error responses | Search logs for code substring; none found | After request, grep logs for “123456” → zero hits. |
| 51 | OTP binding to session/request identifier (nonce) | Verification fails if session_id tampered or reused with different code | Change one char in session_id → 401. |
| 52 | Rate limiting per identifier (phone, IP, device ID) | After N failures, further attempts blocked; block scoped to identifier | 5 failed attempts → 429 for same phone; other phone still allowed. |
| 53 | OTP valid only for intended purpose (login vs. transaction) | Using login OTP for transaction endpoint returns 400/403 | Send login code to /txn/verify → error. |
| 54 | No OTP leakage via URL (GET parameters, Referer) | All OTP‑related endpoints use POST; OTP never appears in query string | Inspect network tab; no ?otp=. |
| 55 | OTP not cached by browser or intermediate proxies | Response headers include Cache-Control: no-store, private | Verify headers. |
| 56 | OTP not exposed in client‑side JavaScript variables accessible via console | Search bundled JS for hard‑coded strings; none | grep -r "otp" dist/ yields no matches. |
| 57 | OTP delivery channel uses TLS 1.2+ and certificate pinning where applicable | Network trace shows TLS version ≥ 1.2; pinning matches known hash | Use Wireshark or openssl s_client -connect api.example.com:443 -tls1_2. |
| 58 | OTP invalidated immediately after successful verification | Subsequent verification with same code returns 410/400 | Verify twice → second fails. |
| 59 | OTP invalidated after expiration even if not used | Attempt after TTL returns 410 | Wait 70 s, try → fail. |
| 60 | OTP not reusable across devices (device‑binding optional) | If device‑binding enabled, code from device A fails on device B | Simulate request from two user‑agents; second fails. |
| 61 | Personal data (phone/email) masked in UI after entry (e.g., shows only last 4 digits) | UI displays “* 4567” after user moves focus away | After blur, input shows masked version. |
| 62 | No storage of OTP in localStorage, sessionStorage, or cookies | Inspect DevTools Application tab; no key contains OTP | localStorage.getItem("otp") → null. |
| 63 | Audit log records OTP request and verification with pseudonymized ID | Logs contain hashed phone or UUID, not raw number | Log entry: user_id_hash: a3f9…. |
| 64 | Resistance to SIM‑swap: service offers fallback to email or authenticator app | When SMS delivery fails, alternative channel offered within UI | After SMS failure, banner “Try email instead”. |
| 65 | Protection against brute force via exponential backoff or CAPTCHA after threshold | After 10 failed attempts, CAPTCHA presented or delay ≥ 2 min | Simulate 10 fails → see CAPTCHA widget. |
| 66 | Secure deletion of OTP from server memory after use (if applicable) | Memory dump post‑verification shows no plaintext OTP | Requires privileged env; use gcore and strings. |
Pass criteria notes – Items 49‑51 can be verified with a script that calls a test‑only endpoint returning many OTPs and runs entropy tests. Items 52‑55 are typically enforced by API gateway; you can confirm by probing with curl and observing status codes and headers. Items 56‑62 are client‑side checks; use static analysis tools (e.g., npm audit, bandit) and manual inspection of bundles. Items 63‑66 often require backend logs or privileged access; in a staging environment you can enable debug logging and verify the output.
Automation snippet – checking entropy with Python:
import requests, collections, math, scipy.stats as stats
BASE = "https://api.example.com"
samples = [requests.get(f"{BASE}/test/otp").json()["code"] for _ in range(10000)]
# flatten digits
digits = [int(d) for code in samples for d in code]
cnt = collections.Counter(digits)
# chi‑square vs uniform
chi2, p = stats.chisquare([cnt.get(i,0) for i in range(10)])
assert p > 0.05, f"Non‑uniform OTP digits: p={p}"
Otp Verification Testing Checklist (2026) – Performance and Load Testing
Performance testing ensures that OTP request/verification pipelines stay responsive under expected load and that they do not become a bottleneck for authentication flows.
| # | Test Item | Pass Criteria | Example |
|---|---|---|---|
| 67 | Average latency for OTP request (95th percentile) ≤ 800 ms on 3G‑simulated network | Use network throttling (e.g., tc or Chrome DevTools) to simulate 3G; measure 95th‑pct latency | wrk or k6 shows 95th pct = 620 ms. |
| 68 | Average latency for OTP verification ≤ 500 ms under same conditions | Same as above, but for verify endpoint | 95th pct = 410 ms. |
| 69 | System sustains 100 requests/second (RPS) with < 2 % error rate | Load generator ramps to 100 RPS for 5 min; track HTTP 5xx/429 | locust reports 1.3 % errors (mostly 429 due to rate limit). |
| 70 | Graceful degradation when backend OTP service is degraded (e.g., 50 % latency increase) | UI shows informative message (“Sending code may take a moment”) but does not block UI | Inject latency via toxiproxy; observe spinner + message. |
| 71 | No memory leak in client OTP handling after repeated cycles | Run 10 000 request/verify cycles; monitor JS heap size; growth < 5 MB | Chrome DevTools → Memory → heap snapshot diff. |
| 72 | Battery impact minimal on mobile (< 5 % extra drain per 100 OTP cycles) | Use Android Battery Historian or Xcode Energy Log; compare baseline | Baseline 2 % → after test 6 % (acceptable). |
| 73 | Concurrent OTP requests for different users do not cause contention on shared resources (e.g., rate‑limit counters) | Each user’s limit independent; no cross‑talk | Simulate 50 users; each hits limit after its own quota. |
| 74 | OTP service scales horizontally – adding instances reduces latency linearly | Deploy 2× replicas; re‑run latency test; observe ≤ ‑ 30 % latency | Latency drops from 720 ms → 480 ms. |
| 75 | Fallback to cached OTP (if allowed) does not violate replay protection | Cached OTP rejected if used after first verification | Attempt reuse → 410. |
| 76 | Alert on abnormal latency spikes (> 2 × baseline) in production monitoring | Set up alert rule in Prometheus/Grafana; test by injecting delay; verify notification | Inject 2 s delay → alert fires within 30 s. |
Pass criteria notes – Items 67‑68 are best measured with synthetic traffic tools (k6, wrk, locust) that can also emulate network conditions via tc netem or browser throttling. Items 69‑71 require observing system metrics (CPU, memory, error rates) from monitoring dashboards. Items 72‑74 need device‑level profiling; you can use Android Studio Profiler or Instruments on iOS. Items 75‑76 are operational; ensure your observability stack captures the relevant metrics.
Automation snippet – using k6 to test request latency under throttling:
import http from 'k6/http';
import { sleep, check } from 'k6';
import { Counter, Trend } from 'k6/metrics';
const errors = new Counter('errors');
const lat = new Trend('latency');
export let options = {
stages: [
{ duration: '2m', target: 50 }, // ramp up
{ duration: '3m', target: 50 }, // stay
{ duration: '2m', target: 0 }, // ramp down
],
thresholds: {
'latency': ['p(95)<800'], // 95th < 800ms
'errors': ['rate<0.02'], // <2% errors
},
};
export default function () {
const res = http.post(
'https://api.example.com/api/v1/otp/request',
JSON.stringify({ phone: '+15551234567', purpose: 'login' }),
{ headers: { 'Content-Type': 'application/json' } }
);
const ok = check(res, {
'status is 200': (r) => r.status === 200,
'json has session': (r) => r.json().hasOwnProperty('session_id'),
});
if (!ok) errors.add(1);
lat.add(res.timings.duration);
sleep(0.5);
}
Run with K6_NETWORK=slow_3g k6 run script.js to emulate 3G.
Otp Verification Testing Checklist (2026) – Release Readiness and Regression
Before a release, verify that the OTP flow remains stable, that documentation matches implementation, and that rollback procedures are documented.
| # | Test Item | Pass Criteria | Example |
|---|---|---|---|
| 77 | All checklist items from sections 1‑6 marked PASS in the latest build | Zero FAIL items; any N/A justified and reviewed |
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