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
Release / build-and-push (push) Successful in 3m12s
This commit is contained in:
+27
-11
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user