feat(auth): rate-limit login and refresh to 5/min per IP (M0-08, closes #103)#231
Merged
Conversation
Closes #103. - Add slowapi dependency with in-memory MemoryStorage (appropriate for single uvicorn process; no external dependency required). - New app/rate_limit.py: client_ip() key function (reads X-Forwarded-For stamped by HAProxy; falls back to socket peer in dev/tests), Limiter instance, AUTH_RATE_LIMIT = "5/minute", and a custom 429 handler that returns the API-standard {"detail": ...} body with Retry-After: 60 so the login form shows the real error rather than the generic "Request failed" fallback. - POST /auth/login and POST /auth/refresh decorated with @limiter.limit(). login gains a request: Request parameter (refresh already had one). - HAProxy frontend: http-request set-header X-Forwarded-For %[src] overwrites any client-supplied value with the real source IP, preventing header-spoofing bypass of the limit. Removed now-redundant option forwardfor from be_app (both haproxy.cfg and haproxy.cfg.j2 updated so the reference-comparison test stays green). - tests/conftest.py: autouse _reset_rate_limiter fixture resets in-memory counters around each test to prevent counter leakage across the shared app. - Integration tests: 6th login/refresh → 429 + Retry-After + {"detail"}, per-IP isolation (ip_a exhausted; ip_b still passes), ≤5 boundary check. - Unit tests: client_ip() for single XFF, multi-value XFF, whitespace stripping, no-XFF fallback, empty-XFF fallback. - README.architecture.md: new Authentication & Rate Limiting section covering token design, brute-force limit, IP resolution, and timing-attack mitigation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements issue #103 (M0-08): brute-force protection for the auth endpoints.
POST /auth/loginandPOST /auth/refreshare now rate-limited to 5 requests/minute per client IP using slowapi (in-memory; appropriate for a single uvicorn process).Retry-After: 60and a{"detail": "..."}body (API-standard shape — the login form will surface a real message rather than the generic fallback).X-Forwarded-For: %[src]in the frontend, overwriting any client-supplied value so the limit cannot be bypassed by header spoofing. The now-redundantoption forwardforinbe_appis removed.README.architecture.mdgains an Authentication & Rate Limiting section covering token design, the brute-force limit, IP resolution, and timing-attack mitigation.New files
src/backend/app/rate_limit.pyclient_ip()key func,Limiter, custom 429 handler,AUTH_RATE_LIMITsrc/backend/tests/unit/test_rate_limit.pyclient_ip()(XFF variants, fallback)Test plan
uv run pytest -m "not e2e" -q→ 383 pass, 1 pre-existing failure (test_rendered_haproxy_template_validates_with_haproxy— fails becausecoraza.cfgis absent from the localhaproxy -cenv; unrelated to this change and was failing before).Retry-After+{"detail": ...}10.0.0.1's bucket doesn't affect10.0.0.2client_ip()— single XFF, multi-value XFF (first entry), whitespace stripping, no-XFF fallback, empty-XFF fallbackuv run ruff check app/ tests/→ cleanuv run mypy app/→ cleanDefinition of done checklist (from #103)
🤖 Generated with Claude Code