Add rate limiting on auth and invite endpoints #1
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
There is no rate limiting anywhere in the application.
/v1/auth/loginis wide open for credential stuffing and brute-force attacks./v1/invites/{code}/acceptcan be used for invite code enumeration./v1/auth/signupenables account spam.Attack scenario
An attacker can attempt thousands of login requests per second against
/v1/auth/loginto brute-force credentials, or enumerate invite codes against the accept endpoint.Affected files
SecurityConfig.java— no rate limiting filter configuredRecommended fix
Add rate limiting at minimum on:
/v1/auth/login(e.g., 5 attempts per minute per IP)/v1/auth/signup(e.g., 3 per minute per IP)/v1/invites/*/accept(e.g., 10 per minute per IP)Consider using a servlet filter with Bucket4j or Spring Boot's built-in rate limiting support.
Severity
Critical — without this, brute-force and credential stuffing are trivially possible.
👨💻 Kai — Frontend Engineer
Rate limiting is a backend concern, but it surfaces directly in the frontend UX when limits are hit. Here's what I need to handle.
Frontend implications of 429 responses:
/v1/auth/loginreturns429 Too Many Requests, the login form must show a clear, user-friendly message — not a generic error. Something like "Too many login attempts. Please wait a minute before trying again." I'll wire this up in the+page.server.tslogin action, catching 429 specifically.429response should ideally include aRetry-Afterheader. If it does, I can display a countdown: "Try again in 45 seconds." That's a better UX than "wait a minute" with no feedback./v1/auth/signup), same pattern — 429 should show "Too many signup attempts. Please try again later."Questions:
429response body include amessagefield with a human-readable reason, or just the status code? I'll use whatever is in the response body for the error display.Retry-Afterheader in the plan? If yes, I'll build the countdown UI. If no, the message will be generic.No code blockers — just need the response contract defined before I implement the error states.
🏗️ Backend Engineer — Spring Boot / PostgreSQL Specialist
Agreed this is critical. Let me lay out the implementation options with the tradeoffs clearly, since the issue mentions a few approaches.
Option 1: Bucket4j (recommended for v1)
Bucket4j is a Java rate-limiting library based on the token bucket algorithm. It integrates cleanly with Spring Boot as a servlet filter or Spring MVC interceptor, and supports in-memory (no Redis required) or distributed (Redis/Hazelcast) backends.
For v1 (single instance), Caffeine-backed in-memory buckets are sufficient. If we later go multi-instance, we switch to Redis-backed Bucket4j — the API is the same.
Option 2: Spring's built-in rate limiting
The issue mentions "Spring Boot's built-in rate limiting support" — worth clarifying: Spring Boot doesn't have native rate limiting out of the box as of Spring Boot 3.x/4.x. Spring Cloud Gateway has rate limiting, but we're not using that. The recommendation here is Bucket4j or a servlet filter, not a built-in Spring feature.
Implementation plan:
bucket4j-core+caffeinedependencies topom.xmlRateLimitFilterthat implementsOncePerRequestFilter, extracts client IP fromX-Forwarded-FororRemoteAddr, and applies per-endpoint limitsSecurityFilterChainbefore the auth processing filters429 Too Many Requestswith aRetry-Afterheader on limit exceededQuestions:
X-Forwarded-For— we must trust this header only from known proxy IPs, or attackers can spoof it to bypass per-IP limits.🧪 QA Engineer
Rate limiting is deceptively tricky to test correctly. Here's the full test plan.
Unit tests:
429returnedIntegration tests:
POST /v1/auth/login— send 6 rapid requests from the same IP → first 5 succeed (or return auth error), 6th returns429POST /v1/auth/signup— send 4 rapid requests → first 3 succeed/fail normally, 4th returns429POST /v1/invites/*/accept— send 11 rapid requests → 10th + returns429429response includesRetry-Afterheader with a numeric valueTesting challenges I want to flag:
Thread.sleep().MockHttpServletRequest.setRemoteAddr()should work.E2E consideration: Rate limiting should not be verified in E2E tests — that's the integration layer's job. But E2E tests should be written to not accidentally trigger rate limits (e.g., don't call login 10 times in a test without thinking about it).
🔒 Sable — Security Engineer
This is the foundational defense layer. Without it, issues #2 (invite brute force) and the credential stuffing vector are wide open. Let me add the threat model precision and implementation security requirements.
Why all three endpoints are critical:
/v1/auth/login— credential stuffing and password brute force. Automated tools (Hydra, Burp Intruder) can attempt thousands of login combinations per second. Without rate limiting, a leaked credential list becomes a working attack./v1/auth/signup— account spam and resource exhaustion. Spamming signup creates junk accounts (household pollution, DB growth) and, combined with issue #3's orphaned session bug, is a DoS vector on the session store./v1/invites/*/accept— invite code enumeration. Even after fixing issue #2 (UUIDv4), rate limiting here is defense-in-depth. Rate limiting also provides a signal for detecting probing behavior.Security implementation requirements:
IP extraction must be safe from spoofing. If behind a proxy, use
X-Forwarded-For— but only trust it if the request comes from a known proxy IP. Accepting arbitraryX-Forwarded-Forvalues lets attackers rotate IPs with a header change. If in doubt, useRemoteAddronly.Rate limit by IP AND by username for login. Per-IP alone can be bypassed with IP rotation. Per-username rate limiting (e.g., lock out an account after 10 failed attempts regardless of IP) adds a second layer. Username enumeration is a separate concern — the lockout response should be the same whether the account exists or not.
The
429response must not leak information. Don't say "account locked" vs "IP rate limited" differently — same response in both cases to prevent enumeration.Log every 429 event. IP, endpoint, timestamp, username (if applicable, and only if it doesn't reveal account existence). These logs are an early warning system for attacks in progress.
Alert threshold: Consider logging a warning when a single IP exceeds the rate limit by 10x in a single window — that's an active attack, not an accidental retry.
Interaction with other issues: This issue should be resolved before or alongside issues #2 and #3. Rate limiting is the safety net that makes the other fixes meaningful at scale.
🎨 Atlas — UI/UX Designer
Rate limiting is invisible to users when it works — it only becomes visible when they hit a limit. That's the design challenge: communicating a limit clearly without alarming legitimate users.
Error states I need to design:
Login rate limit hit: The login form should show an inline message — not a toast, not a modal. Something like: "Too many login attempts. Please wait 60 seconds before trying again." If there's a
Retry-Afterheader, the message can count down: a subtle countdown timer below the submit button. The form should disable the submit button during the lockout window.Signup rate limit hit: Less common for legitimate users, so a simpler message works: "Too many accounts created from this connection. Please try again later." Don't make it feel accusatory.
Invite accept rate limit hit: This is the most UX-sensitive case. A legitimate user clicks an invite link and gets a 429 — they have no idea what "rate limit" means. The message should be: "Something went wrong. Please try again in a moment." with a retry button. Technical language (429, rate limit, IP) must never surface to the user.
Design principles for error states:
--color-errorfor the message text, not a full red background — it's informational, not catastrophicQuestions: