Release v1.7.15: security hardening, async auth, CSP tightening, model validation, SSRF guard, rate limiting improvements, frontend extraction, Docker compose security
Release / build-and-push (push) Successful in 3m12s

This commit is contained in:
2026-05-28 14:57:09 +02:00
parent fe95dfcfce
commit f7fca05210
18 changed files with 943 additions and 873 deletions
+27 -11
View File
@@ -1,4 +1,6 @@
import asyncio
import contextvars
import threading
import time
import requests
@@ -20,23 +22,37 @@ from jwt.algorithms import RSAAlgorithm
_auth_context: contextvars.ContextVar[dict | None] = contextvars.ContextVar("auth_context", default=None)
JWKS_CACHE = {"exp": 0, "keys": []}
_jwks_lock = threading.Lock()
logger = structlog.get_logger("aoc.auth")
def _get_jwks():
now = time.time()
if JWKS_CACHE["keys"] and JWKS_CACHE["exp"] > now:
return JWKS_CACHE["keys"]
def _fetch_jwks_blocking() -> list:
"""Fetch JWKS from Microsoft — runs in a thread, never in the event loop."""
oidc = requests.get(
f"https://login.microsoftonline.com/{AUTH_TENANT_ID}/v2.0/.well-known/openid-configuration",
timeout=10,
).json()
jwks_uri = oidc["jwks_uri"]
keys = requests.get(jwks_uri, timeout=10).json()["keys"]
JWKS_CACHE["keys"] = keys
JWKS_CACHE["exp"] = now + 60 * 60 # cache 1h
return keys
return requests.get(jwks_uri, timeout=10).json()["keys"]
def _get_jwks():
now = time.time()
with _jwks_lock:
if JWKS_CACHE["keys"] and JWKS_CACHE["exp"] > now:
return JWKS_CACHE["keys"]
keys = _fetch_jwks_blocking()
JWKS_CACHE["keys"] = keys
JWKS_CACHE["exp"] = now + 60 * 60 # cache 1h
return keys
async def _get_jwks_async() -> list:
"""Non-blocking JWKS fetch: return from cache or refresh in a thread pool."""
now = time.time()
if JWKS_CACHE["keys"] and JWKS_CACHE["exp"] > now:
return JWKS_CACHE["keys"]
return await asyncio.to_thread(_get_jwks)
def _allowed(claims: dict, allowed_roles: set[str], allowed_groups: set[str]) -> bool:
@@ -96,7 +112,7 @@ def user_can_access_privacy_services(claims: dict) -> bool:
return bool(user_roles.intersection(PRIVACY_SERVICE_ROLES))
def require_auth(authorization: str | None = Header(None)):
async def require_auth(authorization: str | None = Header(None)):
if not AUTH_ENABLED:
user = {"sub": "anonymous"}
_auth_context.set(user)
@@ -106,7 +122,7 @@ def require_auth(authorization: str | None = Header(None)):
raise HTTPException(status_code=401, detail="Missing bearer token")
token = authorization.split(" ", 1)[1]
jwks = _get_jwks()
jwks = await _get_jwks_async()
claims = _decode_token(token, jwks)
if not _allowed(claims, AUTH_ALLOWED_ROLES, AUTH_ALLOWED_GROUPS):