Careeroza
Account Lockout
Why
Attackers try leaked passwords from other sites against your login (credential stuffing) or guess passwords one by one (brute force). Two defences work together:
- Account lockout: after N failures for one account, block that account for a while.
- Rate limiting: limit how many attempts one IP (or one client) can make per minute, across all accounts.
A Lockout Counter
Example
Five wrong passwords lock the account for 15 minutes. Even the correct password is refused while locked. Time is passed in, so the example is deterministic (in Spring you inject a Clock):
import java.time.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
LoginService svc = new LoginService(Map.of("asha", "S3cret!pass"));
Instant t = Instant.parse("2026-03-01T10:00:00Z");
for (int i = 1; i <= 5; i++) {
System.out.println(time(t.plusSeconds(i)) + " asha / wrong" + i + " -> " + svc.login("asha", "wrong" + i, t.plusSeconds(i)));
}
System.out.println(time(t.plusSeconds(10)) + " asha / correct -> " + svc.login("asha", "S3cret!pass", t.plusSeconds(10)));
Instant later = t.plus(Duration.ofMinutes(16));
System.out.println(time(later) + " asha / correct -> " + svc.login("asha", "S3cret!pass", later));
System.out.println(time(later) + " ravi / guess -> " + svc.login("ravi", "guess", later));
}
static String time(Instant i) { return i.toString().substring(11, 19); }
}
class LoginService {
static final int MAX_FAILURES = 5;
static final Duration LOCK_TIME = Duration.ofMinutes(15);
private final Map<String, String> passwords; // demo only: real code stores BCrypt hashes
private final Map<String, Integer> failures = new HashMap<>();
private final Map<String, Instant> lockedUntil = new HashMap<>();
LoginService(Map<String, String> passwords) { this.passwords = passwords; }
String login(String user, String password, Instant now) {
Instant until = lockedUntil.get(user);
if (until != null && now.isBefore(until)) return "LOCKED until " + until.toString().substring(11, 19);
if (until != null) { lockedUntil.remove(user); failures.remove(user); } // lock expired
if (password.equals(passwords.get(user))) {
failures.remove(user);
return "OK";
}
int count = failures.merge(user, 1, Integer::sum); // unknown users are counted too
if (count >= MAX_FAILURES) {
lockedUntil.put(user, now.plus(LOCK_TIME));
return "BAD_CREDENTIALS, account locked";
}
return "BAD_CREDENTIALS (" + count + "/" + MAX_FAILURES + ")";
}
}Output
10:00:01 asha / wrong1 -> BAD_CREDENTIALS (1/5) 10:00:02 asha / wrong2 -> BAD_CREDENTIALS (2/5) 10:00:03 asha / wrong3 -> BAD_CREDENTIALS (3/5) 10:00:04 asha / wrong4 -> BAD_CREDENTIALS (4/5) 10:00:05 asha / wrong5 -> BAD_CREDENTIALS, account locked 10:00:10 asha / correct -> LOCKED until 10:15:05 10:16:00 asha / correct -> OK 10:16:00 ravi / guess -> BAD_CREDENTIALS (1/5)
Note: Lockout lets an attacker lock out a real user on purpose (denial of service). Keep lock times short (15 minutes, growing on repeat), notify the user by email, and show a CAPTCHA after 3 failures instead of locking immediately.