Password reset flow testing: the edge cases that become account takeovers
Password reset is the auth flow attackers reach for first. Here's how to test it end to end — token reuse, expiry, enumeration, session invalidation — in five plain-English prompts, no Playwright.
Password reset flow testing: the edge cases that become account takeovers
The "forgot password" link is the part of your auth system an attacker reaches for first. It's the one flow that, by design, hands an unauthenticated stranger a way to change a password — so every shortcut in it is a potential account takeover, not just a bug. And it's the flow nobody tests properly, because the happy path is trivial: request a link, click it, set a new password, done. The happy path is maybe 20% of what can go wrong.
This is the testing playbook we'd actually run against a password reset flow, written as five plain-English prompts. No Playwright, no token-decoding logic baked into a test suite, no selectors to maintain. If you've shipped a reset flow before and just want the prompts, scroll down. If you're about to ship one, the next two sections will save you a security advisory.
What "testing password reset" actually means
OWASP's Forgot Password Cheat Sheet is the reference here, and it's refreshingly concrete about what a secure reset flow has to do. Strip it down to the things you can actually observe from a browser and you get a checklist most teams have never fully run:
- Single-use tokens. A reset token works exactly once. After it's been used to set a new password, it's dead — even within its lifetime. OWASP lists "invalidated after they have been used" as a hard requirement, and it's the single most commonly violated one.
- Token expiry. A token presented after its lifetime fails cleanly. The cheat sheet calls for tokens that "expire after an appropriate period" — short enough that a leaked email from last week is worthless.
- No account enumeration. The page after "send me a reset link" returns the same message and in the same amount of time whether or not an account exists. OWASP is explicit: "return a consistent message for both existent and non-existent accounts" and "ensure that the time taken for the user response message is uniform." Miss either and you've built a free user-existence oracle.
- Rate limiting on requests. Submitting the same address a hundred times doesn't fire a hundred emails. OWASP frames this as protecting the user's inbox from being flooded, not just protecting your mail budget.
- Session handling after reset. Resetting a password invalidates other sessions (or asks the user whether to), and — per OWASP — does not automatically log the user in. The old password stops working immediately.
- No account lockout via reset. Requesting a reset for someone else's account must not lock them out. OWASP calls this out directly: "accounts should not be locked out in response to a forgotten password attack," because that turns the reset form into a denial-of-service tool against any known username.
Anything less than this is a smoke test that proves the email sends. We're going to write prompts for all of it.
The bug everyone ships at least once
Before the prompts, the failure mode to hunt first: a reset token that stays valid after it's been used.
It happens because the naive implementation treats "the password was changed" and "the token was consumed" as two separate facts, and only reliably writes the first one. The reset handler verifies the token, updates the password, redirects to a success page — and the line that marks the token consumed either runs in a code path that the redirect skips, or was never written because "the password already changed, who'd reuse it?"
Plenty of people would. If the token survives in an inbox, a forwarded email, a browser history, or a proxy log, a single-use failure means anyone who finds it later can change the password again and walk in. OWASP's wording is blunt for a reason: tokens must be "single use and expire after an appropriate period." Those are two independent checks, and the single-use one is the one teams forget.
You can't see the database from a browser, but you can see the visible consequence — use a token to set a password, then try the same token again. If the second attempt is accepted, the enforcement is missing. Prompt 2 below does exactly that.
Setup: what the agent needs
You want a staging environment where:
- Reset emails land in a real test inbox you can read — a
@yourapp.testcatch-all, or a hosted service like Mailosaur or Mailtrap with a readable message URL. - The token TTL matches production. Don't test against a 24-hour TTL "to make it easier."
- Rate limiting is enabled with production-like thresholds.
The Agent executor reads the reset email the way a person would — give it the inbox URL or the mailbox's API endpoint. Don't bake inbox credentials into the prompt text; pass them through your Test Scenario config and reference them as a placeholder like the inbox URL token below. Throughout, the target is https://staging.yourapp.com — substitute your own.
Prompt 1: the happy path
The boring one. If this is broken, every other test is noise.
The bug class this catches that you wouldn't think to script: a reset link that's double-percent-encoded and 404s, a confirmation page that leaks account existence in its wording, an auto-login that quietly skips the login-screen step OWASP warns against.
Prompt 2: token reuse
The test most teams skip, and the one most likely to surface a real vulnerability.
The failure you're hunting: the token is single-use in intent but not in enforcement, so the same link sets a password twice. The agent won't reproduce a millisecond-level double-submit race, but it nails the structurally identical "I used this link yesterday and it still works" case — which is the one that shows up in a bug bounty report.
Prompt 3: expired token
The TTL test. You need a way to age a token past its lifetime — an admin endpoint, a non-prod query parameter that shortens the TTL, or a clock you can advance. Most staging setups have one; if yours doesn't, the cheapest version is a request-side override the endpoint only honors outside production.
The bug to catch: an expired token that fails silently on a blank page, or — far worse — one that's accepted anyway because the expiry check reads the wrong column or compares timestamps in the wrong timezone.
Prompt 4: account enumeration
The security one. OWASP devotes specific language to it because it's so easy to ship: the request flow must be indistinguishable for real and fake accounts, in both wording and timing.
OWASP's guidance is two-pronged on purpose: "return a consistent message" and "ensure that responses return in a consistent amount of time." A team that fixes the wording and leaves a 200ms timing gap has fixed half the bug. The agent can't measure microseconds, but it reliably tells apart differences in the 100ms-and-up range — which is where the lazy "quick exit on unknown account" implementations live.
Prompt 5: session invalidation after reset
The one that matters most after a real compromise, and the one almost nobody tests. If an attacker had a session open, resetting the password must not leave that session alive.
OWASP recommends the app either invalidate existing sessions automatically or ask the user whether to. The bug to catch is the silent third option: the password changes, but the attacker's already-open session keeps working because session invalidation was never wired to the reset event. That's the difference between "we forced a reset" and "we forced a reset and it actually evicted them."
What you do with a failure
A failed run on a reset flow gives you the same artifact as any other Test Run: a Monito Session with a screenshot timeline, the full network log (the POST /forgot-password request and the POST /reset-password call with payloads), console output, and the agent's reasoning at each branch. For auth bugs the network log is where you'll live — it's where you'll see the reset endpoint return success on a reused token, or the timing gap between a real and a fake account. If you're new to reading agent runs instead of green checkmarks, why AI QA agents find bugs your scripts miss is the longer argument for why the session beats the assertion.
These five prompts are close cousins of the ones in our magic-link auth playbook and the signup flow guide — token reuse, expiry, and enumeration recur across every credential flow, which is exactly why describing the intent once and reusing it beats re-scripting selectors per form.
Run these on every release
Five prompts, a few minutes of agent time per full sweep, well under a dollar in run credits (a typical full run is 8–13 credits, roughly $0.08–$0.13). Wire them into a CI/CD step that runs against every preview deploy or against staging on a schedule. The prompts don't rot when you rename the "Send reset link" button or restyle the form — there are no selectors to update — so the maintenance cost over the next year rounds to zero.
Password reset is the flow where a single missed check equals a stranger inside someone's account. Five prompts is cheap insurance against the one bug class that turns "forgot password" into "forgot whose account this is."
Want to run these now? Sign up for Monito, drop the five prompts into a new Project pointed at your staging URL, wire up a test inbox, and go. Your first run is free.