guide· 14 min read· by Pramod Dutta

How to Test 2FA and OTP Login Flows With AI

Learn how to test 2fa otp login flows with AI, variables, saved sessions, and honest human-in-the-loop handling for real codes.

When you need to test 2fa otp login flows, the hard part is rarely typing an email and password. The hard part is timing, delivery, one-time code handling, session state, and deciding which parts should be automated at all. BrowserBash lets you describe the login journey in plain English, drive a real Chrome or Chromium browser, and still keep the verdict structured enough for CI and AI agents.

Why test 2fa otp login flows is harder than a normal happy path

Two-factor login flows sit at the boundary between automation and security controls. A time-based authenticator code changes every few seconds. An SMS code may arrive late, get throttled, or be blocked in a test environment. An email OTP may land behind a provider API, a mailbox delay, or a spam filter. Some products also add risk checks, CAPTCHA, device approval, or step-up authentication only after a new browser session appears. A brittle script that assumes a code field is instantly available will fail for reasons that say little about the product.

A selector-first script usually assumes the page is already in the right state. That assumption is fragile for 2FA and OTP login. Real users wait, retry, scroll, scan labels, notice errors, and correct themselves. BrowserBash starts closer to that user model. You give it a plain-English objective, and an AI agent drives a real Chrome or Chromium browser step by step. It is not replacing every low-level test you already have. It gives SDETs and AI-agent builders a validation layer that can exercise a flow the way a person describes it.

BrowserBash is free and open source under Apache-2.0, created by The Testing Academy and founded by Pramod Dutta. Install it with npm install -g browserbash-cli, then run browserbash. The current version is 1.5.1. Its strongest fit is end-to-end validation where the page can change shape but the user intent stays stable.

How BrowserBash helps you test 2fa otp login flows

The best way to test this area is to separate what should be automated from what must remain human or provider-driven. BrowserBash can handle the browser journey: navigate to the login page, enter credentials, recognize the second-factor screen, pause when a real human code is needed, continue after the code is entered, and verify that the correct signed-in state appears. That makes it practical to run a real smoke check without pretending that every security boundary should be bypassed.

The important distinction is that BrowserBash is not a selector recorder. You do not write page objects. You describe the business outcome and let the agent inspect the live page. Under the hood, it can use local Chrome by default, or providers such as CDP, Browserbase, LambdaTest, and BrowserStack. Stagehand is the default engine, and the builtin engine is available for the Anthropic tool-use loop and required for LambdaTest or BrowserStack.

The model story matters for test privacy. BrowserBash is Ollama-first, which means it defaults to free local models with no API keys and nothing leaving your machine. If a local Ollama model is not available, it can auto-resolve to ANTHROPIC_API_KEY, then OPENAI_API_KEY, then OpenRouter. For hard flows, very small local models around 8B parameters and under can be flaky on long multi-step objectives. A mid-size local model such as a Qwen3 or Llama 3.3 70B-class model, or a capable hosted model, is a more realistic choice.

npm install -g browserbash-cli
browserbash run "Open https://staging.example.com/login, sign in as {{email}}, pause for the OTP code if needed, submit the code, and verify the dashboard is visible"

For deeper examples, the BrowserBash learning center and BrowserBash tutorials are useful places to connect the concepts to working CLI usage.

Write a plain-English objective for test 2fa otp login flows

For OTP flows, the objective should be honest about the source of the code. If the test environment exposes a safe test mailbox or API, use that. If the code must come from a human authenticator app, say that the run is interactive and use the human-in-the-loop pause. Do not hide a production authenticator seed in source control. Do not claim that a test fully validates SMS delivery unless your system actually observes the carrier path.

A good objective names the start URL, the data you expect to use, the visible signals that matter, and the final state. Avoid wording that says only "make sure it works." That gives an agent too much freedom and gives a human reviewer too little information. Say what must be true when the flow succeeds.

For example, you can write the objective as a sentence for a quick local check, then move it into a committed markdown test once the flow becomes part of your release gate. BrowserBash writes a human-readable Result.md after each run, so the result is inspectable by a developer, tester, or AI coding agent.

A practical objective has three parts. First, describe the setup: account, environment, fixture, or saved login. Second, describe the action in user language. Third, describe the assertion in terms a product owner would recognize. That keeps the test stable when a CSS class changes, when a component moves, or when a team swaps one implementation detail for another.

Use markdown tests and variables without leaking secrets

Variables keep 2FA tests readable without scattering sensitive values through logs. Use variables for username, password, environment URL, test mailbox, and OTP value when a test harness provides one. If a human must type the OTP, keep that test out of unattended CI and label it as an interactive release smoke check.

BrowserBash markdown tests are committable *_test.md files. They support @import composition and {{variables}} templating. Secret-marked variables are masked as ***** in every log line, which is the right default for credentials, temporary codes, API tokens, and customer-like fixture data.

In version 1.5.0, testmd v2 added version: 2 frontmatter. Steps execute one at a time against a single browser session. Two deterministic step types never touch a model: API steps for seeding data and Verify steps for checking UI state. Consecutive plain-English steps run as grouped agent blocks on the same page. v1 files without frontmatter behave as before. One caveat is important: testmd v2 currently drives the builtin engine, so it needs ANTHROPIC_API_KEY or an ANTHROPIC_BASE_URL compatible gateway. It does not yet run on Ollama or OpenRouter directly.

browserbash run-test site/tests/login_2fa_test.md --auth qa-user --agent
browserbash run-all site/tests --shard 2/4 --budget-usd 2.50

A v2 test can combine setup, intent, and deterministic assertions:

---
version: 2
auth: qa-user
---
GET https://staging.example.com/api/test-users/otp-ready?email={{email}} Expect status 200, store $.id as 'user_id'
Open https://staging.example.com/login and sign in with {{email}} and {{password}}
When the 2FA page appears, enter {{otp_code}} and continue
Verify URL contains "/dashboard"
Verify text "Dashboard" is visible

The --agent flag emits NDJSON, one JSON event per line, with exit codes designed for automation: 0 for passed, 1 for failed, 2 for error, infrastructure failure, or budget stop, and 3 for timeout. AI coding agents do not need to parse prose. They can read structured events and the final verdict.

Make verification deterministic wherever possible

The deterministic gate for a 2FA test should usually be the post-login state, not the act of receiving a code. Verify that the URL moved to the authenticated area, that the user menu or dashboard heading is visible, and that an unauthenticated login form is not the final state. If your application shows a recovery-code warning, device trust prompt, or profile completion dialog, make that explicit so the run does not pass on the wrong intermediate screen.

BrowserBash 1.5.0 introduced deterministic Verify assertions. Supported Verify steps compile to real Playwright checks rather than LLM judgment. That includes URL contains, title is or contains, visible text, a named button, link, or heading being visible, element counts, and stored value equality.

This is the difference between "the agent thinks the page looks right" and "the condition held in the browser." If a deterministic Verify step fails, the evidence is reported in run_end.assertions and in the assertion table in Result.md. If a Verify line falls outside the grammar, it can still run as agent-judged, but it is flagged with judged: true so you can separate deterministic checks from judgment-based checks.

For 2FA and OTP login, that split matters. Let the agent do the parts humans naturally do, such as recognizing a visible control or moving through a changing interface. Let deterministic assertions own the final gate wherever the condition can be expressed as URL, title, text, count, or stored value.

Handle authentication and session setup cleanly

Saved auth is useful after you have one focused test that covers login itself. Most end-to-end tests should not repeat 2FA on every run. Save a session after a known-good login, then reuse that profile for checkout, billing, admin, or reporting flows. Keep one separate 2FA test to validate the gate itself, and let the rest of the suite start from a trusted authenticated state.

Saved logins reduce noise in tests that should not spend half their time logging in. With BrowserBash 1.5.0, browserbash auth save <name> --url <login-url> opens a browser. You log in once, press Enter, and BrowserBash saves the Playwright storageState. Reuse it with --auth <name> on run, testmd, run-all, and monitor, or with auth: frontmatter in a test file.

A useful safety detail is that a profile whose saved origins do not cover the target start URL prints a warning instead of silently doing nothing. That helps when staging, preview, and production domains look similar but do not share browser storage.

Save the profile with browserbash auth save qa-user --url https://staging.example.com/login, then reuse it with browserbash run "Open the account settings page and verify the Security heading is visible" --auth qa-user --viewport 1280x720.

For teams adopting BrowserBash across more flows, the BrowserBash features, BrowserBash blog, and open-source GitHub repo give you a quick way to check what is local, what is optional cloud dashboard, and what is implemented in the open.

Run test 2fa otp login flows in CI and agent workflows

A fully unattended CI test for 2FA needs a controlled test-code source. That might be a test-only email inbox, an internal OTP API, or a seeded fixture in a non-production environment. If your only code source is a personal authenticator app or real SMS device, run the BrowserBash objective locally with an interactive pause instead of pretending CI can complete the flow safely.

The MCP server added in 1.5.0 makes BrowserBash usable from AI coding agents without wrapping the CLI yourself. browserbash mcp serves the CLI over the Model Context Protocol on stdio. You can add it to an MCP host with claude mcp add browserbash -- browserbash mcp, with the same idea applying to Cursor, Windsurf, Codex, and Zed. BrowserBash is also listed on the official MCP Registry as io.github.PramodDutta/browserbash.

The MCP tools are intentionally small: run_objective for one plain-English objective, run_test_file for a *_test.md file, and run_suite for a folder in parallel. Each returns structured verdict JSON with status, summary, final_state, assertions, cost_usd, and duration_ms. A failed test is a successful validation. The tool call succeeds, and the agent reads the verdict instead of guessing.

For CI, BrowserBash includes action.yml at the repo root. It installs the CLI, runs the suite, uploads JUnit, NDJSON, and result artifacts, supports shard: matrix jobs and budget-usd:, and posts a self-updating PR comment with the verdict table. The GitHub Action guide explains the setup details.

Monitor the flow without noisy alerts

2FA pages are good candidates for low-noise monitoring because they often break when identity providers, cookie settings, or redirects change. Use a synthetic account with a safe test-code mechanism. If the flow depends on a human typing a real code, monitor the pre-2FA screen and the saved-auth post-login screens separately, then run the full interactive journey before releases.

Monitor mode is useful when 2FA and OTP login has a history of breaking after deployments, provider changes, or design-system updates. browserbash monitor <test|objective> --every 10m --notify <webhook> runs on an interval and alerts only on pass to fail or fail to pass state changes. It does not page the team on every green run. Slack incoming-webhook URLs get Slack formatting automatically, while other URLs receive the raw JSON payload.

The replay cache also matters for monitoring cost. A green run records its actions. The next identical run replays them with zero model calls, and the agent steps back in only when the page changed. That makes an always-on monitor much more practical than a naive AI agent that spends tokens every ten minutes for the same unchanged screen.

Cost governance gives you another guardrail. run_end carries a cost_usd estimate from a bundled per-model price table. Unknown models get no estimate rather than a fake number. run-all --budget-usd 2.50 or --budget-tokens stops launching new tests after the suite crosses the budget. Remaining tests are reported as skipped, the suite exits 2, and spend lands in RunAll-Result.md and JUnit properties.

When to choose this approach, and when not to

Choose this approach when you need confidence that the real browser can reach the 2FA challenge, submit a legitimate test code, and land on the right authenticated screen. It is also useful when an AI coding agent changes login UI and needs a verdict from a real browser instead of a screenshot claim. Choose direct API tests for OTP generation rules, throttling windows, and token expiry math because those are faster and more precise below the UI.

Choose BrowserBash when the user journey matters more than implementation details. It is a strong fit when your team wants to express tests in product language, when AI coding agents need an independent browser verdict, or when selectors are expensive to maintain because the UI is still moving.

Keep lower-level tests where they are cheaper and more precise. A pure unit test is better for date math, permission predicates, parser behavior, or API schema validation. A hand-written Playwright test can still be the best tool when you need exact control of a browser primitive or a highly specialized assertion. BrowserBash is the validation layer on top of those checks, especially for flows that benefit from natural language intent and structured verdicts.

Do not treat any AI browser agent as magic. Be explicit about data, expected state, and boundaries. Use deterministic Verify steps for the final gate. Use saved auth instead of repeatedly exercising login unless login is the subject of the test. Pick a capable model for long journeys. Those choices are what turn a flashy demo into a test you can run before a merge.

Practical checklist before you add the test

A good OTP checklist starts with ownership of the test account. Make sure the account is isolated, recoverable, and not tied to a personal phone. Decide whether the test code comes from a fixture, a mailbox, a TOTP seed designed for test, or a human operator. Then decide whether the run belongs in CI, a monitor, or a manual release gate.

Before committing a 2FA and OTP login test, run through a short checklist. Is the start state controlled? Are variables used for environment-specific values? Are secrets masked? Is the final assertion deterministic? Does the test explain what failure means? Can it run in CI without a person present, or is it intentionally an interactive smoke check?

For BrowserBash specifically, decide whether the flow belongs in a single objective, a *_test.md file, or a suite. Use --viewport for a single responsive size, and use --matrix-viewport 1280x720,390x844 when the same test should run across desktop and mobile widths. Use run-all --shard 2/4 when parallel CI machines need deterministic slices based on sorted discovery order.

If you are migrating from Playwright, browserbash import <specs-or-dir> can convert many specs into plain-English *_test.md files deterministically, with no model involved. It handles common goto, click, fill, press, check, selectOption, getBy locators, and common expects. Anything untranslatable goes to IMPORT-REPORT.md instead of being dropped or invented. The recorder is useful for new manual discovery: browserbash record <url> opens a visible browser, lets you click through once, and writes a plain-English test when you stop it.

For 2FA specifically, document the test boundary in the file itself. If the code comes from a staging-only API, say so. If a tester types the code during an interactive run, say so. If the suite reuses a saved authenticated profile after one focused login check, say so. That small note prevents future maintainers from assuming the test proves more than it does. It also helps security teams review the automation because they can see that the flow respects the real control instead of bypassing it. When the identity provider changes, rerun the focused 2FA objective once, refresh the saved auth profile, and keep the rest of the suite pointed at user journeys that happen after login.

FAQ

Can AI fully automate 2FA and OTP login testing?

It can automate the browser journey, but it should not bypass security controls. If a safe test-code source exists, BrowserBash can use variables to enter it. If a real human code is required, use an interactive pause and treat the run as a human-in-the-loop smoke check.

How do I avoid exposing OTP secrets in logs?

Use BrowserBash variables and mark sensitive values as secrets so they are masked as ***** in logs. Do not commit authenticator seeds or production recovery codes. Keep production identity data out of test repositories.

Should every end-to-end test repeat 2FA login?

No. Keep one focused test for the login and 2FA gate, then use saved auth profiles for the rest of the suite. Repeating 2FA everywhere increases flake and cost without adding much new signal.

Can BrowserBash test SMS delivery itself?

BrowserBash can validate what appears in the browser and what your test environment exposes. It cannot prove carrier delivery unless your test setup observes the SMS delivery path. Be explicit about whether you are testing UI handling, backend OTP generation, or real telecom delivery.

Ready to try it locally? Install BrowserBash with npm install -g browserbash-cli, then run a plain-English browser check from your terminal. You can also sign up, and an account is optional because the CLI and local dashboard work without one.

Try it on your own appnpm install -g browserbash-cli
Start learning