# #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` ```json {"version": 1, "tokens": {"": { "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 ` + exemple de config MCP avec headers sur `/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) ```json {"mcpServers": {"obsigate": { "url": "http://localhost:2020/mcp", "headers": {"Authorization": "***"} }}} ```