SPB Git

Repository Public

Commit

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
2 changed files +94 −1 Browse files
modified src/api/main.py +2 −1
@@ -26,7 +26,7 @@ from fastapi.staticfiles import StaticFiles
26 from src.api import reqstats 26 from src.api import reqstats
27 from src.api.middleware.logging import RequestLoggingMiddleware 27 from src.api.middleware.logging import RequestLoggingMiddleware
28 from src.api.middleware.ratelimit import RateLimitMiddleware 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 from src.config import SERVICES, get_settings, verify_node 30 from src.config import SERVICES, get_settings, verify_node
31 from src.database.db import init_db 31 from src.database.db import init_db
32 from src.utils.logger import get_logger 32 from src.utils.logger import get_logger
@@ -84,6 +84,7 @@ app.include_router(runs.router)
84 app.include_router(stats.router) 84 app.include_router(stats.router)
85 app.include_router(services.router) 85 app.include_router(services.router)
86 app.include_router(agent.router) 86 app.include_router(agent.router)
87+app.include_router(iosauth.router)
87 88
88 89
89 WEB_DIR = Path(__file__).resolve().parent / "web" 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