1. De quoi parle ce cours et ce qu’il ne couvre pas
Ce sera un cours très intéressant, dans lequel nous :
- mettons en place dans notre tête l’image du « triangle de confiance » entre MCP Client, MCP Server et MCP Auth Server — avec l’utilisateur humain qui se tient « au‑dessus » de ce triangle en tant que propriétaire des ressources ;
- détaillons le flux : qui envoie un jeton à qui, où l’utilisateur se connecte et pourquoi le serveur MCP ne voit jamais son mot de passe ;
- relions cela à notre backend Next.js/MCP et à la configuration future de Keycloak/Auth0.
Ce que nous ne faisons pas aujourd’hui :
- nous ne cochons pas des cases dans Keycloak et nous ne configurons pas d’IdP spécifique ;
- nous n’écrivons pas une vérification complète de JWT ni une introspection — ce sont les thèmes des prochains cours (sur l’Auth Server et sur le MCP Server en tant que ressource protégée).
L’objectif maintenant — que vous puissiez prendre une feuille, dessiner des flèches entre ChatGPT, votre serveur et Auth0/Keycloak et expliquer sans hésitation : où est la connexion, où est le jeton, où sont les données.
2. Le triangle de confiance : MCP Client, MCP Server, MCP Auth Server
Commençons par les protagonistes. Le « triangle de confiance » technique est formé par MCP Client, MCP Server et MCP Auth Server ; l’utilisateur (User) — un rôle séparé, le propriétaire des ressources, qui se tient pour ainsi dire au‑dessus de ce triangle et donne son consentement à l’accès. Dans le contexte MCP et Apps SDK, cette architecture est assez strictement formalisée.
Utilisateur (Resource Owner)
C’est la personne de l’autre côté de l’écran. Il/elle :
- ouvre ChatGPT ;
- écrit une requête « montre‑moi mes commandes / mes listes de cadeaux » ;
- accepte de « lier le compte » de votre service à ChatGPT.
L’essentiel : c’est bien lui/elle qui possède les ressources (historique des commandes, profils, listes de cadeaux) et c’est lui/elle qui consent à y donner accès.
MCP Client
Ici, pour nous, c’est :
- ChatGPT avec Apps SDK ;
- parfois — MCP Jam Inspector (pour le débogage).
MCP Client sait :
- lire les métadonnées de votre serveur MCP (via .well-known) ;
- lancer un flux OAuth dans le navigateur de l’utilisateur ;
- stocker et joindre les jetons aux appels des outils MCP.
Il faut garder en tête que MCP Client est un public client. Il ne stocke pas votre client_secret, il communique donc avec l’Auth Server comme une application SPA publique : Authorization Code + PKCE.
MCP Server (Resource Server)
C’est votre backend qui implémente MCP :
- établit une connexion avec ChatGPT ;
- déclare des outils (tools), des ressources, des prompts ;
- pour chaque appel d’outil, regarde l’en‑tête Authorization: Bearer <token> ;
- vérifie le jeton (signature, exp, aud, scope) et, si tout va bien, exécute la logique métier.
Point de principe : le serveur MCP ne gère pas la connexion. Il ne voit pas les mots de passe, n’affiche pas de formulaire de connexion, n’envoie pas de mail « veuillez confirmer votre email ». Il ne fait confiance qu’aux jetons signés cryptographiquement par l’Auth Server.
MCP Auth Server (Authorization Server / IdP)
C’est un service séparé d’authentification et d’autorisation : Keycloak, Auth0, Ory Hydra+Kratos, Okta, Cognito, Azure AD, etc.
Il est responsable de :
- l’UI de connexion (email/mot de passe, SSO, 2FA) ;
- la gestion des comptes utilisateurs ;
- l’émission des jetons (access token, refresh token) ;
- la publication des métadonnées OAuth/OIDC (/authorize, /token, jwks_uri, /registration, etc.).
Pour MCP, il doit prendre en charge OAuth 2.1 pour les public clients (PKCE S256, dynamic client registration, etc.).
Tableau récapitulatif des rôles
| Qui | Ce qu’il fait | Ce qu’il ne fait pas |
|---|---|---|
| User | Saisit l’identifiant/le mot de passe, donne son consentement pour l’accès aux données | Ne communique pas directement avec MCP Server |
| MCP Client (ChatGPT/Jam) | Initie OAuth, stocke le jeton, appelle les MCP tools | Ne vérifie pas les mots de passe, ne vérifie pas la signature du jeton |
| MCP Server | Vérifie les jetons, exécute la logique métier des tools | N’affiche pas de formulaire de connexion, ne stocke pas les mots de passe |
| MCP Auth Server | Connecte l’utilisateur, émet des jetons | Ne connaît pas vos outils MCP ni leur logique métier |
Si, dans votre tête, tout cela se confondait en un « gros serveur qui fait tout » — il est temps de séparer.
3. À quoi ressemble le flux : du « pas de jeton » à l’appel protégé des outils
Regardons maintenant le flux de messages. Dans la spécification MCP, ce processus est appelé « The Flow » : discovery → redirect → code → token → authorized calls.
Étape 0. Tentative d’appeler un outil protégé sans jeton
L’utilisateur écrit : « Montre‑moi mes idées de cadeaux enregistrées ».
ChatGPT, en tant que MCP Client, décide : « pour cela, il faut appeler l’outil getUserGiftLists sur notre serveur MCP ». Il effectue l’appel sans jeton (l’utilisateur ne s’est pas encore connecté).
Votre serveur MCP :
- voit l’absence d’en‑tête Authorization ou un en‑tête incorrect ;
- répond 401 Unauthorized et ajoute l’en‑tête WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource" avec un lien vers les métadonnées de ressource protégée (resource metadata, voir ci‑dessous).
Ça ressemble à peu près à ceci (logique, pas un HTTP complet) :
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource"
ChatGPT voit cet en‑tête et comprend : « ah, la ressource est protégée par OAuth, il faut exécuter le flux OAuth et lier un compte ».
Discovery : .well-known/oauth-protected-resource
Ensuite, MCP Client demande les métadonnées à votre serveur :
GET /.well-known/oauth-protected-resource
Le serveur répond par un document JSON avec l’identifiant de ressource et la liste des serveurs d’autorisation auprès desquels obtenir des jetons.
Exemple minimal (nous affinerons plus tard, l’idée est ce qui compte) :
{
"resource": "https://api.giftgenius.com",
"authorization_servers": [
"https://auth.giftgenius.com"
],
"scopes_supported": ["gifts.read", "gifts.write"]
}
Ici :
- resource — l’ID canonique de votre ressource ; il devra ensuite être utilisé comme audience ou resource lors de l’émission du jeton ;
- authorization_servers — la liste des Auth Servers auprès desquels ChatGPT peut demander un jeton ;
- scopes_supported — les « droits » que votre serveur MCP comprend.
Authorization Request : redirection vers l’Auth Server
Après avoir reçu les métadonnées, MCP Client se rend sur l’Auth Server. Il ouvre un onglet dans le navigateur :
GET https://auth.giftgenius.com/authorize
?response_type=code
&client_id=chatgpt-giftgenius
&redirect_uri=... (URL de redirection du MCP Client)
&code_challenge=...
&code_challenge_method=S256
&scope=openid gifts.read
&resource=https://api.giftgenius.com
L’utilisateur :
- voit un écran de connexion familier (par exemple, Keycloak ou Auth0) ;
- saisit identifiant/mot de passe, passe la 2FA ;
- confirme que ChatGPT peut lire ses listes de cadeaux (scope gifts.read).
Code → Token : échange du code contre un jeton avec PKCE
Après une connexion réussie, l’Auth Server redirige l’utilisateur vers MCP Client avec un code. MCP Client :
- fait un POST vers /token ;
- envoie le code et le code_verifier (qui correspond au code_challenge de l’étape précédente).
L’Auth Server vérifie PKCE : il hache le code_verifier, le compare au code_challenge initial. Si tout est OK et que le client est bien celui qui a commencé le flux, alors :
- il émet un access_token de courte durée (généralement un JWT) ;
- il y indique :
- sub — l’ID de l’utilisateur dans l’Auth Server ;
- aud ou resource — votre serveur MCP ;
- scope — les actions autorisées (gifts.read, openid, etc.).
Authenticated Request : appel d’un outil MCP avec un jeton
MCP Client est maintenant prêt à rappeler votre outil, mais avec l’en‑tête :
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Le serveur MCP :
- vérifie la signature du jeton (via la JWK de l’Auth Server) ou via l’introspection ;
- vérifie l’échéance (exp) ;
- vérifie aud / resource — que le jeton est bien émis pour https://api.giftgenius.com ;
- regarde le scope et décide s’il est permis d’appeler getUserGiftLists.
Ensuite, il va tranquillement dans votre base via un certain userId et retourne les listes de cadeaux personnelles.
Remarquez qu’à ce stade, nous n’avons parlé que du flux réseau : comment le jeton est obtenu et arrive jusqu’au serveur MCP. Il est ensuite important de comprendre comment, à partir de sub et des autres claims du jeton, on obtient un userId concret dans votre base — c’est là que le pont d’identité (identity bridge) entre en jeu.
4. Identity Bridge : comment l’utilisateur ChatGPT devient un userId dans votre base
La partie la plus intéressante de l’architecture — c’est le « pont d’identité » (identity bridge). La spécification MCP souligne explicitement : le serveur MCP ne connaît pas les utilisateurs de ChatGPT, il s’appuie sur les données du jeton émis par l’Auth Server.
Le schéma ressemble à ceci :
flowchart TD User[Utilisateur dans ChatGPT] -->|Login/SSO| Auth[Auth Server] Auth -->|JWT: sub, email, tenant| MCP[MCP Server] MCP -->|userId/tenantId| DB[(Votre base de données)]
Étape par étape, cela ressemble à ceci.
Premièrement, l’Auth Server connaît ses utilisateurs en interne : il a des entités user, email, id, éventuellement tenant, roles. En cas de connexion réussie, il place ces informations dans le jeton (dans les claims) :
{
"sub": "auth0|abc123",
"email": "user@example.com",
"given_name": "Alice",
"https://giftgenius.com/tenant": "tenant-42",
"scope": "openid gifts.read",
"aud": "https://api.giftgenius.com"
}
Deuxièmement, le MCP Server, lors de la vérification du jeton, extrait ces claims et détermine qui c’est dans son monde. Par exemple :
- si sub est déjà présent dans la table User.authProviderId — on prend le userId associé ;
- sinon — on crée un enregistrement local (approvisionnement à la volée) et on établit le lien.
Un petit extrait de code TypeScript côté serveur MCP (simplifié, sans vérification de signature) peut ressembler à ceci :
type TokenClaims = {
sub: string;
email?: string;
scope?: string;
};
async function mapClaimsToUserId(claims: TokenClaims): Promise<string> {
const user = await db.user.findUnique({ where: { authSub: claims.sub } });
if (user) return user.id;
const created = await db.user.create({
data: { authSub: claims.sub, email: claims.email ?? null }
});
return created.id;
}
Troisièmement, avec son propre userId, le serveur MCP récupère tout ce qu’il faut : listes de cadeaux, historique des commandes, paramètres, abonnement.
Ainsi, l’Auth Server devient un « pont » entre le monde extérieur (ChatGPT, Google, SSO) et votre monde interne (customer_id dans la base des commandes).
5. Pourquoi séparer Auth Server et MCP Server
La tentation peut survenir : « et si mon serveur MCP affichait lui‑même la connexion et émettait le jeton ». Formellement, c’est possible (vous pouvez intégrer un mini‑IdP en son sein), mais architecturalement, c’est une mauvaise idée. Les raisons sont très concrètes.
Premièrement, sécurité et scalabilité. Un Auth Server est une machine lourde : 2FA, social login, politiques de mots de passe, blocage de compte, récupération d’accès, audit des connexions, éventuellement des certifications. Réécrire cela dans chaque microservice (chaque serveur MCP) — c’est la voie vers l’enfer et le PCI DSS. Il est bien plus simple de déléguer cela à Keycloak/Auth0 et de simplement vérifier leur jeton.
Deuxièmement, interchangeabilité du client. Aujourd’hui, vous n’avez que ChatGPT. Demain, vous brancherez Claude Desktop, votre front web en Next.js, une application mobile. Tous peuvent utiliser le même Auth Server et le même schéma OAuth 2.1, et votre serveur MCP continuera simplement à vérifier les jetons. Vous n’aurez pas à réécrire la logique métier pour chaque nouveau client.
Troisièmement, propreté du code. Idéalement, le MCP Server :
- sait publier /.well-known/oauth-protected-resource ;
- sait vérifier un jeton Bearer et en extraire userId, scopes, tenant ;
- implémente les outils métier (orders, gifts, profiles).
Toute l’UI de connexion — formulaires, mise en page, logins sociaux — vit dans l’Auth Server et n’encombre pas le backend.
6. À quoi cela ressemble dans notre application pédagogique GiftGenius
Revenons à l’application que nous déroulons tout au long du cours. Supposons que nous ayons :
- une ChatGPT App « GiftGenius » avec widget (Apps SDK) qui sait proposer des cadeaux ;
- un serveur MCP sur Node/Next.js, qui fournit des outils :
- searchGifts — anonyme, ne requiert pas de connexion ;
- getSavedGiftLists — personnel, nécessite une authentification ;
- un Auth Server (plus tard — Keycloak/Auth0), où chaque utilisateur possède un compte.
Scénario utilisateur anonyme et connecté
Si l’utilisateur écrit simplement « trouve un cadeau pour mon frère, 30 ans, fan de jeux de société », notre App peut :
- appeler l’outil anonyme searchGifts ;
- afficher des recommandations dans l’interface.
Dans ce cas :
- pas besoin de jeton ;
- le serveur MCP exécute simplement la requête (par exemple, vers votre catalogue ou une API tierce).
Dès que l’utilisateur dit « enregistre ceci dans mes listes » ou « montre‑moi mes idées enregistrées », le modèle décide d’appeler l’outil protégé getSavedGiftLists. Le serveur répond 401 + WWW-Authenticate avec resource_metadata. ChatGPT lance l’assistant OAuth « Link GiftGenius account », fait passer l’utilisateur par la connexion et obtient un jeton.
Ensuite, à chaque appel protégé :
- le MCP Server voit désormais Authorization: Bearer ... ;
- extrait le userId du jeton ;
- filtre les données selon ce userId.
Grâce à cela, nous pouvons :
- séparer les données des différents utilisateurs ;
- afficher en toute sécurité l’historique des commandes, la liste des favoris ;
- implémenter des fonctions commerce (plus tard dans le cours).
Architecture du backend : middleware + handlers d’outils
Concrètement, dans le code Node/Next.js, cela ressemble souvent à une chaîne : « middleware d’authentification → handler métier de l’outil ». Dans le cours sur l’implémentation des handlers d’outils, nous avons déjà souligné qu’il faut leur transmettre un contexte : user_id, jetons, paramètres.
Un extrait de code peut être le suivant :
// auth-context.ts
export type AuthContext = {
userId: string | null; // null pour les appels anonymes
scopes: string[];
};
Un middleware accroché à tous les endpoints MCP :
// mcp-auth-middleware.ts
export async function buildAuthContext(req: Request): Promise<AuthContext> {
const header = req.headers.authorization || "";
const token = header.replace(/^Bearer\s+/i, "");
if (!token) return { userId: null, scopes: [] }; // utilisateur anonyme
const claims = await verifyAndDecodeToken(token); // vérification du jeton
const userId = await mapClaimsToUserId(claims);
const scopes = (claims.scope || "").split(" ");
return { userId, scopes };
}
Et le handler de l’outil reçoit ce contexte :
// tools/getSavedGiftLists.ts
export async function getSavedGiftLists(_args: {}, ctx: AuthContext) {
if (!ctx.userId) throw new Error("User must be authenticated");
return db.giftList.findMany({
where: { ownerId: ctx.userId }
});
}
L’idée est que le handler d’outil ne sait rien d’OAuth ni de PKCE. Il travaille simplement avec un userId « évident ». Toute la magie OAuth est cachée en amont : dans le MCP Client et dans l’Auth middleware.
7. Schémas visuels : comment cohabitent Client, Server et Auth
Nous avons déjà détaillé le flux à l’écrit dans la section 3. Parfois, il est plus simple de dessiner une fois que d’expliquer sept fois ; nous montrons donc les mêmes interactions sous forme de deux diagrammes.
Le squelette des interactions (The Triangle of Trust)
flowchart TD U[User] -->|1. Login / Consent| A[MCP Auth Server] U -->|2. Discute| C["MCP Client (ChatGPT)"] C -->|3. OAuth Flow| A C -->|4. Bearer Token| S[MCP Server] S -->|5. Data| C
Le schéma se lit ainsi.
D’abord, l’utilisateur se connecte via l’Auth Server, qui en substance confirme son identité et émet un jeton. MCP Client orchestre ce processus puis utilise le jeton pour s’adresser au serveur MCP. Le serveur MCP ne voit pas l’identifiant/mot de passe, il ne voit que le jeton et décide de ce qui est autorisé.
Flux de la requête à la réponse
sequenceDiagram participant User participant ChatGPT as MCP Client participant Auth as Auth Server participant MCP as MCP Server User->>ChatGPT: "Montre-moi mes listes de cadeaux" ChatGPT->>MCP: callTool(getSavedGiftLists) (sans jeton) MCP-->>ChatGPT: 401 + WWW-Authenticate (resource_metadata) ChatGPT->>Auth: /authorize + PKCE User->>Auth: Saisit identifiant/mot de passe, donne son consentement Auth-->>ChatGPT: redirect + code ChatGPT->>Auth: /token + code_verifier Auth-->>ChatGPT: access_token (JWT) ChatGPT->>MCP: callTool(getSavedGiftLists) + Authorization: Bearer ... MCP-->>ChatGPT: JSON avec les listes personnelles ChatGPT-->>User: Liste rendue dans le widget
C’est le diagramme que vous devez être capables de « raconter les yeux fermés » à la fin du module.
8. Un peu plus loin : plusieurs ressources, plusieurs clients, DCR
Ce qui est bien avec cette architecture — elle passe à l’échelle.
Premièrement, vous pouvez avoir plusieurs serveurs MCP (par exemple, un pour les cadeaux, un autre pour les commandes) et un Auth Server unique qui émet des jetons avec des aud/resource différents. Chaque serveur de ressources doit vérifier que le jeton lui est réellement destiné, sinon vous obtenez le problème classique du « confused deputy », où un jeton pour un service est accepté par un autre.
Deuxièmement, vous pouvez avoir de nombreux clients :
- ChatGPT App ;
- votre propre front‑end ;
- une application mobile ;
- une intégration partenaire via MCP Gateway.
Tous vont :
- lire /.well-known/oauth-protected-resource ;
- apprendre où se trouve l’Auth Server ;
- suivre le flux OAuth 2.1 ;
- obtenir des jetons et appeler le serveur MCP.
Troisièmement, les Auth Servers modernes prennent de plus en plus en charge le Dynamic Client Registration (DCR) — la possibilité d’enregistrer dynamiquement des clients via API. La spécification MCP suppose précisément une telle possibilité : le client (ChatGPT/Jam) peut s’enregistrer automatiquement sur l’Auth Server via son registration_endpoint.
Dans ce module, il est important de comprendre que :
- le MCP Client, le MCP Server et l’Auth Server communiquent via des documents de discovery standardisés et des jetons ;
- vous n’avez pas besoin de « coder en dur » tous les clients dans le backend ;
- vous pouvez étendre l’écosystème sans casser le modèle d’autorisation existant.
9. Erreurs typiques dans la compréhension de l’architecture d’autorisation MCP
Erreur n° 1 : « le serveur MCP doit connecter lui‑même l’utilisateur ».
Parfois, des développeurs essaient d’intégrer un formulaire de connexion directement dans le serveur MCP, puis d’envoyer l’identifiant/le mot de passe via les outils. Cela casse l’idée même d’OAuth. Le serveur MCP ne doit en aucun cas voir le mot de passe. La connexion et le consentement — c’est la responsabilité de l’Auth Server. Le serveur MCP ne travaille qu’avec des jetons et leurs claims.
Erreur n° 2 : confusion entre MCP Client et MCP Server.
Il arrive que ChatGPT soit perçu comme « une partie de mon backend » et que l’on tente, par exemple, d’y stocker des secrets ou d’attendre qu’il vérifie lui‑même les droits d’accès. En réalité, MCP Client ne fait qu’initier OAuth et joindre des jetons. La vérification du jeton et des droits — c’est la tâche du serveur MCP, pas de ChatGPT.
Erreur n° 3 : « une clé API dans .env au lieu d’OAuth ».
Antipattern classique : créer un gros SERVICE_API_KEY, le mettre dans le .env du serveur MCP et penser que le problème est réglé. Dans ce scénario, il n’y a pas de séparation des droits par utilisateur, on ne peut pas afficher des données personnelles en toute sécurité ni effectuer des achats, tout est fait « au nom du service » et non de l’utilisateur. Cela contredit complètement les objectifs de l’autorisation dans ChatGPT Apps.
Erreur n° 4 : ignorer audience et resource.
Si le serveur MCP accepte n’importe quel JWT valide avec une signature correcte et ne regarde pas aud/resource, alors tout jeton émis pour un autre service par le même Auth Server peut être utilisé pour appeler vos outils. C’est une violation directe du modèle de sécurité OAuth. Le serveur doit vérifier que le jeton est émis pour son resource.
Erreur n° 5 : mélanger la logique d’auth et la logique métier.
Parfois, on commence à faire passer dans les handlers d’outil tout le parsing du jeton, la vérification de signature, le travail avec les JWK, etc. Le code devient alors fragile et difficile à maintenir. Il est bien préférable de séparer la couche « vérification du jeton, mappage vers userId » (middleware) de la couche « logique de l’outil », qui reçoit un AuthContext déjà clair.
Erreur n° 6 : s’attendre à ce que ChatGPT « fasse tout tout seul » sans .well-known.
Sans endpoint correct /.well-known/oauth-protected-resource, le client MCP ne sait tout simplement pas où se trouve votre Auth Server ni quels scopes sont nécessaires. Résultat : le chat « ne sait pas se connecter » en silence, et le développeur scrute longtemps des logs vides. La bonne voie : le serveur MCP annonce clairement ses exigences d’autorisation via .well-known, le client les lit et construit le flux.
Erreur n° 7 : oublier l’utilisateur dans la logique métier.
Parfois, même en configurant correctement OAuth et le mappage du jeton vers le userId, des développeurs n’utilisent pas cela dans les requêtes à la base : par exemple, ils oublient de filtrer sur ownerId = userId. Alors tout utilisateur authentifié peut voir les données d’autrui. La présence d’un jeton n’est que la première étape ; la seconde est toujours l’utilisation correcte du userId et du scope dans le code métier.
GO TO FULL VERSION