Password authentication
Defend against automated login
Add layered throttling, monitoring, breached-password checks, and optional MFA without creating a permanent account-lockout attack.
A correct password check is not enough. Attackers don’t type passwords, they run scripts. They take email and password pairs leaked from other sites and try them against yours, thousands per minute. Your login endpoint needs an abuse boundary.
Three attacks, three fingerprints
Each automated attack leaves different evidence:
- Brute force: many passwords against one account.
- Password spraying: one common password against many accounts.
- Credential stuffing: leaked email and password pairs, tried at scale from many IPs.
A defense tuned for one misses the other two. That’s why we layer signals.
Why a single limit fails
Rate limiting by IP alone misses an attack spread over a botnet. It also punishes a whole school or office behind one shared NAT address.
Rate limiting by account alone creates a new attack. Ten wrong guesses lock the account for an hour? Now anyone can lock you out by typing your email and mashing the keyboard.
The answer is short-window counters on several keys at once: network address, account, device fingerprint, and a global counter for the whole endpoint. Then react progressively.
Escalate gradually
Start soft. A few failures get a one or two second delay. More failures get a challenge, a CAPTCHA or a “confirm it’s you” email. If risk keeps climbing, require a second factor.
The real user always keeps a way in. And the responses stay identical for existing and unknown accounts, so the throttling doesn’t become an enumeration oracle.
Block breached passwords
Stop the problem earlier too. When a user picks a password, check it against known breach lists with a k-anonymity API such as Have I Been Pwned’s range endpoint, which only sees the first five characters of the SHA-1 hash. Never send the full password to a third party.
Log decisions, not secrets
Record reason codes, rate-limit decisions, and coarse network information, with a retention policy. Alert on changes in the success rate, failures spread across many accounts, and attempts against administrators. Never log the submitted password.
Test with concurrency
Here’s the failure I see most often. Someone writes the counter as a JavaScript Map in process memory. It works in development. In production there are four instances behind a load balancer, each sees a quarter of the traffic, and none ever trips the limit.
Keep counters in shared storage, Redis or your database, and test with concurrent requests from a script, not by clicking the form.
Try this: write down the thresholds and responses for one failed login, for repeated failures against one account, and for a spray across many accounts. Next to each limit, write what a malicious user could do with it. If it lets an attacker lock someone out, redesign it.
Lesson completed