API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com
Python 60.9%
HTML 21%
TypeScript 7.3%
JavaScript 5.2%
CSS 4.8%
Shell 0.8%
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