SPB Git forge

spb/api-ka

Public

API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com

48commits 1branches 0releases
5.9 MBsize
maindefault branch
19 days agolast push
Python 60.9% HTML 21% TypeScript 7.3% JavaScript 5.2% CSS 4.8% Shell 0.8%
4.6 KB · 128 lines python
Raw Blame History
1# ============================================2# Projet   : API-KA3# Fichier  : src/api/middleware/kaauth.py4# Node     : m3u96b5# Author   : Simon-Pierre Boucher6# Contact  : contact@spboucher.ai7# Date     : 2026-08-238# ============================================9"""Authentification KA ID obligatoire sur l'API produit.1011Deux moyens d'accès :12- une session KA ID (cookie ``apika_session`` — « Se connecter avec KA ID »13  sur www.api-ka.com), pour le playground de la documentation ;14- un jeton personnel ``Authorization: Bearer kapi_…`` généré sur la page15  compte de groupe-ka.com (/compte), vérifié auprès du hub via16  ``/api/sso/token-verify`` (HMAC du secret SSO partagé), cache 5 min.1718Chemins protégés : ``/api/v1/*`` (données produit, runs, search) SAUF19``/api/v1/monitoring/*`` (poll anonyme des gardiens ka2/ka4/ka6) et20``/api/v1/louka/fairvalue/*`` (proxy consommé par l'app iOS KA).21Le reste (``/api/stats/*`` pour les rapports du groupe, ``/api/agent/chat``22pour le widget des 13 sites, ``/api/auth/*``, pages web) reste public.23"""2425from __future__ import annotations2627import hashlib28import hmac29import time3031import httpx32from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint33from starlette.requests import Request34from starlette.responses import JSONResponse, Response3536from src.api.routes.auth import _hub_url, _ka_secret, current_user37from src.config import get_settings3839CLIENT_ID = "api-ka"40PROTECTED_PREFIX = "/api/v1/"41EXEMPT_PREFIXES = ("/api/v1/monitoring", "/api/v1/louka/fairvalue/")42CACHE_TTL = 300.0  # verdict positif43NEG_TTL = 60.0  # verdict négatif (jeton inconnu/révoqué)4445HELP_401 = (46    "Authentification requise. Connectez-vous avec votre KA ID sur "47    "www.api-ka.com, ou envoyez votre jeton personnel (généré sur "48    "www.groupe-ka.com/compte) dans l'en-tête « Authorization: Bearer kapi_… »."49)505152class KaAuthMiddleware(BaseHTTPMiddleware):53    """Exige une session KA ID ou un jeton Bearer valide sur l'API produit."""5455    def __init__(self, app) -> None:  # type: ignore[no-untyped-def]56        super().__init__(app)57        # jeton -> (données hub | None, expiration time.monotonic)58        self._cache: dict[str, tuple[dict | None, float]] = {}5960    async def dispatch(61        self, request: Request, call_next: RequestResponseEndpoint62    ) -> Response:63        path = request.url.path64        if (65            not path.startswith(PROTECTED_PREFIX)66            or path.startswith(EXEMPT_PREFIXES)67            or request.method == "OPTIONS"  # préflight CORS68        ):69            return await call_next(request)7071        # 1) session KA ID (cookie) — le playground du site fonctionne connecté72        user = current_user(request)73        if user:74            request.state.ka_user = user75            return await call_next(request)7677        # 2) jeton personnel Bearer78        auth = request.headers.get("authorization", "")79        token = auth[7:].strip() if auth.lower().startswith("bearer ") else ""80        if token:81            data = await self._verify_token(token)82            if data:83                request.state.ka_user = data84                return await call_next(request)8586        return JSONResponse(87            status_code=401,88            content={89                "success": False,90                "error": HELP_401,91                "meta": {"node": get_settings().node_name},92            },93            headers={"WWW-Authenticate": "Bearer"},94        )9596    async def _verify_token(self, token: str) -> dict | None:97        now = time.monotonic()98        hit = self._cache.get(token)99        if hit and hit[1] > now:100            return hit[0]101        secret = _ka_secret()102        if not secret:103            return None104        ts = str(int(time.time()))105        sig = hmac.new(106            secret, f"{CLIENT_ID}.{token}.{ts}".encode(), hashlib.sha256107        ).hexdigest()108        try:109            async with httpx.AsyncClient(timeout=6.0) as cl:110                r = await cl.get(111                    f"{_hub_url()}/api/sso/token-verify",112                    params={113                        "client_id": CLIENT_ID,114                        "token": token,115                        "ts": ts,116                        "sig": sig,117                    },118                )119            j = r.json() if r.status_code == 200 else {}120            data = j if j.get("valid") else None121        except Exception:122            # hub injoignable : on garde le dernier verdict connu, sinon refus123            return hit[0] if hit else None124        if len(self._cache) > 5000:  # purge grossière anti-croissance125            self._cache.clear()126        self._cache[token] = (data, now + (CACHE_TTL if data else NEG_TTL))127        return data128