Comment un serveur MCP appelle votre API au nom des utilisateurs : une stratégie de jeton en production
Explique la stratégie de jeton sortant pour les serveurs MCP : pourquoi le transfert de jetons et le M2M échouent, et comment l’échange de jetons permet de maintenir l’alignement des permissions avec celles des utilisateurs.
YijunDeveloper
Arrêtez de perdre des semaines sur l'authentification des utilisateurs
Lancez des applications sécurisées plus rapidement avec Logto. Intégrez l'authentification des utilisateurs en quelques minutes et concentrez-vous sur votre produit principal.
Comment un serveur MCP appelle votre API au nom des utilisateurs : une stratégie de jeton en production#
Dans notre précédent article, nous avons partagé notre expérience globale de la construction du serveur MCP distant Logto. Cet article décrit en détail la conception de l’architecture et le flux OAuth.
Avec le serveur MCP comme frontière, l’authentification pour un serveur MCP distant comporte deux volets : entrant et sortant.
Entrant : le client MCP (VS Code, Cursor, etc.) se connecte via OAuth, obtient un jeton d’accès, et l’utilise pour accéder à votre serveur MCP
Sortant : lors du traitement des appels d’outil, le serveur MCP sollicite votre propre API métier au nom de l’utilisateur
Le volet entrant est bien défini dans la spécification MCP, avec de nombreuses discussions et implémentations dans la communauté. Nous avons également écrit un guide de mise en œuvre auparavant. Le volet sortant reçoit nettement moins d’attention : le serveur MCP ne dispose que d’un jeton pour accéder à lui-même. Comment peut-il appeler votre API métier au nom de l’utilisateur ?
C’est la question avec laquelle nous avons constamment peiné lors du développement du serveur MCP Logto. Logto Cloud est un produit multi-tenant B2B typique : un utilisateur peut appartenir à de multiples tenants et les autorisations proviennent du rôle de l’utilisateur dans chaque tenant. L’IA doit non seulement agir en tant qu’utilisateur mais aussi sélectionner le bon tenant.
Cet article suit notre processus décisionnel réel :
Pourquoi le serveur MCP doit être déployé indépendamment, comme une ressource protégée à part
Deux approches sortantes que nous avons rejetées (transfert de jeton et jeton M2M) et leurs problèmes
Le design final, échange de jetons plus jeton sujet, y compris comment les jetons organisationnels gèrent le multi-tenant
Quelques principes à garder à l’esprit si vous construisez quelque chose de similaire
Le problème central : le double rôle d’un serveur MCP#
Du point de vue OAuth, un serveur MCP distant joue deux rôles en même temps :
Pour le client MCP, il est un serveur de ressources : le client doit présenter un jeton pour y accéder
Pour l’API métier, il est un client : il présente un jeton pour accéder à une ressource tierce
En d’autres termes, le serveur MCP joue un rôle différent de chaque côté, et chaque côté utilise un jeton différent. Le jeton que le client MCP obtient via OAuth a le serveur MCP comme audience, donc l’API métier le rejettera lors de la validation de l’audience.
Ainsi, le volet sortant est fondamentalement un problème de délégation : comment faire pour que le serveur MCP appelle les API en aval en tant qu’utilisateur, dans le cadre de ses permissions, sans détenir les identifiants de l’utilisateur ?
Décision d’architecture : le serveur MCP comme service autonome#
Intégrer l’endpoint MCP au service API métier, en partageant le même processus et la même pile d’authentification, peut sembler l’option la plus directe. Après réflexion, nous avons choisi le déploiement autonome :
Isolation des risques : Au moment de notre décision, le SDK officiel MCP n’était pas prêt pour la production et le protocole évoluait rapidement (le passage de SSE à Streamable HTTP en est un exemple). Logto est un service IAM, et la disponibilité de la voie de connexion est essentielle. Avec un déploiement autonome, si le serveur MCP tombe en panne, seul le point d’entrée IA devient indisponible. Le service principal reste intact.
Itération indépendante : L’écosystème MCP évolue chaque semaine, et des problèmes de compatibilité client nécessitent parfois des correctifs immédiats, tandis que le service principal suit un processus de publication strict avec des tests de non-régression. Des déploiements isolés empêchent les deux d’être ralentis mutuellement.
Liberté d’exécution : un service autonome peut choisir le runtime qui lui convient le mieux. Le serveur MCP Logto fonctionne sur Cloudflare Workers : sans état, évolue à la demande, gestion quasi nulle. L’option intégrée ne permet pas ce choix.
Le serveur MCP est déployé sur son propre domaine, mcp.logto.io, sans lien privé avec le service principal. Si un jour nous voulons l’ouvrir en open source, rien ne l’en empêchera.
L’option intégrée ne permet pas non plus d’éviter le problème de jeton : l’endpoint MCP et l’API métier partageraient le même identifiant de ressource, le jeton obtenu par le client MCP donnerait donc toutes les permissions API. La question « avec quelles permissions cet appel est-il exécuté ? » se déplace du problème d’échange de jeton inter-services à celui du passage de permissions en mémoire, et le problème demeure. Le déploiement autonome nous force à concevoir explicitement la frontière des permissions, ce qui fait l’objet de la suite de cet article.
Le serveur MCP doit avoir son propre identifiant de ressource#
La discussion sur le sortant commence par une question plus fondamentale : quelle doit être l’audience du jeton obtenu par le client MCP ?
Réutiliser l’identifiant de ressource de l’API métier est l’option la plus directe : l’API de gestion de Logto est déjà une ressource OAuth classique, donc le client MCP pourrait demander son jeton directement et le serveur MCP le transmettrait tel quel. Beaucoup de premières implémentations de serveur MCP ont fait cela.
Conséquence : si l’audience du jeton MCP est l’API métier, la frontière de permissions n’existe plus.
Vos outils MCP soigneusement conçus deviennent décoratifs : le jeton permet d’appeler l’API complète sans passer par vos outils
Le rayon d’explosion d’un jeton compromis passe de « quelques opérations contrôlées par le serveur MCP » à « l’API de gestion complète »
L’audit, le rate limiting, et le scoping de permissions propres au MCP n’ont plus de fondations
Première décision donc : le serveur MCP est une ressource protégée distincte, avec son propre identifiant (https://mcp.logto.io) et ses propres scopes.
Cette décision clarifie la frontière des permissions, et rend la question sortante concrète : le jeton ne permet d’accéder qu’au serveur MCP, alors que doit utiliser le serveur MCP pour appeler l’API ?
La toute première idée vient de l’analogie évidente.
La Console Logto est une SPA. Pour appeler l’API de gestion, c’est simple : l’utilisateur se connecte via OAuth dans le navigateur, obtient un jeton destiné à l’API de gestion, et l’interface l’utilise directement pour appeler l’API.
Donc, le serveur MCP pourrait-il fonctionner comme une autre Console ? Faire demander au client MCP le jeton de l’API métier à la connexion, et le serveur MCP le transfère tel quel :
Cette approche séduit par sa simplicité extrême : le serveur MCP ne fait que relayer les jetons. Mais elle présente d’évidents problèmes :
Premièrement, elle contredit frontalement la décision de frontière des permissions ci-dessus. Le transfert de jeton requiert que le client MCP détienne le jeton de l’API métier, ce que nous avons précisément rejeté.
Deuxièmement, le serveur MCP n’est plus une vraie ressource protégée. L’audience des jetons reçus n’est pas lui-même, donc la validation de l’audience devient inutile et se limite à « vérifier la signature et l’émetteur ». Cela ne correspond pas à ce que la spec MCP préconise (le serveur MCP doit agir en tant que serveur de ressources et se déclarer selon RFC 9728). Cela revient à déguiser un backend en SPA.
Troisièmement, les clients MCP standards ne coopéreront pas. Un client conforme à la spec demande les jetons selon le Protected Resource Metadata, et l’audience sera le serveur MCP. Il n’existe aucun moyen standard de lui faire demander le jeton de l’API métier, donc cette voie ne fonctionne pas côté client.
Si le jeton utilisateur ne fonctionne pas, pourquoi pas l’identité propre du serveur MCP ?
Donnez au serveur MCP une application M2M (machine-to-machine), obtenez un jeton via client credentials, et appelez l’API métier avec. C’est aussi la pratique standard entre services internes.
L’authentification entrante fonctionne aussi : le jeton du client MCP est destiné au serveur MCP, lequel valide normalement, puis agit en utilisant son propre jeton M2M.
Le défaut majeur est que les permissions du jeton M2M n’ont rien à voir avec celles de l’utilisateur :
Dépassement de permissions : les permissions du jeton M2M reflètent « ce que le serveur MCP peut faire », pas « ce que cet utilisateur peut faire ». Un utilisateur en lecture seule pourrait supprimer des applications via MCP, car le jeton M2M donne ce droit
Perte d’identité : l’API aval voit toujours la même application M2M comme appelant, les audits ne peuvent retrouver la vraie personne
Problème de deputy confus : le serveur MCP devient un mandataire à permissions élevées, et n’importe qui peut abuser de ses droits via lui
Le M2M convient aux scénarios sans contexte utilisateur, comme des tâches planifiées ou synchronisations inter-systèmes. Un serveur MCP est différent : chaque appel est initié par un utilisateur précis, il doit donc s’exécuter sous son identité et ses droits.
En synthétisant les leçons des deux approches on obtient les exigences pour un bon design :
Le jeton du client MCP ne doit permettre d’accéder qu’au serveur MCP (cf. approche 1)
Pour les appels sortants, le serveur MCP doit agir en tant qu’utilisateur, avec ses droits réels (cf. approche 2)
Cela pointe vers un mécanisme standard : l’échange de jetons (RFC 8693). Le serveur MCP prend une preuve représentant l’utilisateur et l’échange auprès du serveur d’authentification contre un jeton API en aval. L’identité et les permissions sont préservées lors de l’échange.
Dans Logto, cette « preuve représentant l’utilisateur » vient de la fonctionnalité usurpation d’utilisateur : le jeton sujet. Il s’agit d’un jeton de courte durée que le serveur demande pour un utilisateur précis, signifiant « le prochain échange de jeton se fait depuis cet utilisateur ». L’utilisateur n’a rien à configurer, tout est automatisé. Le jeton sujet est éphémère, usage unique, et expire après utilisation.
Il y a quatre rôles dans l’architecture. Le MCP serveur est déployé indépendamment sur mcp.logto.io :
Le flux complet derrière un appel d’outil :
Étape par étape :
① Validation entrante. Le client MCP appelle un outil avec le jeton utilisateur. L’audience du jeton est l’identifiant du serveur MCP, le scope est mcp:all. Le serveur MCP vérifie la signature, l’émetteur, l’audience, et le scope, puis obtient l’identité utilisateur. Le flux entrant s’arrête là. Ce jeton n’est jamais transmis dans la chaîne sortante.
② Identité service. Le serveur MCP utilise ses propres credentials M2M pour obtenir un jeton d’accès d’un scope dédié, access:mcp:api. Ce scope n’a qu’un seul but : appeler l’endpoint suivant.
③ Demande du jeton sujet, étape clé de toute la chaîne. Le serveur MCP appelle POST /api/mcp/subject-tokens, un endpoint ouvert spécifiquement pour MCP, présentant deux preuves simultanées :
En-tête Authorization : jeton M2M, prouvant « je suis le vrai serveur MCP »
En-tête x-mcp-user-token : jeton utilisateur, prouvant « cet utilisateur m’a autorisé, l’autorisation est toujours valide »
Le Cloud vérifie entièrement le jeton utilisateur : signature, émetteur, expiration, l’audience doit être celle du serveur MCP, scope incluant mcp:all. Le userId vient directement du claim sub du jeton. L’endpoint n’a pas de paramètre utilisateur.
Cette conception empêche toute usurpation via le credential M2M. Si l’endpoint acceptait un userId arbitraire, posséder la credential M2M ferait de vous un superadmin. Avec ce design, le serveur MCP ne peut échanger qu’avec une autorisation utilisateur valide.
Le jeton sujet remis est éphémère et usage unique. L’implémentation ne le met jamais en cache, chaque utilisation demande un nouveau jeton.
④ Échange contre des jetons de travail. Avec le jeton sujet, effectuez un échange de jeton classique pour obtenir deux types de jetons selon le besoin :
Jeton Cloud API : pour les opérations au niveau utilisateur (ex: lister ou créer des tenants)
Jeton orga : limité au tenant choisi. Le jeton est remis au nom de l’utilisateur, permissions précisément celles de son rôle dans ce tenant
⑤ Appel sortant. Appeler l’API métier avec le jeton échangé, puis retourner le résultat au client MCP.
Le contexte multi-tenant est aussi géré dans l’échange : le tenant intervient à l’étape échange, pas à la connexion. list_tenants liste les options avec un jeton Cloud API, l’utilisateur choisit dans la conversation, le serveur MCP échange alors un jeton orga pour le tenant sélectionné. Un endpoint pour tous les tenants, aucun déploiement spécifique, un tenant créé en cours de session est disponible immédiatement.
Regardez chaque point d’échec des approches précédentes, chacun est couvert :
Le rayon d’action du jeton utilisateur est borné (cf. approche 1) : son audience est seulement le serveur MCP. Même s’il fuit, il ne peut accéder qu’aux outils contrôlés
Le jeton M2M n’est pas utilisable à mauvais escient (cf. approche 2) : il ne sert plus à appeler l’API métier directement, il atteste seulement de l’identité service. Pour obtenir un jeton sujet, il faut présenter à la fois le M2M et un jeton utilisateur valide. Plus de deputy confus
Aucune cred utilisateur stockée : les jetons sujets sont demandés à la volée et détruits après usage. Le serveur MCP ne garde rien sauf ses credentials M2M
Révocation connectée : si l’utilisateur révoque l’autorisation MCP, le jeton utilisateur devient invalide, le contrôle x-mcp-user-token échoue, toute la chaîne s’arrête
Permissions alignées utilisateur : les jetons échangés sont remis au nom de l’utilisateur. Un utilisateur en lecture seule reste limité dans MCP, toute escalade échoue côté auth server. Les journaux d’audit mentionnent la vraie identité
Avec du recul, le credential M2M a le bon job : il prouve « qui je suis », mais la capacité d’« agir en tant qu’utilisateur » s’obtient uniquement via une autorisation valide à l’instant T.
Vue d’ensemble, la stratégie de jetons pour serveur MCP distant se résume à quelques principes :
Entrant et sortant sont deux volets séparés, et l’audience du jeton MCP doit être le serveur MCP lui-même
Sortant se gère via échange de jetons : si votre serveur d’auth accepte d’échanger le jeton entrant directement, l’échange standard suffit ; sinon (ex: Logto a besoin d’un jeton sujet), utilisez une option de type usurpation pour démarrer l’échange côté serveur
Le multi-tenant ne change pas le mécanisme : le contexte organisationnel est juste un paramètre lors de l’échange, les produits mono-tenant l’ignorent simplement
L’IA agit au nom de l’utilisateur : les droits du jeton aval doivent être bornés à l’utilisateur initial
Test simple : supposez qu’un jeton MCP coté client fuite. Tout ce qu’un pirate pourra faire est d’appeler les outils contrôlés par le serveur MCP, toujours dans les limites de l’utilisateur. S’il parvient à toucher à l’API complète, la frontière de permissions est rompue.
L’écosystème MCP évolue encore très vite. L’authentification entrante est bien standardisée, tandis que « comment le serveur MCP appelle en aval » dépend encore de chaque équipe. Nous espérons que notre cas pratique pourra servir de référence utile.