This commit is contained in:
2025-10-23 18:10:34 +02:00
parent 5c2691b070
commit 9a64ca5b01
14 changed files with 598 additions and 149 deletions

View File

@@ -33,17 +33,46 @@ class AuthMiddleware(BaseHTTPMiddleware):
- For POST requests to ``/api/auth/login`` and ``/api/auth/setup``
a simple per-IP rate limiter is applied to mitigate brute-force
attempts.
- Rate limit records are periodically cleaned to prevent memory leaks.
"""
def __init__(self, app: ASGIApp, *, rate_limit_per_minute: int = 5) -> None:
def __init__(
self, app: ASGIApp, *, rate_limit_per_minute: int = 5
) -> None:
super().__init__(app)
# in-memory rate limiter: ip -> {count, window_start}
self._rate: Dict[str, Dict[str, float]] = {}
self.rate_limit_per_minute = rate_limit_per_minute
self.window_seconds = 60
# Track last cleanup time to prevent memory leaks
self._last_cleanup = time.time()
self._cleanup_interval = 300 # Clean every 5 minutes
def _cleanup_old_entries(self) -> None:
"""Remove rate limit entries older than cleanup interval.
This prevents memory leaks from accumulating old IP addresses.
"""
now = time.time()
if now - self._last_cleanup < self._cleanup_interval:
return
# Remove entries older than 2x window to be safe
cutoff = now - (self.window_seconds * 2)
old_ips = [
ip for ip, record in self._rate.items()
if record["window_start"] < cutoff
]
for ip in old_ips:
del self._rate[ip]
self._last_cleanup = now
async def dispatch(self, request: Request, call_next: Callable):
path = request.url.path or ""
# Periodically clean up old rate limit entries
self._cleanup_old_entries()
# Apply rate limiting to auth endpoints that accept credentials
if (
@@ -75,7 +104,8 @@ class AuthMiddleware(BaseHTTPMiddleware):
},
)
# If Authorization header present try to decode token and attach session
# If Authorization header present try to decode token
# and attach session
auth_header = request.headers.get("authorization")
if auth_header and auth_header.lower().startswith("bearer "):
token = auth_header.split(" ", 1)[1].strip()
@@ -87,7 +117,9 @@ class AuthMiddleware(BaseHTTPMiddleware):
# Invalid token: if this is a protected API path, reject.
# For public/auth endpoints let the dependency system handle
# optional auth and return None.
if path.startswith("/api/") and not path.startswith("/api/auth"):
is_api = path.startswith("/api/")
is_auth = path.startswith("/api/auth")
if is_api and not is_auth:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Invalid token"}