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": "***"}
}}}