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%
3.1 KB · 94 lines python
Raw Blame History
1# Auteur : Simon-Pierre Boucher — contact@spboucher.ai2# src/api/routes/iosauth.py — échange de jeton KA ID pour l'app iOS « KA ».3# L'app obtient un ka_token (JWT HS256 du hub, aud="ka-ios") via4# ASWebAuthenticationSession → POST /api/ios/auth/exchange : on VÉRIFIE la5# signature côté serveur (le secret ne quitte jamais le serveur) puis on6# enrichit avec le profil hub (requête signée HMAC), et on renvoie le profil.7from __future__ import annotations89import base6410import hashlib11import hmac12import json13import os14import time15from typing import Any1617import httpx18from fastapi import APIRouter, HTTPException, Request1920router = APIRouter(prefix="/api/ios/auth", tags=["ios-auth"])2122HUB = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")23CLIENT_ID = "ka-ios"24CLIENT_IDS = {"ka-ios", "ka-android"}252627def _secret() -> bytes:28    s = os.environ.get("KA_IOS_SSO_SECRET", "")29    if not s:30        raise HTTPException(503, "KA_IOS_SSO_SECRET absent")31    return s.encode()323334def _b64d(s: str) -> bytes:35    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))363738def _verify_jwt(token: str) -> dict[str, Any]:39    try:40        h64, p64, s64 = token.split(".")41        header = json.loads(_b64d(h64))42        payload = json.loads(_b64d(p64))43        if header.get("alg") != "HS256":44            raise ValueError("alg")45        expected = hmac.new(_secret(), f"{h64}.{p64}".encode(), hashlib.sha256).digest()46        if not hmac.compare_digest(expected, _b64d(s64)):47            raise ValueError("signature")48        if payload.get("aud") not in CLIENT_IDS:49            raise ValueError("aud")50        if payload.get("iss") != HUB:51            raise ValueError("iss")52        if float(payload.get("exp", 0)) < time.time():53            raise ValueError("exp")54        return payload55    except HTTPException:56        raise57    except Exception as exc:58        raise HTTPException(401, f"jeton invalide ({exc})")596061async def _hub_profile(ka_id: str) -> dict[str, Any] | None:62    """Profil public/enrichi du hub (requête serveur-à-serveur signée HMAC)."""63    ts = str(int(time.time()))64    sig = hmac.new(_secret(), f"{CLIENT_ID}.{ka_id}.{ts}".encode(), hashlib.sha256).hexdigest()65    try:66        async with httpx.AsyncClient(timeout=8) as cx:67            r = await cx.get(f"{HUB}/api/sso/profile",68                             params={"client_id": CLIENT_ID, "ka_id": ka_id, "ts": ts, "sig": sig})69            if r.status_code == 200:70                return r.json()71    except Exception:72        pass73    return None747576@router.post("/exchange")77async def exchange(request: Request):78    body = await request.json()79    token = str(body.get("ka_token") or "")80    if not token:81        raise HTTPException(400, "ka_token manquant")82    claims = _verify_jwt(token)83    ka_id = claims.get("ka_id") or claims.get("sub")84    profile = await _hub_profile(str(ka_id)) if ka_id else None85    return {86        "ok": True,87        "ka_id": ka_id,88        "name": claims.get("name"),89        "email": claims.get("email"),90        "picture": claims.get("picture"),91        "provider": claims.get("provider"),92        "profile": profile,93    }94