"""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