Fase 0 complete: extract all business logic, auth, database, and 176 endpoints from monolithic app.py into dedicated modules. Structure: - app.py: FastAPI hub (CORS, routers, startup/shutdown) - models.py: 13 Pydantic request models - utils.py: shared utilities (embed status, upload validation, process registries) - config.py: constants, env vars, model catalogs - auth/: crypto, jwt_auth, oidc, rate_limit - database/: db(), init_db(), schema DDL - services/: genai, compliance, chat, terraform, embeddings, cis_reports, mcp - routes/: 13 APIRouter modules (auth, users, oci_config, oci_explorer, genai, mcp, adb, embeddings, reports, chat, terraform, settings, cis_engine) Also: README updated with OCIR auth instructions for manual docker run. 86 tests passing.
94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
"""OIDC helpers — discovery, JWKS, state management, token validation."""
|
|
import json
|
|
import time
|
|
import threading
|
|
|
|
import jwt as pyjwt
|
|
from fastapi import HTTPException
|
|
|
|
from database import db
|
|
from auth.crypto import _safe_dec
|
|
|
|
# ── Caches ───────────────────────────────────────────────────────────────────
|
|
_oidc_discovery_cache: dict = {}
|
|
_oidc_jwks_cache: dict = {}
|
|
_OIDC_CACHE_TTL = 3600
|
|
|
|
_oidc_pending: dict = {}
|
|
_oidc_pending_lock = threading.Lock()
|
|
_OIDC_STATE_TTL = 300
|
|
|
|
|
|
def _get_oidc_config() -> dict:
|
|
keys = ("auth_mode", "oidc_issuer", "oidc_client_id", "oidc_client_secret",
|
|
"oidc_redirect_uri", "oidc_group_admin", "oidc_group_user", "oidc_group_viewer")
|
|
cfg = {}
|
|
with db() as c:
|
|
for k in keys:
|
|
row = c.execute("SELECT value FROM app_settings WHERE key=?", (k,)).fetchone()
|
|
cfg[k] = row["value"] if row else ""
|
|
if cfg.get("oidc_client_secret"):
|
|
cfg["oidc_client_secret"] = _safe_dec(cfg["oidc_client_secret"])
|
|
return cfg
|
|
|
|
|
|
def _oidc_discover(issuer: str) -> dict:
|
|
import requests as req
|
|
now = time.time()
|
|
cached = _oidc_discovery_cache.get(issuer)
|
|
if cached and now - cached[1] < _OIDC_CACHE_TTL:
|
|
return cached[0]
|
|
url = issuer.rstrip("/") + "/.well-known/openid-configuration"
|
|
resp = req.get(url, timeout=10)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
_oidc_discovery_cache[issuer] = (data, now)
|
|
return data
|
|
|
|
|
|
def _oidc_get_jwks(jwks_uri: str) -> list:
|
|
import requests as req
|
|
now = time.time()
|
|
cached = _oidc_jwks_cache.get(jwks_uri)
|
|
if cached and now - cached[1] < _OIDC_CACHE_TTL:
|
|
return cached[0]
|
|
resp = req.get(jwks_uri, timeout=10)
|
|
resp.raise_for_status()
|
|
keys = resp.json().get("keys", [])
|
|
_oidc_jwks_cache[jwks_uri] = (keys, now)
|
|
return keys
|
|
|
|
|
|
def _oidc_store_state(state: str, nonce: str):
|
|
with _oidc_pending_lock:
|
|
now = time.time()
|
|
expired = [k for k, v in _oidc_pending.items() if now - v["created"] > _OIDC_STATE_TTL]
|
|
for k in expired:
|
|
del _oidc_pending[k]
|
|
_oidc_pending[state] = {"nonce": nonce, "created": now}
|
|
|
|
|
|
def _oidc_pop_state(state: str) -> dict | None:
|
|
with _oidc_pending_lock:
|
|
return _oidc_pending.pop(state, None)
|
|
|
|
|
|
def _validate_id_token(id_token: str, issuer: str, client_id: str, nonce: str) -> dict:
|
|
from jwt.algorithms import RSAAlgorithm
|
|
disco = _oidc_discover(issuer)
|
|
jwks = _oidc_get_jwks(disco["jwks_uri"])
|
|
header = pyjwt.get_unverified_header(id_token)
|
|
kid = header.get("kid")
|
|
key_data = None
|
|
for k in jwks:
|
|
if k.get("kid") == kid:
|
|
key_data = k
|
|
break
|
|
if not key_data:
|
|
raise HTTPException(401, "OIDC: chave não encontrada no JWKS")
|
|
public_key = RSAAlgorithm.from_jwk(json.dumps(key_data))
|
|
claims = pyjwt.decode(id_token, public_key, algorithms=["RS256"], audience=client_id, issuer=issuer)
|
|
if claims.get("nonce") != nonce:
|
|
raise HTTPException(401, "OIDC: nonce inválido")
|
|
return claims
|