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%

KA ID iOS : POST /api/ios/auth/exchange (vérification HS256 du ka_token aud=ka-ios + profil hub signé) pour l app native KA

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 18, 2026) parent c047f3c

2 changed files +94 −1

modified src/api/main.py +2 −1
@@ -26,7 +26,7 @@ from fastapi.staticfiles import StaticFiles
26 26 from src.api import reqstats
27 27 from src.api.middleware.logging import RequestLoggingMiddleware
28 28 from src.api.middleware.ratelimit import RateLimitMiddleware
29 from src.api.routes import agent, auth, envelope, health, runs, services, stats
29 +from src.api.routes import agent, auth, iosauth, envelope, health, runs, services, stats
30 30 from src.config import SERVICES, get_settings, verify_node
31 31 from src.database.db import init_db
32 32 from src.utils.logger import get_logger
@@ -84,6 +84,7 @@ app.include_router(runs.router)
84 84 app.include_router(stats.router)
85 85 app.include_router(services.router)
86 86 app.include_router(agent.router)
87 +app.include_router(iosauth.router)
87 88
88 89
89 90 WEB_DIR = Path(__file__).resolve().parent / "web"
added src/api/routes/iosauth.py +92 −0
@@ -0,0 +1,92 @@
1 +# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +# 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") via
4 +# ASWebAuthenticationSession → POST /api/ios/auth/exchange : on VÉRIFIE la
5 +# signature côté serveur (le secret ne quitte jamais le serveur) puis on
6 +# enrichit avec le profil hub (requête signée HMAC), et on renvoie le profil.
7 +from __future__ import annotations
8 +
9 +import base64
10 +import hashlib
11 +import hmac
12 +import json
13 +import os
14 +import time
15 +from typing import Any
16 +
17 +import httpx
18 +from fastapi import APIRouter, HTTPException, Request
19 +
20 +router = APIRouter(prefix="/api/ios/auth", tags=["ios-auth"])
21 +
22 +HUB = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")
23 +CLIENT_ID = "ka-ios"
24 +
25 +
26 +def _secret() -> bytes:
27 + s = os.environ.get("KA_IOS_SSO_SECRET", "")
28 + if not s:
29 + raise HTTPException(503, "KA_IOS_SSO_SECRET absent")
30 + return s.encode()
31 +
32 +
33 +def _b64d(s: str) -> bytes:
34 + return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
35 +
36 +
37 +def _verify_jwt(token: str) -> dict[str, Any]:
38 + try:
39 + h64, p64, s64 = token.split(".")
40 + header = json.loads(_b64d(h64))
41 + payload = json.loads(_b64d(p64))
42 + if header.get("alg") != "HS256":
43 + raise ValueError("alg")
44 + expected = hmac.new(_secret(), f"{h64}.{p64}".encode(), hashlib.sha256).digest()
45 + if not hmac.compare_digest(expected, _b64d(s64)):
46 + raise ValueError("signature")
47 + if payload.get("aud") != CLIENT_ID:
48 + raise ValueError("aud")
49 + if payload.get("iss") != HUB:
50 + raise ValueError("iss")
51 + if float(payload.get("exp", 0)) < time.time():
52 + raise ValueError("exp")
53 + return payload
54 + except HTTPException:
55 + raise
56 + except Exception as exc:
57 + raise HTTPException(401, f"jeton invalide ({exc})")
58 +
59 +
60 +async def _hub_profile(ka_id: str) -> dict[str, Any] | None:
61 + """Profil public/enrichi du hub (requête serveur-à-serveur signée HMAC)."""
62 + ts = str(int(time.time()))
63 + sig = hmac.new(_secret(), f"{CLIENT_ID}.{ka_id}.{ts}".encode(), hashlib.sha256).hexdigest()
64 + try:
65 + async with httpx.AsyncClient(timeout=8) as cx:
66 + r = await cx.get(f"{HUB}/api/sso/profile",
67 + params={"client_id": CLIENT_ID, "ka_id": ka_id, "ts": ts, "sig": sig})
68 + if r.status_code == 200:
69 + return r.json()
70 + except Exception:
71 + pass
72 + return None
73 +
74 +
75 +@router.post("/exchange")
76 +async def exchange(request: Request):
77 + body = await request.json()
78 + token = str(body.get("ka_token") or "")
79 + if not token:
80 + raise HTTPException(400, "ka_token manquant")
81 + claims = _verify_jwt(token)
82 + ka_id = claims.get("ka_id") or claims.get("sub")
83 + profile = await _hub_profile(str(ka_id)) if ka_id else None
84 + return {
85 + "ok": True,
86 + "ka_id": ka_id,
87 + "name": claims.get("name"),
88 + "email": claims.get("email"),
89 + "picture": claims.get("picture"),
90 + "provider": claims.get("provider"),
91 + "profile": profile,
92 + }
93