How to test 2FA/TOTP login: what an agent can actually verify

Most guides on how to test 2FA/TOTP login start by generating the code. That's the least interesting part. Here's how to test the enforcement, lockout, backup codes, and disable flow around TOTP in plain English — and an honest note on what a browser agent can and can't do with a rotating code.

playbookauthentication2fasecurity-testingno-codeqa
monito

How to test 2FA/TOTP login: what an agent can actually verify

playbookauthentication2fasecurity-testing
September 11, 2026

Every guide on how to test 2FA/TOTP login opens the same way: here's how to generate the six-digit code in your test. Store the shared secret as a fixture, run it through an otpauth library, type the result into the form. That's genuinely useful for a scripted suite, and it's also the least interesting thing about a two-factor login. The code generation is the part that already works — it's a well-specified algorithm with a dozen libraries. The bugs don't live there. They live in everything the app does around the code: whether it actually requires the second factor, whether it locks out after a hundred wrong guesses, whether a used backup code still works, whether "remember this device" quietly turns the whole thing off. That's the surface this playbook tests, and most of it doesn't need a valid TOTP at all.

The honest limit, stated first

A browser agent driving a real Chromium session cannot compute a rotating TOTP code on its own. TOTP is, per OWASP's testing guide, "a code based on HMAC of a secret and the current time" — a fresh six digits every 30 seconds derived from a secret you'd have to hold and a clock you'd have to track. Without a code-generating tool wired in, the agent has no way to produce the number your authenticator app would show. So if you're picturing an agent that scans the QR code, becomes your authenticator, and logs in through 2FA end to end on its own — that's not the honest pitch, and any guide implying it is glossing over the mechanism.

Here's what changes once you accept that: almost none of the interesting 2FA tests require the agent to generate a live code. Enforcement, lockout, backup-code behavior, the disable/reset flow, the UX of a wrong code — every one of those is testable from the outside with either no code, a deliberately wrong code, or a static fixture value you supply. TOTP has one companion secret that a browser agent can use perfectly well: the single-use backup codes the app hands you at setup. They're just strings. Provide one as a scenario credential and the agent can complete a real authenticated login without ever computing a time-based number. So the real testable surface is wide — it's just not the part the tutorials fixate on.

Prompt 1: is the second factor actually enforced?

The first and most important 2FA test has nothing to do with codes. It's whether the app can be walked past the second step entirely. OWASP names the classic bypass directly: "it may be possible to bypass it by completing the first step of the authentication process (entering the username and password), and then force-browsing to the application or making direct API requests without completing the second stage (entering the MFA code)." If entering the password gets you a session and the 2FA screen is just a page you can navigate away from, the second factor is decoration.

Go to https://staging.yourapp.com and start logging in as
2fa-test@example.com / Password123!

This account has 2FA enabled, so after the password step you should
land on a "enter your authentication code" screen. Do NOT enter a code.

Instead, test whether the second factor can be skipped:
1. From the code-entry screen, try navigating directly by URL to a
   protected page (e.g. /dashboard or /account).
2. Then try the browser Back button and Forward button from the
   code-entry screen.
3. Report whether ANY of these reach logged-in content, or whether you
   are held on the 2FA step / bounced back to login every time.

The correct behavior: no password-only session works. Every protected
page must refuse you until the second factor is completed. Flag it if
any private data renders after only the password step, and report the
HTTP status of each protected request you tried.

Step 3 is the whole test. A screenshot of the code-entry screen proves nothing; what matters is what the protected request returns while you're stuck on that screen. The agent watches the network, so a /dashboard that quietly returns 200 with real data after only the password is the bypass, reported as one — not hidden behind a redirect that looked fine to a human. This is the same reason an agent finds the bugs a script walks past: the script was written to enter the code, so it never tries the door that skips it.

While you're here, OWASP's other enforcement note is worth a second scenario: "all of the different login methods should be reviewed, to ensure that MFA is enforced consistently." If your app has a separate mobile login, an API sign-in, or a federated path, 2FA has to hold on every one of them. The agent can't test your native app, but it can hit an alternate web login route or an API login endpoint and confirm the second factor isn't optional there.

Prompt 2: lockout and the wrong-code path

A six-digit code has a million possibilities, which sounds like a lot until you realize it isn't if the endpoint accepts unlimited guesses. OWASP's MFA testing checklist leads with exactly this: "Is the account locked out after multiple failed MFA attempts? Is the user's IP address blocked after multiple failed MFA attempts across different accounts? Are failed MFA attempts logged?" You test all three with codes you know are wrong, so no generation needed.

Go to https://staging.yourapp.com and log in as
2fa-test@example.com / Password123!, reaching the 2FA code-entry
screen.

Now test brute-force protection using deliberately WRONG codes:
1. Enter an obviously invalid code (e.g. 000000) and submit. Note the
   error message.
2. Repeat with different wrong codes (111111, 123456, 999999, and a few
   more) up to about 10 attempts.
3. Report at what point — if ever — the app stops accepting attempts:
   does it lock the account, rate-limit, add a delay, or show a
   CAPTCHA? Or does it accept wrong code number 10 as readily as number
   1 with no throttling at all?

Also check the error copy: does a wrong code say something neutral like
"incorrect code," or does it leak whether the account or 2FA setup
exists? Capture the screen after the final attempt and report the exact
lockout behavior.

"No lockout at all" is the finding, and it's common, because rate-limiting the second factor is an easy thing to forget when the happy path works on the first try. The agent gives you the concrete number — where the throttle kicks in, or that it never does — which is the difference between a checkbox and evidence.

If your app sends codes by email or SMS instead of (or alongside) TOTP, there's a second wrong-path bug worth a scenario, and the agent can drive it because it involves requesting codes, not computing them. OWASP flags two things to check: whether "multiple OTPs are valid at once, or generating a new code invalidates the previous one," and whether there's "sufficient rate-limiting to prevent an attacker requesting large numbers of codes." The MFA cheat sheet's rule is that on "resend, generate a new OTP and overwrite the old record" — so an agent can hammer the "resend code" button a dozen times and report whether the app throttles it (correct) or cheerfully fires a dozen emails that each cost you money and, worse, whether every one of those codes still works. A pile of simultaneously-valid codes is a bigger brute-force surface than the six digits suggest.

Prompt 3: backup codes, and whether "used" means used

This is where a browser agent completes a real authenticated 2FA login, because backup codes are static strings you can hand it as a credential. OWASP's requirement for them is precise: recovery codes "can only be used once," must have "brute-force protection… (such as account lockout)," and the user should be "notified… when a code is used." The single-use rule is the one that breaks in practice.

Go to https://staging.yourapp.com and log in as
2fa-test@example.com / Password123! When prompted for the
authentication code, choose the "use a backup code" option.

Backup code to use: 4a91-bd07-2fce

1. Enter that backup code and confirm you are logged in successfully.
2. Now log out completely.
3. Log back in with the same password, choose "use a backup code"
   again, and enter the SAME code (4a91-bd07-2fce) a second time.
4. Report whether the app REFUSES the reused code (correct) or lets you
   in again on a code that was already spent (a real vulnerability).

Also note: after step 1, did the app show any indication that a backup
code had been consumed — a warning, a decremented count, an email? Flag
a reused code that still works, and capture each login result.

Substitute a real backup code from your test account. If step 3 logs you in, the app is treating single-use codes as reusable, which turns the recovery path into a permanent bypass. Because backup codes are static, this is a flow the agent can drive fully — the same durable, intent-described approach the magic-link auth playbook and the password-reset playbook use for the auth flows that get rebuilt most often.

Prompt 4: the disable/reset flow, and "remember this device"

The riskiest 2FA code path is the one that turns 2FA off. OWASP's cheat sheet is blunt that changing or removing a factor must "require reauthentication with an existing enrolled factor before allowing changes" and must "not rely solely on the active session, as it may be hijacked." In other words: an attacker who steals a live session should not be able to silently disable your second factor from the settings page.

Log in to https://staging.yourapp.com as
2fa-test@example.com / Password123! (use backup code 4a91-bd07-2fce
if prompted for a second factor).

Go to the security / account settings where 2FA can be disabled or
reset, and test what it takes to turn 2FA off:
1. Attempt to disable 2FA. Report whether the app requires you to
   re-authenticate — re-enter your password or a current code — BEFORE
   it will disable, or whether an already-logged-in session can switch
   it off with no further proof.
2. If the app offers "remember this device," enable it, then report
   what it actually changes on the next login: does it skip the code on
   THIS browser only, or does it appear to stop prompting entirely?

Flag it if 2FA can be disabled from a live session with no
re-authentication, or if "remember this device" seems to disable the
second factor rather than scoping a trusted device. Capture the
settings state and each result.

"Remember this device" is where teams accidentally ship an off switch labeled as a convenience. It should scope one browser as trusted; it should not mean "never ask again, anywhere." The agent reports which one you built. And the disable-without-reauth case is the quiet one — it looks fine in a demo because the demo user is legitimately logged in, and it's a full account-takeover primitive the moment a session leaks.

Reading the results

Every run returns a Monito Session: a screenshot timeline of each state — password step, code-entry, wrong-code lockout, backup-code login, settings — plus the network log with every request and its status code. For 2FA the network log is the evidence that matters, because the whole question is server-side enforcement: each network event is captured with its method, URL, and status, so a protected page that returned 200 after only the password is provably a bypass, and a clean 401/302 is provably a pass. None of the prompts above names a selector — only the behavior — which is why they survive the frequent redesigns that auth screens go through, the same describe-the-intent durability that makes agent runs hold up where scripts rot. For where 2FA sits in the broader auth surface, the signup-flow and session-timeout playbooks cover the neighbors.

The Monito CLI reads your account and Environment config; the docs on authenticating the CLI cover wiring a staging Environment and test credentials so these scenarios point at the right build.

The one to run right now

If you run a single 2FA check today, run the enforcement probe — it needs no code, and it proves the one property that makes the whole feature real: that the second factor can't be skipped. Save it as a Test Scenario, point it at staging, and wire it into CI on every deploy that touches auth or middleware.

Go to https://staging.yourapp.com and begin logging in as
2fa-test@example.com / Password123! — an account with 2FA enabled — so
you reach the "enter authentication code" screen. Do NOT enter a code.

Now try every way to reach logged-in content without completing the
second factor: navigate directly by URL to /dashboard, /account, and
any other protected page; use the browser Back and Forward buttons from
the code screen; and if the app exposes an API login or an alternate
web login route, try that path too. For each attempt, report the HTTP
status and whether any private, logged-in-only data rendered.

The correct result is that NO password-only session reaches protected
content — every attempt is refused until the code is entered. Flag any
route that serves private data or a valid session after only the
password, capture screenshots of each attempt, and include the full
network log.

A full run is typically 8–13 credits — roughly $0.08–$0.13 — and your first run is free. Monito plans start at $99/mo, with Enterprise at $129/mo. One honest caveat, to match the one at the top: this tests 2FA from the outside, the way a user or attacker meets it, which is exactly the coverage scripted suites skip — but it's not a substitute for a code-level review of your TOTP verification and secret storage. Treat it as the every-deploy proof that the second factor you shipped still actually stands between the password and the account.

All Posts