Implement login endpoint rate limiting (TASK-007)

- Add in-memory rate limiter with per-IP deque tracking of attempt timestamps
- Limit login attempts to 5 per 60 seconds per IP, return 429 on excess
- Add Retry-After header to rate limit responses
- Implement IP extraction utility with proxy trust validation (prevent X-Forwarded-For spoofing)
- Integrate rate limiter into auth router and dependencies
- Add 10-second asyncio.sleep on failed login attempts to further slow brute-force
- Add comprehensive tests for rate limiting (9 new tests, all passing)
- Update Features.md to document login rate limiting
- Update Backend-Development.md with rate limiting conventions and design patterns
- Fix test infrastructure issues: update password to meet complexity requirements
- Fix TestValidateSession tests to use Bearer token authentication
- All tests passing: 23 auth tests + full test suite coverage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-26 12:40:52 +02:00
parent 9725714aa2
commit ea4c7c2f85
9 changed files with 414 additions and 73 deletions

View File

@@ -72,6 +72,7 @@ from app.routers import (
setup,
)
from app.startup import startup_shared_resources
from app.utils.rate_limiter import RateLimiter
from app.utils.runtime_state import ApplicationState, RuntimeState
from app.utils.session_cache import InMemorySessionCache, NoOpSessionCache
from app.utils.setup_state import is_setup_complete_cached, set_setup_complete_cache
@@ -159,6 +160,11 @@ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
# deployments, it should be replaced with a shared backend.
_update_session_cache(app, settings)
# Initialize the login rate limiter (5 attempts per 60 seconds per IP).
# This is process-local and not cluster-safe. In multi-worker deployments,
# each worker has independent counters, limiting the blast radius of attacks.
app.state.login_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60)
log.info("bangui_started")
try:
@@ -479,6 +485,10 @@ def create_app(settings: Settings | None = None) -> FastAPI:
if resolved_settings.session_cache_enabled and resolved_settings.session_cache_ttl_seconds > 0.0
else NoOpSessionCache()
)
# Initialize the login rate limiter (5 attempts per 60 seconds per IP).
# This is also re-initialized in the lifespan, but must be present here
# for tests that bypass the lifespan via ASGITransport.
app.state.login_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60)
set_setup_complete_cache(app, False)
# --- CORS ---