Security
Account takeover prevention checklist for SaaS products
A practical account takeover prevention checklist for SaaS teams: breached-password checks, exposed email monitoring, MFA prompts, rate limiting and alerting.

Account takeover (ATO) rarely starts with a clever exploit. It usually starts with a password that already leaked somewhere else and a login form that accepted it without a second thought. This checklist walks through the controls that stop most takeovers in a SaaS product, in the order most teams can ship them, and shows where Finowl fits in.
Why account takeover keeps happening
Attackers have access to billions of leaked credentials from past breaches. People reuse passwords, so a username and password pair stolen from one site often works on another. Automated tools replay those pairs against login forms at scale, a technique known as credential stuffing. We cover the mechanics in more depth in credential stuffing and breached passwords.
The good news is that the defenses are well understood. Current NIST guidance (SP 800-63B) recommends checking new passwords against lists of known-compromised passwords, and most of the remaining controls are standard engineering: limit attempts, ask for a second factor when risk is high, and tell people when something looks wrong.
The checklist at a glance
| Control | Where it runs | What it stops |
|---|---|---|
| Breached-password check | Signup, password change, login | Reused and commonly leaked passwords |
| Breach-exposed email monitoring | Background job, onboarding | Accounts tied to leaked addresses |
| Step-up MFA prompts | Login, sensitive actions | Correct-password logins from attackers |
| Rate limiting and lockout | Login and reset endpoints | Automated guessing and stuffing |
| Alerting and notifications | Security events | Silent compromise that goes unnoticed |
Each section below explains the control, the common mistakes, and a reasonable default.
1. Check passwords against breach data
At signup and password change
The highest-value single control is refusing passwords that have already appeared in breach data. If a password has been seen in a leak, it is in the wordlists attackers use, no matter how complex it looks.
Finowl's password breach check takes a password and tells you whether it has been seen and how many times. Finowl hashes the password with SHA-1 and looks up only a 5-character prefix of the hash using a k-anonymity range search with padding. The password is processed transiently and is never logged, stored or returned.
curl -X POST https://api.finowl.co/v1/breaches/password \
-H "Authorization: Bearer $FINOWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "password": "Summer2019!" }'A leaked password returns something like this:
{ "breached": true, "count": 133211 }A password that was not found returns { "breached": false, "count": 0 }.
In your signup handler, block the password when breached is true and show a clear message: "This password has appeared in a data breach. Please choose a different one." Avoid vague errors, because people will just try a small variation of the same password. The password checks at signup guide and our post on how to block breached passwords at signup cover the UX in more detail.
At login
Checking at login catches accounts created before you added the signup check. A login-time check should not block the user, because they have just proven they know the password. Instead, let them in and require a password change, or step up to MFA first (see section 3).
Here is a minimal Node.js helper you can call after a successful password verification:
async function isPasswordBreached(password) {
const res = await fetch("https://api.finowl.co/v1/breaches/password", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FINOWL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ password }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
// Fail open on login so an API problem never locks users out.
console.warn("password check failed", res.status, body.error?.code);
return false;
}
const { breached } = await res.json();
return breached;
}Decide in advance whether each check fails open or closed. Most teams fail open at login (let the user in, log the error) and fail closed or retry at signup, where a short delay is acceptable.
Checklist items
- Check every new password at signup, reset and change.
- Check existing passwords at login and flag breached ones for a forced change.
- Show a specific, friendly error message.
- Never log the password or the request body that contains it.
2. Monitor emails for breach exposure
A breached password tells you about the secret. A breached email address tells you about the person: their address appeared in a breach, so a password they used elsewhere may be in circulation, and they are a likely phishing target.
Finowl's email breach check returns whether an address appears in known breaches, along with details for each one.
curl -X POST https://api.finowl.co/v1/breaches/email \
-H "Authorization: Bearer $FINOWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "alex@example.com" }'Each entry in breaches includes fields such as Title, Domain, BreachDate, DataClasses and IsVerified. The fields that matter most for ATO risk are:
DataClasses: if it includes "Passwords", the person's credentials from that site may be circulating.BreachDate: a recent breach is a stronger signal than one from many years ago.IsVerified: unverified breaches are included in results, so weigh them accordingly.
How to use the signal
Use email exposure to adjust risk rather than to block anyone. Reasonable actions include nudging the user to enable MFA, raising the risk score on their next login from a new device, or recommending a password change when a recent breach exposed passwords. A scheduled job that rechecks active accounts every month or so keeps the signal fresh.
Checklist items
- Check emails at signup and periodically for active accounts.
- Treat breaches with passwords in
DataClassesas higher risk. - Use the result to prompt MFA enrollment, not to deny access.
3. Prompt for MFA when risk is high
Even a perfect password check cannot help when an attacker has the user's real, unique password. Multi-factor authentication is the control that holds when the password does not.
Make enrollment easy
Offer authenticator apps and passkeys. Prompt for enrollment at moments when users are motivated: right after signup, after a breached-password warning, or when they add billing details or invite teammates.
Use step-up prompts
You do not need to challenge every login. Step-up MFA asks for a second factor only when signals suggest risk:
- The password was found in breach data.
- The email has recent exposure with passwords in
DataClasses. - The login comes from a new device, browser or location.
- The user is about to do something sensitive, like changing their email, exporting data or creating API keys.
Checklist items
- Support at least one phishing-resistant method if you can.
- Require MFA for admin and owner roles.
- Re-verify before changing email, password or MFA settings.
4. Rate limit login and recovery endpoints
Credential stuffing depends on volume. Rate limiting makes it slow and noisy.
What to limit
- Attempts per account, to stop targeted guessing.
- Attempts per IP address and per network range, to slow distributed attacks.
- Password reset and magic-link requests, which attackers use for enumeration.
Return the same response for "wrong password" and "no such account" so attackers cannot test which emails are registered. Prefer progressive delays and CAPTCHAs over hard lockouts, since hard lockouts let attackers lock real users out on purpose.
Respect limits on your dependencies too
If you call Finowl inside your login flow, remember that the API allows 60 requests per minute per account. Over the limit you get a 429 with code rate_limited and a Retry-After header in seconds. During an attack, your own rate limiter should reject junk traffic before it reaches any paid lookup, which also protects your credits.
5. Alert people and your team
Takeovers that nobody notices do the most damage. Good alerting shortens the window.
User-facing notifications
Email the user when a password changes, an email address changes, MFA is disabled, or a login happens from a new device. Include a one-click "this wasn't me" link that locks the session and starts recovery.
Team-facing alerts
Track failed-login rates, breached-password hits at login, and reset requests per hour. A sudden spike in failed logins across many accounts is the classic signature of credential stuffing. Route those alerts to whoever is on call.
Checklist items
- Notify users of every security-relevant change.
- Alert your team on spikes in failed logins and breached-password hits.
- Keep an audit log of security events per account.
Where Finowl fits
Finowl covers the breach-data part of this checklist: the password check for signup, change and login, and the email check for onboarding and monitoring. Both are simple POST requests with a bearer token, so they drop into existing auth code without new infrastructure. You can try both in the Finowl Sandbox before writing any code.
Each lookup costs 1 credit, and every account gets 100 free credits each month, which is enough to prototype the whole flow. For production volume, prepaid credit packs never expire; see pricing. The rest of the list (MFA, rate limiting and alerting) lives in your own application, and Finowl's results make those controls smarter by telling you when risk is elevated.
Frequently asked questions
Should I block login when a password is found in breach data?
No. The user has just proven they know the password, so blocking them adds friction without stopping an attacker who has the same password. Let them in behind step-up MFA, then require a password change.
Does Finowl see the passwords I check?
Finowl hashes the password with SHA-1 and looks up only a 5-character hash prefix using a k-anonymity range search with padding. The password is processed transiently and is never logged, stored or returned.
How often should I check user emails for breaches?
Check at signup, then on a schedule that fits your risk and budget, such as monthly for active accounts. Each check costs 1 credit, so focus on accounts with access to sensitive data first.
What happens if the breach API is unavailable during login?
Decide this up front. Most teams fail open at login and log the error, and retry or fail closed at signup. Retry a 429 only after the Retry-After delay, and be careful retrying 5xx errors because the lookup may have run and been charged.


