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

@@ -78,6 +78,30 @@ class LoginRateLimiterTest {
() -> rateLimiter.checkAndConsume("1.2.3.4", "other@example.com"));
}
@Test
void email_lookup_is_case_insensitive_so_mixed_case_shares_the_same_bucket() {
for (int i = 0; i < 10; i++) {
rateLimiter.checkAndConsume("1.2.3.4", "User@Example.COM");
}
assertThatThrownBy(() -> rateLimiter.checkAndConsume("1.2.3.4", "user@example.com"))
.isInstanceOf(DomainException.class)
.satisfies(ex -> org.assertj.core.api.Assertions.assertThat(((DomainException) ex).getCode())
.isEqualTo(ErrorCode.TOO_MANY_LOGIN_ATTEMPTS));
}
@Test
void invalidateOnSuccess_is_case_insensitive_so_mixed_case_clears_the_bucket() {
for (int i = 0; i < 10; i++) {
rateLimiter.checkAndConsume("1.2.3.4", "user@example.com");
}
rateLimiter.invalidateOnSuccess("1.2.3.4", "User@Example.COM");
assertThatNoException().isThrownBy(
() -> rateLimiter.checkAndConsume("1.2.3.4", "user@example.com"));
}
@Test
void ip_exhaustion_does_not_consume_ipEmail_tokens_for_blocked_attempts() {
// Use a tighter limiter so the phantom-consumption effect is observable.