fix(auth): sequential rate-limit check with ipEmail token refund on IP failure
Addresses Felix (blocker 1): the old implementation consumed from both buckets before checking either result, silently eroding the per-email quota when only the per-IP limit was blocking. The fix checks ipEmail first, then IP; on IP failure it refunds the ipEmail token so legitimate users behind a shared IP are not penalised. Also adds two new test cases: - different_email_from_same_ip_not_blocked_by_sibling_email_exhaustion (Sara) - ip_exhaustion_does_not_consume_ipEmail_tokens_for_blocked_attempts (red → green) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,10 +36,18 @@ public class LoginRateLimiter {
|
||||
.build(key -> newBucket(maxPerIp, windowMinutes));
|
||||
}
|
||||
|
||||
// NOTE: This cache is node-local (in-memory). In a multi-replica deployment,
|
||||
// effective limits would be multiplied by replica count.
|
||||
// For the current single-VPS setup this is the correct, simplest implementation.
|
||||
|
||||
public void checkAndConsume(String ip, String email) {
|
||||
boolean ipEmailOk = byIpEmail.get(ip + ":" + email).tryConsume(1);
|
||||
boolean ipOk = byIp.get(ip).tryConsume(1);
|
||||
if (!ipEmailOk || !ipOk) {
|
||||
if (!byIpEmail.get(ip + ":" + email).tryConsume(1)) {
|
||||
throw DomainException.tooManyRequests(ErrorCode.TOO_MANY_LOGIN_ATTEMPTS,
|
||||
"Too many login attempts from " + ip);
|
||||
}
|
||||
if (!byIp.get(ip).tryConsume(1)) {
|
||||
// Refund the ipEmail token so IP-level blocking does not erode the per-email quota.
|
||||
byIpEmail.get(ip + ":" + email).addTokens(1);
|
||||
throw DomainException.tooManyRequests(ErrorCode.TOO_MANY_LOGIN_ATTEMPTS,
|
||||
"Too many login attempts from " + ip);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user