Flow
What each counter key lets through
1Account and address together
Stops one machine hammering one account; misses many addresses against one account.
2Account only
Catches spread-out guessing at one person; lets a stranger lock that person out.
3Address only
Catches one machine trying many accounts; can punish a whole store on one network.
4Both, each with a time window
Covers both kinds of guessing with separate limits; needs tuning and a trustworthy address.
Anyone who knows the email of someone on my staff scheduler can lock that person out. A few wrong guesses from one laptop is enough, and a small script can keep it up all day.
The lockout went in on the day I built most of the scheduler's login. The plan was simple: count wrong passwords, and after a few, block the account for a short time. When I read the diff again, the plan held up and the details didn't.
How it counts
Each wrong password adds one to a counter for that account and that address together. When one counter hits the limit, the whole account is locked for a while, and all its counters are erased. A good login clears it all.
The count and the punishment disagree about who's guilty
The count is per address, but the lock is per account, and that's the wrong way around twice.
Someone with lots of addresses can stay one short of the limit on each one, so the account never locks. And someone with one address can lock the real owner out of every device. Since the lock erases the counters, they get a fresh set of tries after every cool-down. I built a way for a stranger to shut off a named employee's login.
A timestamp nobody reads
The table saves the time of each miss but never checks it. So the limit has no time window. Old misses still count, and the owner's normal login wipes the attacker's count along with their own.
The locked answer is an account checker
Made-up emails are never counted, so they never get the locked answer. Real ones do. That tells anyone which emails belong to my staff. The made-up path also skips the slow password check, so I'd guess it's faster too, but I haven't timed it.
One good thing: the lock is now checked before the password, so a locked account no longer tells you if your guess was right.
The race between checking and counting
Guesses sent at the same moment all pass the lock check before any of them is counted. A burst can probably get a few extra tries.
The reset flow is part of this
A password reset clears the lock. The reset request has no limit, so it can be spammed to keep breaking the owner's reset email.
The version I'd rather have
If I were starting this login over, so a stranger could slow staff down but not lock them out, this is what I'd want:
- one counter per email and one per address, each with a time window
- growing delays instead of a hard lock
- the same answer and the same slow check for real and fake emails
- one step that adds to the count and returns it
One step adds to the counter for an email and returns the new count.
Leave room for a whole store sharing one network, and test the parts I only guessed at, since the commit had none. Right now a locked-out worker can wait out the cool-down or reset their password, and neither helps if a stranger keeps locking them again.