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

@@ -32,6 +32,7 @@ from app.repositories.protocols import (
)
from app.services.geo_cache import GeoCache
from app.utils.constants import SESSION_COOKIE_NAME
from app.utils.rate_limiter import RateLimiter
from app.utils.runtime_state import ApplicationState, RuntimeState
from app.utils.session_cache import NoOpSessionCache, SessionCache
@@ -51,6 +52,7 @@ class ApplicationContext:
runtime_settings: Settings | None
runtime_state: RuntimeState
session_cache: SessionCache | None
login_rate_limiter: RateLimiter
# ---------------------------------------------------------------------------
@@ -76,6 +78,10 @@ def _build_app_context(request: Request) -> ApplicationContext:
if session_cache is None:
session_cache = NoOpSessionCache()
login_rate_limiter: RateLimiter = getattr(state, "login_rate_limiter", None)
if login_rate_limiter is None:
login_rate_limiter = RateLimiter()
return ApplicationContext(
settings=state.settings,
http_session=getattr(state, "http_session", None),
@@ -86,6 +92,7 @@ def _build_app_context(request: Request) -> ApplicationContext:
runtime_settings=getattr(state, "runtime_settings", None),
runtime_state=state.runtime_state,
session_cache=session_cache,
login_rate_limiter=login_rate_limiter,
)
@@ -210,6 +217,13 @@ async def get_session_cache(app_context: Annotated[ApplicationContext, Depends(g
return app_context.session_cache
async def get_login_rate_limiter(
app_context: Annotated[ApplicationContext, Depends(get_app_context)],
) -> RateLimiter:
"""Provide the login endpoint rate limiter from application context."""
return app_context.login_rate_limiter
async def get_session_repo() -> SessionRepository:
"""Provide the concrete session repository implementation.
@@ -410,3 +424,4 @@ Fail2BanDbRepositoryDep = Annotated[Fail2BanDbRepository, Depends(get_fail2ban_d
AppStateDep = Annotated[ApplicationContext, Depends(get_app_state)]
AppDep = Annotated[FastAPI, Depends(get_app)]
AuthDep = Annotated[Session, Depends(require_auth)]
LoginRateLimiterDep = Annotated[RateLimiter, Depends(get_login_rate_limiter)]