SPB Git

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

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 (Aug 18, 2026) parent c047f3c Browse files

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
Menu

Theme