Files
BanGUI/backend/app/utils/client_ip.py
Lukas ea4c7c2f85 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>
2026-04-26 12:40:52 +02:00

60 lines
1.9 KiB
Python

"""Utilities for extracting client IP addresses from HTTP requests.
Handles X-Forwarded-For and X-Real-IP headers when behind a reverse proxy (nginx).
Only trusts these headers when the request comes from a known trusted proxy.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from fastapi import Request
def get_client_ip(request: Request, trusted_proxies: list[str] | None = None) -> str:
"""Extract the client IP address from a request.
When the request comes from a trusted proxy, reads the real IP from
X-Forwarded-For or X-Real-IP headers. Otherwise returns the immediate
connection source (request.client.host).
X-Forwarded-For can be spoofed by the client, so we only trust it if
the request comes from a known proxy IP.
Args:
request: The incoming FastAPI request.
trusted_proxies: Optional list of trusted proxy IP addresses. If None,
only uses request.client.host.
Returns:
The best-guess client IP address suitable for rate limiting.
"""
if not request.client:
return "0.0.0.0"
immediate_ip = request.client.host
trusted_proxies = trusted_proxies or []
# If the immediate connection is not from a trusted proxy, use it directly.
if immediate_ip not in trusted_proxies:
return immediate_ip
# Proxy is trusted, check for forwarded headers.
# X-Forwarded-For can contain multiple IPs (client, proxy1, proxy2).
# We use the leftmost (the original client).
forwarded_for = request.headers.get("X-Forwarded-For", "").strip()
if forwarded_for:
# Take the first IP in the list
client_ip = forwarded_for.split(",")[0].strip()
if client_ip:
return client_ip
# Fall back to X-Real-IP
real_ip = request.headers.get("X-Real-IP", "").strip()
if real_ip:
return real_ip
# No forwarded headers found, use immediate connection
return immediate_ip