fix(auth): normalise email to lowercase before rate-limit key lookup

Case variants of the same address (e.g. User@EXAMPLE.COM vs user@example.com)
now share a single Bucket4j bucket, preventing a trivial bypass of per-email
limits via mixed-case submissions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-18 15:43:19 +02:00
committed by marcel
parent 6f452a9a8b
commit 64d8f9d904
2 changed files with 29 additions and 3 deletions

View File

@@ -10,6 +10,7 @@ import org.raddatz.familienarchiv.exception.ErrorCode;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
@Service
@@ -41,20 +42,21 @@ public class LoginRateLimiter {
// For the current single-VPS setup this is the correct, simplest implementation.
public void checkAndConsume(String ip, String email) {
if (!byIpEmail.get(ip + ":" + email).tryConsume(1)) {
String key = ip + ":" + email.toLowerCase(Locale.ROOT);
if (!byIpEmail.get(key).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);
byIpEmail.get(key).addTokens(1);
throw DomainException.tooManyRequests(ErrorCode.TOO_MANY_LOGIN_ATTEMPTS,
"Too many login attempts from " + ip);
}
}
public void invalidateOnSuccess(String ip, String email) {
byIpEmail.invalidate(ip + ":" + email);
byIpEmail.invalidate(ip + ":" + email.toLowerCase(Locale.ROOT));
byIp.invalidate(ip);
}