How to Test OTP Verification on Web (Complete Guide)
One‑time password (OTP) flows are the gatekeepers for account recovery, multi‑factor authentication, and transaction confirmation on the web. A single flaw—whether it lets an attacker bypass the code,
Why OTP Verification Demands Rigorous Testing
One‑time password (OTP) flows are the gatekeepers for account recovery, multi‑factor authentication, and transaction confirmation on the web. A single flaw—whether it lets an attacker bypass the code, traps a legitimate user in an endless resend loop, or leaks the secret in a network trace—can undermine the security guarantees of the whole application and expose users to fraud or account takeover. Because OTP verification touches UI, backend services, third‑party gateways, and often involves asynchronous timing, bugs that survive unit tests frequently surface only under real‑world load, specific browser quirks, or when a user behaves in an unexpected way. A disciplined testing strategy that combines manual exploration, automated scripts, and persona‑driven autonomous analysis is therefore essential to ship reliable OTP experiences.
Core Concepts: How OTP Works on the Web
Understanding the moving parts helps you design tests that hit the right seams.
Typical Flow
- Trigger – User clicks “Send code” or submits a form that requires OTP.
- Request – Frontend calls an API endpoint (e.g.,
POST /auth/send-otp) with an identifier (email, phone, user‑id). - Generation – Backend creates a cryptographically random numeric or alphanumeric token, stores it (often hashed) with an expiry timestamp, and associates it with the identifier.
- Delivery – The token is dispatched via SMS, email, push notification, or a third‑party authenticator API.
- Presentation – User receives the code and enters it into a verification field.
- Validation – Frontend posts the code to
/auth/verify-otp; backend compares the supplied value (after hashing) with the stored secret, checks expiry, and either marks the identifier as verified or returns an error. - Post‑verification – On success, the user is redirected, a session token is issued, or a protected action is allowed.
Key Attributes to Test
| Attribute | What to Verify | Typical Failure Modes |
|---|---|---|
| Entropy & Length | Token must be unpredictable and of configured length (usually 4‑8 digits). | Predictable patterns, reuse of old tokens, insufficient entropy. |
| Expiry | Token becomes invalid after a defined window (e.g., 5 minutes). | No expiry check, expiry too short/long, clock‑skew issues. |
| Rate Limiting | Only N send attempts per identifier per time window. | Missing limits enable SMS/email flooding; overly strict limits lock out legitimate users. |
| Resend Logic | Resend button works, respects cooldown, does not reset expiry incorrectly. | Resend sends a new token but keeps old expiry, or allows immediate resend. |
| Input Handling | Leading/trailing whitespace, non‑numeric chars, paste, autocomplete are handled. | Trimming bugs, rejection of valid paste, acceptance of malformed input. |
| Error Messaging | Clear, user‑friendly messages for wrong code, expired code, too many attempts. | Generic “error”, leakage of whether a code exists (user enumeration). |
| Security | Token never appears in URLs, logs, or client‑side storage; transmission over HTTPS only. | Token in query string, debug logs, localStorage, or HTTP. |
| Accessibility | Fields labeled correctly, announce errors, support keyboard navigation, sufficient contrast. | Missing aria-label, reliance on color alone, focus traps. |
| Internationalization | Works with locale‑specific phone formats, language‑specific error messages, RTL layouts. | Hard‑coded US‑centric regex, missing translation keys. |
Test Matrix for OTP Verification
A systematic matrix ensures you cover happy paths, error paths, edge cases, and non‑functional aspects. Below is a comprehensive table you can adapt to your own feature flags or delivery channels.
Table 1: Functional Test Matrix
| ID | Category | Sub‑case | Description | Expected Result | Notes for Automation |
|---|---|---|---|---|---|
| F1 | Happy Path | Valid code entry | User receives correct OTP, enters it within expiry. | Verification succeeds, user proceeds. | Use a mock OTP service that returns a known token. |
| F2 | Happy Path | Code entry after resend | User clicks “Resend”, receives new OTP, enters it. | New OTP validates, old OTP rejected. | Track token IDs to ensure old token is invalidated. |
| F3 | Input Validation | Leading/trailing spaces | User pastes “ 123456 ” (spaces). | Spaces trimmed, code accepted. | Send raw string with spaces via type() or fill(). |
| F4 | Input Validation | Non‑numeric characters | User types “12a456”. | Field rejects or strips non‑digits, shows error. | Verify UI feedback and API payload. |
| F5 | Input Validation | Empty submit | User clicks Verify with empty field. | Inline error: “Code required”. | Check that no network request is sent. |
| F6 | Rate Limiting – Send | Exceed max sends | User hits “Send code” >5 times in 30 s. | Further sends blocked, UI shows cooldown. | Mock backend to return 429 after threshold. |
| F7 | Rate Limiting – Verify | Exceed max verifies | User attempts wrong code >5 times. | Account temporarily locked or CAPTCHA shown. | Simulate failures and assert lockout state. |
| F8 | Expiry | Code used after expiry | Wait 6 min after receipt, then submit. | Error: “Code expired”. | Use clock.mock or backend time‑shift. |
| F9 | Expiry | Code used before expiry | Submit within 30 s. | Success. | Standard happy path. |
| F10 | Resend Timing | Immediate resend blocked | Click Resend before cooldown (e.g., 10 s). | Button disabled, tooltip shows wait time. | Verify UI state and API call suppression. |
| F11 | Resend Token Change | New token differs | After resend, entered old token fails. | Old token rejected, new token accepted. | Compare token hashes in backend logs. |
| F12 | Delivery Failure | SMS gateway down | Backend simulates provider error. | UI shows “Unable to send code, try again”. | Mock HTTP 503 from gateway. |
| F13 | Delivery Failure | Email bounce | Invalid email address supplied. | Validation error before sending. | Ensure client‑side email regex runs. |
| F14 | Security – Token Leakage | Token in URL | User manually edits URL to include ?code=123456. | Server ignores or rejects token from query string. | Attempt GET with token in query, assert 400/401. |
| F15 | Security – Token in Logs | Backend logs OTP | Debug level logs capture token. | No token appears in production logs. | Scan log output for token pattern. |
| F16 | Accessibility – Label | Missing label | Inspect DOM for without associated or aria-label. | Fix: add proper label. | Use axe-core or manual inspection. |
| F17 | Accessibility – Error Announcement | Screen reader does not read error | Submit invalid code, listen with NVDA/Jaws. | Error message announced. | Test with screen‑reader emulator. |
| F18 | Internationalization – Phone Format | Non‑US number | Input +44 7911 123456. | Accepted, OTP sent via appropriate gateway. | Ensure backend normalizes E.164. |
| F19 | Internationalization – RTL Layout | Language switched to Arabic | UI mirrors, input field aligns right. | Layout correct, no overlap. | Test with lang="ar" and dir=rtl. |
| F20 | Cross‑Browser | Safari autofill | Safari suggests OTP from messages. | Autofill works, code fills correctly. | Test on real device or BrowserStack. |
| F21 | Cross‑Browser | Firefox password manager interference | Manager offers to save OTP as password. | No interference, OTP field not saved. | Verify autocomplete="one-time-code" attribute. |
| F22 | Edge Case – Rapid Taps | User double‑taps Send | Two rapid requests. | Only one request processed, second ignored or queued. | Spy on network calls. |
| F23 | Edge Case – Navigation Away | User leaves page after sending, returns later. | OTP still valid if within expiry. | Verification works on return. | Use page.goBack() then forward. |
| F24 | Edge Case – Tab Duplication | User opens same flow in two tabs. | Each tab has independent OTP state. | Sending in one tab does not affect the other. | Verify isolation via separate storage keys. |
| F25 | Edge Case – Offline | User loses network after receiving OTP. | Can still submit when back online. | Submission succeeds if within expiry. | Simulate offline with page.context().setOffline(true). |
Table 2: Non‑Functional & Persona‑Driven Test Matrix
| Persona | Goal | Specific Checks | Tools / Techniques |
|---|---|---|---|
| Curious Explorer | Try every UI element, including hidden or debug links. | Verify no hidden OTP bypass, no exposed API keys in dev tools. | Manual exploratory + DOM search. |
| Impatient User | Spam Send/Resend, attempt rapid verification. | Rate limiting, UI feedback, no crash under rapid fire. | Automated burst scripts (e.g., for i in {1..20}; do curl …; done). |
| Novice User | Relies on placeholders, expects clear instructions. | Placeholder text, tooltip, error messages in plain language. | Usability testing, readability scores. |
| Adversarial Tester | Attempts to brute‑force, replay, or leak tokens. | Token entropy, replay protection, lockout, no timing side‑channels. | Burp Suite Intruder, OWASP ZAP, custom scripts. |
| Elderly User | May have reduced vision, motor control. | Large touch targets, high contrast, support for zoom, no time‑pressured auto‑advance. | Accessibility audits (axe, WCAG contrast checker). |
| Accessibility User | Uses screen reader, keyboard only. | All controls reachable via Tab, ARIA labels, live regions for errors. | Keyboard navigation + screen‑reader (NVDA, VoiceOver). |
| Power User | Wants to paste from password manager, use autocomplete. | autocomplete="one-time-code" works, paste accepted, no extra steps. | Test with Bitwarden, LastPass, native OS autofill. |
| Security‑Focused Auditor | Checks for leakage, insecure storage, improper HTTP verbs. | No token in URL, localStorage, cookies; HTTPS only; proper SameSite flags. | Network inspection, static analysis, CSP review. |
Manual Testing Approach: Step‑by‑Step
A disciplined manual session catches nuances that scripts may overlook, especially around UX, timing, and environmental factors.
- Prepare a Clean Test Environment
- Use an incognito/private window to avoid cached tokens or autofill.
- Disable extensions that might interfere (e.g., password managers) unless you are testing their interaction.
- Set device timezone to match the server’s expected timezone to avoid expiry confusion due to clock skew.
- Trigger the Flow
- Identify the entry point (login page, checkout, account recovery).
- Fill any prerequisite fields (email, phone) with a valid format for your test harness.
- Click the “Send code” button and observe the network request in DevTools → Network. Verify:
- Request method is POST (or GET if poorly designed).
- Payload contains only the identifier, no secret.
- Response includes a token ID or reference (never the raw OTP).
- Intercept the OTP Delivery
- If using email, log into a test inbox (Mailinator, Ethereal, or a dedicated test mailbox).
- If using SMS, employ a SIM‑box service or a mock gateway like Twilio’s test credentials.
- Record the exact OTP value and timestamp.
- Validate Input Handling
- Paste the OTP with leading/trailing spaces; watch if the field trims automatically or shows an error.
- Try pasting a non‑numeric string; ensure the field either rejects or strips invalid characters.
- Use the keyboard to enter the code digit by digit, then try using the mouse to right‑click → Paste.
- Test Expiry and Resend
- Wait for the configured expiry (e.g., 5 minutes). Attempt verification → expect expiration error.
- Before expiry, click “Resend”. Confirm a new OTP arrives and the old one is rejected.
- Attempt to resend before the cooldown period; ensure the button is disabled and a tooltip explains the wait.
- Attempt Error Paths
- Enter an obviously wrong code (e.g., all zeros). Verify the error message is user‑friendly and does not hint whether the code exists (avoid user enumeration).
- Exceed the maximum allowed verification attempts; confirm lockout or CAPTCHA appears as designed.
- Simulate network loss after receiving OTP, then restore connectivity and submit; the code should still be valid if within expiry.
- Check Security Properties
- Look at the URL bar after submitting the code; ensure no query parameter contains the OTP.
- Open DevTools → Application → Local Storage / Session Storage; confirm no OTP stored.
- Filter network logs for any request that includes the OTP in headers, body, or query string.
- If the app uses websockets, inspect those frames as well.
- Accessibility Spot‑Check
- Turn on a screen reader (NVDA on Windows, VoiceOver on macOS). Tab to the OTP field; verify it announces its purpose (“One‑time code, required”).
- Submit an invalid code; listen for the error announcement.
- Use the Color Contrast Analyzer to ensure the input field and error text meet WCAG AA (≥4.5:1).
- Cross‑Browser / Device Checks
- Repeat steps 2‑8 on Chrome, Firefox, Safari, and Edge (desktop).
- On mobile browsers (Chrome Android, Safari iOS), test autofill from the OS message notification.
- If the site offers a responsive layout, verify that touch targets are at least 44 × 44 dp.
- Document Findings
- Use a lightweight template: ID, Description, Steps, Expected, Actual, Severity, Notes.
- Attach screenshots or screen recordings for visual bugs (misaligned labels, overflow).
Automated Testing Strategies
Automation provides repeatability and regression safety. Below are patterns that work well for web OTP verification, grouped by layer.
Unit / Service Layer
- Mock the OTP generation and storage functions.
- Test that the token is cryptographically random (use a statistical test on a large sample if you want to be thorough).
- Verify expiry logic with a mocked clock (e.g.,
sinon.useFakeTimers()or Jest’sjest.setSystemTime).
// Example: Jest test for expiry
jest.useFakeTimers();
const { generateOTP, verifyOTP } = require('./otpService');
test('OTP expires after 5 minutes', () => {
const { token, expiresAt } = generateOTP('user@example.com');
expect(verifyOTP('user@example.com', token)).toBe(true);
jest.advanceTimersByTime(5 * 60 * 1000 + 1000); // just past expiry
expect(verifyOTP('user@example.com', token)).toBe(false);
});
API Contract Tests
- Use tools like Pact or Dredd to ensure the
/auth/send-otpand/auth/verify-otpendpoints conform to the expected schema, status codes, and error messages. - Include negative cases (invalid identifier, missing payload, rate‑limit responses).
UI‑Level Automation
Choose a framework that can handle asynchronous waits and iframes (common for OTP widgets). Playwright and Cypress are popular; the snippets below illustrate each.
#### Playwright (TypeScript)
import { test, expect } from '@playwright/test';
test.describe('OTP verification flow', () => {
test('happy path with mocked email OTP', async ({ page }) => {
// 1. Navigate to login page that offers OTP
await page.goto('/login');
await page.fill('#email', 'test@example.com');
await page.click('button:has-text("Send code")');
// 2. Intercept the outbound email request (using a mock SMTP server)
const [email] = await Promise.all([
page.waitForEvent('response', resp =>
resp.url().includes('/api/send-email') && resp.request().method() === 'POST'
),
page.waitForTimeout(500) // give the mock time to capture
]);
const emailBody = await email.json();
const otpCode = emailBody.otp; // assume mock returns the OTP in JSON
// 3. Fill OTP and submit
await page.fill('#otp-input', otpCode);
await page.click('button:has-text("Verify")');
// 4. Assert success redirect or toast
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.locator('.success-toast')).toContainText('Verified');
});
test('expired OTP shows error', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', 'test@example.com');
await page.click('button:has-text("Send code")');
// Grab OTP from mock, then fast‑forward time
const [resp] = await Promise.all([
page.waitForResponse(r => r.url().includes('/api/send-email')),
page.waitForTimeout(200)
]);
const otp = (await resp.json()).otp;
// Simulate 6 minute delay
await page.evaluate(() => new Promise(r => setTimeout(r, 6 * 60 * 1000)));
await page.fill('#otp-input', otp);
await page.click('button:has-text("Verify")');
await expect(page.locator('.error-message')).toHaveText(
/code.*expired/i
);
});
});
#### Cypress (JavaScript)
describe('OTP verification', () => {
const mockOTP = '123456';
beforeEach(() => {
// Stub the outbound email/SMS call
cy.intercept('POST', '/api/send-otp', {
statusCode: 200,
body: { message: 'Sent', otp: mockOTP } // mock returns OTP for testability
}).as('sendOTP');
// Stub the verification endpoint
cy.intercept('POST', '/api/verify-otp', (req) => {
if (req.body.otp === mockOTP) {
req.reply({ statusCode: 200, body: { success: true } });
} else {
req.reply({ statusCode: 400, body: { error: 'Invalid code' } });
}
}).as('verifyOTP');
});
it('accepts correct OTP', () => {
cy.visit('/login');
cy.get('#email').type('user@test.com{enter}');
cy.wait('@sendOTP');
cy.get('#otp-input').type(mockOTP);
cy.get('button:contains("Verify")').click();
cy.wait('@verifyOTP');
cy.url().should('include', '/dashboard');
cy.get('.success').should('contain', 'Verified');
});
it('shows error for wrong OTP', () => {
cy.visit('/login');
cy.get('#email').type('user@test.com{enter}');
cy.wait('@sendOTP');
cy.get('#otp-input').type('654321');
cy.get('button:contains("Verify")').click();
cy.wait('@verifyOTP');
cy.get('.error-message').should('contain', 'Invalid');
});
});
Leveraging Test‑Specific OTP Services
- Email: Use services like Mailosaur, MailSlurp, or an internal fake SMTP that captures messages and exposes them via a REST API.
- SMS: Twilio’s test credentials, Nexmo’s sandbox, or an open‑source mock like
mock-sms-gateway. - Authenticator Apps: Libraries such as
speakeasyorotplibcan generate TOTP values that you can feed directly into the UI for testing without relying on external delivery.
Integrating with CI
- Store mock service URLs and API keys as encrypted secrets (GitHub Actions, GitLab CI).
- Run the UI test suite in a headless Chrome/Firefox container; ensure the container can reach the mocked OTP provider (allow outbound HTTP/HTTPS).
- Publish test results as JUnit XML; flaky tests (often due to timing) can be quarantined with
retryoptions.
Leveraging Autonomous, Persona‑Driven Exploration (SUSA)
While scripted tests verify known scenarios, autonomous exploration can surface issues that only appear when a user behaves in an unexpected manner or when the system state deviates from the happy path. SUSA (the autonomous QA platform) fits naturally explores an app by simulating a variety of user personas, each with a distinct behavior model, and learns from each run to avoid revisiting dead ends.
How SUSA Approaches OTP Flows
- Persona Profiles – The platform ships with built‑in personas:
- *Curious* clicks every link, even those hidden in footers or debug menus.
- *Impatient* repeatedly taps the Send button with minimal delay.
- *Novice* relies on placeholders and tooltips, often mis‑clicks.
- *Adversarial* attempts fuzzing on input fields, tries to replay captured OTPs, and manipulates network timing.
- *Elderly* uses zoom, prefers large touch targets, and may trigger accessibility features like high‑contrast mode.
- *Accessibility* navigates exclusively via keyboard and screen reader, listening for live‑region announcements.
- Exploration Loop – For each persona, SUSA:
- Loads the target URL (or APK‑converted web view).
- Executes a weighted action policy (e.g., Curious: 30 % exploration, 20 % form filling, 10 % aggressive tapping).
- Monitors network responses, DOM mutations, console errors, and accessibility events.
- When an OTP send request is observed, it captures the delivered code (via the mocked OTP service integrated into the test harness) and attempts verification with variations: wrong code, expired code, rapid resend, paste, autocomplete, etc.
- Learning & Deduplication – SUSA builds a graph of visited screens and edge cases. If a particular path (e.g., “Send → immediate Resend → verification”) has already been exercised with a given persona, subsequent runs skip it unless a new condition (like a different locale or a modified header) is introduced. This prevents exponential blow‑up while still achieving high coverage.
- Bug Reporting – When a deviation from expected behavior is detected (crash, ANR‑like long task, WCAG violation, security issue such as token leakage in a URL, or a flow that never reaches a terminal state), SUSA emits a detailed report containing:
- The persona that triggered it.
- A step‑by‑step trace (including screenshots and network payloads).
- The exact error or violation (e.g., “Input field lacks
aria-label”, “OTP appears in query string”, “Page blocked main thread for 2.3 s”).
Practical Benefits for OTP Testing
- Discovering Hidden Resend Loops – An Impatient persona may hammer the Send button faster than the backend’s rate‑limit window, exposing a bug where the cooldown timer resets incorrectly, allowing unlimited SMS/email bursts.
- Uncovering Token Leakage via Browser History – A Curious persona might manually edit the URL after receiving an OTP, revealing that the app mistakenly echoes the token in a redirect URL (
/verify?code=123456). - Validating Accessibility Announcements – The Accessibility persona, using a screen‑reader emulator, can confirm that error messages are emitted via
aria-liveand that the OTP field is properly labeled; missing live regions are flagged automatically. - Testing Locale‑Specific Edge Cases – By switching the browser’s
accept-languageheader and running the Novice persona, SUSA can catch problems where the OTP input expects only ASCII digits but the localized keyboard supplies full‑width numerals, causing validation failures. - Detecting Race Conditions – The Adversarial persona can send two verification requests in quick succession with slightly different timestamps, highlighting scenarios where the server accepts an expired token due to a flawed time‑window check.
Running SUSA Against a Web OTP Flow
# Install the agent (once)
pip install susatest-agent
# Point it at your staging site; provide a mock OTP service URL via env var
OTP_MOCK_URL=https://mock-otp.example.com susatest-agent \
--target https://staging.myapp.com/login \
--personas curious impatient adversarial accessibility \
--output ./susareport \
--max-depth 6 \
--timeout 300
The agent will explore the login/OTP flow, generate a report (susareport/summary.json), and highlight any OTP‑specific anomalies it encounters. You can then feed those findings back into your manual test checklist or automated test suite.
> Note: The SUSA mention is kept brief and instructional, focusing on how its autonomous, persona‑driven approach complements traditional testing rather than promoting a sales pitch.
Common Production‑Only Bugs and How to Catch Them
Some defects only manifest under real‑world traffic, specific browser quirks, or atypical user behavior. Below are frequent production‑only OTP issues and tactics to surface them early.
| Bug | Why It Appears Only in Prod | Detection Technique |
|---|---|---|
| Token leakage via Referer header | When the OTP verification page redirects to a third‑party analytics endpoint, the full URL (including query‑string OTP) may be sent as Referer. In dev, analytics may be disabled or mocked. | Enable network logging with header inspection; use a proxy (Burp, OWASP ZAP) to capture outbound requests and verify no OTP appears in Referer. |
| Browser autofill conflicts | Chrome’s “Save password” may mistakenly store the OTP as a password and later auto‑fill it into login fields, causing confusion. This depends on the exact autocomplete attribute and form hierarchy, which can differ between dev and prod builds due to minification or CMS injection. | Test with real Chrome profiles (not incognito) and observe the password manager’s behavior; use autocomplete="one-time-code" and autocomplete="new-password" on appropriate fields. |
| Race condition between token generation and delivery | Under high load, the backend may generate a token, store it, but the SMS gateway experiences a delay; the user attempts verification before the token persists, leading to false‑negative. | Simulate latency with a mock gateway that delays response by a configurable interval; run concurrent verification attempts using tools like k6 or artillery. |
| Cache poisoning of OTP endpoint | A misconfigured CDN may cache the /auth/send-otp response (including any embedded token) and serve it to subsequent users. Caching is often disabled in dev but enabled in prod via Varnish or Cloudflare. | Send a request with a unique identifier, then immediately repeat the request with a different identifier; verify the response bodies differ and contain no OTP from the first request. |
| Locale‑specific regex rejecting valid international numbers | The production validation may rely on a library that only runs when the server’s locale is set to en_US. In dev, the default locale may be different, masking the bug. | Deploy the app with various locale environment variables (LANG=fr_FR.UTF-8, LC_ALL=ja_JP.UTF-8) and run the OTP flow with international phone numbers. |
| Accessibility overlay interfering with OTP field | Some sites inject third‑party accessibility widgets that add tabindex=-1 or cover inputs with a div, making the field unreachable via keyboard. These widgets are often loaded asynchronously and may not appear in staging if the widget script is blocked by ad‑filters. | Run the flow with a screen‑reader and keyboard-only navigation; use the axe extension to detect any elements that obscure focusable items. |
| Session fixation after OTP verification | After successful OTP verification, the app may retain the pre‑verification session ID, allowing an attacker who forced a session ID to hijack the account. Dev tests often use fresh sessions per test, missing this nuance. | Capture the session cookie before sending OTP, after verification, and confirm it has changed (or that the server issued a new session ID with proper Secure; HttpOnly; SameSite=Strict flags). |
| Push notification OTP not displayed on locked screen | On Android/iOS web apps installed as PWAs, the push notification may be hidden when the device is locked, causing the user to think the OTP never arrived. This depends on the notification priority and platform settings, which are not exercised in desktop‑centric test suites. | Use a device farm or emulator; lock the screen after triggering the push; check whether the notification appears on the lock screen (Android: heads‑up, iOS: banner). |
Mitigation Checklist for Production‑Only Risks
- Headers Scrub: Ensure any redirect after OTP verification strips query parameters or uses
Referrer-Policy: no-referrer-when-downgrade. - Autofill Guard: Apply
autocomplete="one-time-code"on the OTP input andautocomplete="off"orautocomplete="new-password"on password fields; test with real browser profiles. - Idempotent Token Store: Make token creation an atomic operation (e.g., INSERT … ON CONFLICT DO NOTHING) and verify persistence before allowing verification.
- Cache Control: Set
Cache-Control: no-store, privateon OTP endpoints; verify with a curl-Irequest. - Locale‑Independent Validation: Use a robust phone‑number library (Google’s libphonenumber) that does not depend on server locale.
- Accessibility Overlay Review: Audit any third‑party widgets for
tabindexandz-index; run axe in production‑like builds. - Session Rotation: After OTP verification, invalidate the old session ID and issue a new one with secure flags; verify via cookie comparison.
- Push Notification Testing: Include a real device matrix in your CI (e.g., Firebase Test Lab
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