Files
ObsiGate/docs/features/api-mcp-tokens-107.md
T
2026-09-22 14:48:51 -04:00

3.7 KiB

#107 — Clés API & MCP (panneau de configuration)

Version : 2.15.0 — statut : complété (septembre 2026)

Problème

Pour brancher un client MCP externe (Claude Desktop, Cursor…) ou scripter l'API REST, il fallait soit se connecter et voler le JWT de session de 1 h dans le navigateur, soit générer un token à la main via docker exec (generer_access_token.sh) — sans expiration maîtrisable ni révocation.

Décision : une seule clé pour l'API ET le MCP

Le serveur MCP (/mcp, backend/mcp/server.py::_authenticate) résout l'appelant via la même dépendance get_current_user() que l'API REST. Un jeton HS256 type=access authentifie donc les deux surfaces — il n'y a pas de famille de clés séparée à exposer dans l'UI. C'est dit explicitement dans la section (« la même clé fonctionne pour les deux ») et verrouillé par tests (REST 200 + MCP initialize 200 avec la même clé ; révocation → 401 des deux côtés).

Conception

Store — data/api_tokens.json

{"version": 1, "tokens": {"<jti>": {
  "name": "Claude Desktop", "username": "admin",
  "created_at": 1790000000, "expires_at": 1792592000,
  "expiry_key": "30d", "last_used_at": null
}}}

Le JWT brut n'est jamais persisté : affiché une seule fois à la création, sinon perdu (pattern GitHub). La révocation fonctionne par jti : le JWT présenté est rejeté par is_token_revoked même s'il est encore valide dans sa signature.

Expirations (choix UI)

Clé Durée exp dans le JWT
1d 1 jour iat + 86 400
30d 1 mois iat + 2 592 000
180d 6 mois iat + 15 552 000
365d 1 an iat + 31 536 000
never sans fin aucun claim exp

Plafond : 50 tokens actifs par utilisateur (API_TOKEN_MAX_PER_USER).

Correction induite — révocation longue durée

L'ancien revoked_tokens.json bornait toute entrée à 7 jours ; une clé 1 an révoquée aurait « repris vie » au nettoyage suivant. Le store devient un dict {jti: valid_until} calé sur l'expiration réelle du jeton (« sans fin » → 100 ans). Session tokens inchangés (7 j).

« Dernière utilisation »

get_current_user appelle maybe_touch_api_token(jti) pour les jetons api: true — écriture disque throttlée à 1 h par jti, silencieuse si le dossier est en lecture seule ; ne fait jamais échouer une requête.

Endpoints (/api/auth, auth requise, périmètre = l'appelant)

  • GET /api/auth/tokens → {tokens: [...], expiry_choices: [...]}
  • POST /api/auth/tokens {name, expiry} → {token, ...record} (secret unique)
  • DELETE /api/auth/tokens/{jti} → révocation immédiate API + MCP
  • Audit : config_change / api_token_create|revoke.

UI — panneau Configuration

Nouvelle section #cfg-tokens « 🔑 Clés API & MCP » (après Sécurité) : liste (badge Active/Expirée, créée/expire/dernière utilisation), champ nom + sélecteur d'expiration + Créer, zone secrète en tirets avec Copier, bloc « Utilisation » : header Authorization: Bearer <clé> + exemple de config MCP avec headers sur <base>/mcp. 28 clés i18n FR/EN, SW v24.

Tests — tests/test_api_tokens.py (13)

Création/liste (secret jamais restitué), les 5 durées dont l'absence d'exp pour never, expiration invalide → 400, clé identique acceptée par REST et /mcp, refus MCP anonyme, isolation par utilisateur, révocation → 401 immédiat des deux côtés, survie de la révocation longue durée après reload disque, drapeau expired, migration de format revoked_tokens.json. Suite auth + MCP complète verte (77).

Config MCP externe (exemple)

{"mcpServers": {"obsigate": {
  "url": "http://localhost:2020/mcp",
  "headers": {"Authorization": "***"}
}}}