CodeGym /Cours /ChatGPT Apps /Pourquoi l’authentification est nécessaire dans ChatGPT A...

Pourquoi l’authentification est nécessaire dans ChatGPT App et brève évolution d’OAuth

ChatGPT Apps
Niveau 10 , Leçon 0
Disponible

1. Pourquoi l’authentification est nécessaire dans ChatGPT App

Commençons par l’essentiel : l’utilisateur dans ChatGPT ≠ l’utilisateur dans votre service.

ChatGPT possède son propre compte utilisateur. Votre service a ses propres userId, tenantId, rôles, facturation, commandes. Il n’y a aucun lien magique entre les deux par défaut. Si vous avez simplement lancé un serveur MCP et décrit quelques tools, ChatGPT les appellera comme un client abstrait.

Rappelons notre exemple d’application GiftGenius — une ChatGPT App qui aide à choisir des cadeaux et à gérer des wishlists. Ce que nous voulons pouvoir faire :

  • Afficher à l’utilisateur ses listes de cadeaux enregistrées.
  • Permettre de marquer les cadeaux comme « achetés » ou « reçus ».
  • Afficher l’historique des commandes (surtout si, ensuite, nous allons vers du commerce/ACP).

Sans authentification, le serveur MCP ne sait absolument pas « qui est-ce ». Au mieux, il voit des identifiants techniques de connexion et un subject anonyme qu’OpenAI fournit pour l’identification et le rate limiting, tout en avertissant explicitement de ne pas l’utiliser pour l’autorisation.

Authentification vs autorisation

Il est très utile de distinguer d’emblée deux notions.

  • L’authentification (AuthN) répond à la question : qui est-ce ?
  • L’autorisation (AuthZ) répond : qu’a le droit de faire ce « quelqu’un » ?

Pour une ChatGPT App, le schéma ressemble à ceci :

  1. D’abord, via OAuth, vous confirmez que l’utilisateur est bien connecté chez votre Identity Provider (IdP) (par exemple, Keycloak/Auth0) et vous obtenez un jeton avec son identifiant. C’est l’authentification.
  2. Ensuite, le serveur MCP lit le jeton, en extrait sub, les rôles et autres claims et décide si cet utilisateur peut appeler un outil particulier (list_orders, delete_profile, etc.). C’est l’autorisation.

Au niveau du code, on peut représenter cela ainsi (simplifié) :

// Type de données que le serveur MCP souhaite connaître à propos de l'utilisateur
export interface AuthContext {
  userId: string;
  roles: string[];
}

// Exemple d'utilisation dans un gestionnaire de tool
async function listGiftLists(auth: AuthContext | null) {
  if (!auth) {
    throw new Error("User is not authenticated");
  }

  // On récupère en base uniquement les listes de cet utilisateur
  return db.giftLists.findMany({ where: { ownerId: auth.userId } });
}

Sans userId et rôles, vous ne pourrez tout simplement pas écrire correctement la logique métier. Tout se transformera en « un grand compte commun pour tout le monde ».

2. Pourquoi « une clé API dans .env » n’est pas une solution

Nous, développeurs, avons un réflexe naturel : « Je vais créer une clé API, la mettre dans .env, et tout fonctionnera ». Et en effet, pour les intégrations interne–interne entre services, les clés API sont un instrument normal. Mais dès que des utilisateurs réels et une ChatGPT App entrent en jeu, l’approche « une seule clé pour tous » se casse.

Regardons un code typique des premiers modules, où nous appelions simplement notre backend depuis MCP :

// mcp/backendClient.ts
export const backendClient = new BackendClient({
  baseUrl: process.env.BACKEND_URL!,
  apiKey: process.env.BACKEND_API_KEY!, // une seule clé pour tout ChatGPT
});

Du point de vue du backend, toutes les requêtes se ressemblent désormais : « c’est l’intégration ChatGPT ». Aucune différence entre Macha et Pacha. Donc :

  • Impossible d’afficher un « espace personnel » — le serveur ne sait pas à qui il appartient.
  • Impossible de séparer les droits : « cet utilisateur peut seulement lire, celui-ci peut aussi acheter ».
  • Impossible d’associer les commandes à une personne dans votre système principal.

Dans le monde MCP, c’est également peu sûr. La spécification recommande d’utiliser une authentification HTTP (Bearer, clés API, etc.) via Streamable HTTP, mais souligne que l’accès complet des utilisateurs à des ressources protégées doit plutôt être construit avec OAuth et des jetons, et non avec une clé de service unique.

De plus, du point de vue de la politique d’OpenAI, une bonne application doit demander uniquement les données réellement nécessaires et donner à l’utilisateur le contrôle de ce qu’il partage avec l’App. Cela s’accorde parfaitement avec le modèle des scopes OAuth, et ne se marie pas du tout avec l’approche « une super-clé qui sait tout faire ».

Pourquoi la clé de service pose problème dans le contexte de ChatGPT

Une clé API de service exprime l’identité du service, pas celle de l’utilisateur. On peut l’utiliser pour signer des appels de votre serveur MCP vers vos services internes ou des API externes (comme l’OpenAI API), mais pas pour dire : « Voici Paul, montre-lui son historique de commandes ».

Anti-exemple le plus simple :

// Mauvaise option : une "illusion" côté utilisateur
async function getMyOrdersFromBackend() {
  // Le serveur MCP appelle /orders/me sur le backend
  const res = await fetch(`${BACKEND_URL}/orders/me`, {
    headers: {
      Authorization: `Bearer ${process.env.BACKEND_API_KEY}`,
    },
  });

  // le backend considère que "me" — c'est un service d'intégration, pas une personne
  return res.json();
}

Même si vous essayez d’insérer artificiellement un userId anonyme dans le corps de la requête, cela restera un « bricolage maison ». Il vous faudra malgré tout :

  • Un moyen fiable de prouver au backend que « voici bien Paul, et pas quelqu’un d’autre ».
  • Un moyen de limiter les droits d’un utilisateur spécifique.
  • Un mécanisme de révocation (revoke) d’accès pour un utilisateur particulier, et non pour tout le monde d’un coup.

C’est là qu’OAuth entre en scène.

3. Mini-glossaire : ce que nous attendons du système de connexion

Avant de plonger dans l’histoire d’OAuth, formulons simplement les exigences d’un système d’authentification « normal » pour une ChatGPT App.

Nous avons besoin d’un mécanisme tel que :

  1. Notre IdP externe (Keycloak, Auth0, Hydra+Kratos, etc.) connaisse l’utilisateur réel : login, email, userId, éventuellement tenant.
  2. Cet IdP délivre un jeton de courte durée, que ChatGPT peut transmettre en toute sécurité au serveur MCP dans l’en-tête HTTP Authorization: Bearer <token>.
  3. Le serveur MCP lit le jeton, vérifie la signature, l’issuer, l’audience, l’expiration et les scopes, en extrait sub (l’identifiant de l’utilisateur) et, sur cette base, fait correspondre l’utilisateur à vos entités (accountId, tenantId).
  4. Ces mêmes scopes permettent de gérer finement les droits : un jeton ne donne que read:gifts, un autre aussi write:gifts ou checkout.
  5. Si le jeton est absent ou n’a pas les bons scopes, le serveur peut renvoyer une erreur avec _meta["mcp/www_authenticate"], afin que ChatGPT affiche une UI d’autorisation et/ou renouvelle le jeton.

Bref, il nous faut un protocole standard et éprouvé, qui sait faire tout cela. Spoiler : c’est OAuth 2.1 (et ses cousins).

4. Brève évolution d’OAuth : des dinosaures à PKCE

Parcourons maintenant l’évolution d’OAuth, sans plonger profondément dans les RFC, mais en comprenant pourquoi les modèles modernes nous intéressent.

OAuth 1.0 / 1.0a : crypto-fitness

Historiquement, OAuth 1.0 est apparu en premier. Il permettait aux sites web de donner à d’autres services l’accès à leurs ressources sans transmettre le mot de passe de l’utilisateur (ce qui est déjà bien). Mais :

  • Les signatures de requêtes étaient complexes : signature HMAC de presque chaque requête, construction de base-strings, normalisation des paramètres.
  • Chaque requête devait être signée, il fallait stocker un consumer secret, savoir former correctement la signature.

La plupart des développeurs modernes n’ont aucune envie de rejouer ces chorégraphies à la main.

La spécification 1.0a a corrigé certaines vulnérabilités, mais la lourdeur générale est restée.

OAuth 2.0 : un framework, pas « un seul protocole »

OAuth 2.0 a grandement simplifié la vie : au lieu d’un seul schéma strict, est apparu un ensemble de flows (authorization code, implicit, resource owner password, client credentials, etc.). Cela a apporté de la flexibilité, mais aussi une diversité d’implémentations.

Avantages :

  • Intégration plus facile pour les SPA, mobiles et applications serveur.
  • Une séparation claire des rôles : Resource Owner, Client, Resource Server, Authorization Server.

Inconvénients :

  • Dans le monde réel, beaucoup de « raccourcis » dangereux sont apparus. Le flow implicit (qui remettait le jeton directement au navigateur sans échange serveur via un code) s’est révélé peu sûr.
  • Le flow password grant (quand le client envoie simplement login/mot de passe de l’utilisateur en échange d’un jeton) contredit la philosophie même d’OAuth — et est devenu un antipattern.

La spécification elle-même a laissé trop de variantes « au choix », d’où l’apparition de nombreuses recommandations et bonnes pratiques, dans des RFC et des billets séparés.

OAuth 2.1 : on se rassemble, on respire, on met de l’ordre

OAuth 2.1 est une tentative de documenter les bonnes pratiques qui s’étaient déjà établies dans la communauté :

  • Accent presque entièrement mis sur le Authorization Code Flow comme option principale.
  • Utilisation obligatoire de PKCE (Proof Key for Code Exchange) pour les clients publics — ceux qui ne peuvent pas stocker un secret (par exemple, applis mobiles, SPA et… clients ChatGPT/MCP).
  • Les flows obsolètes et peu sûrs comme implicit et password grant sont simplement exclus de la spécification.
  • Recommandations pour une courte durée de vie des access tokens et l’utilisation de refresh tokens pour des sessions longues.

Pourquoi est-ce important pour vous ? Parce que l’écosystème autour de MCP et ChatGPT s’aligne clairement sur ces bonnes pratiques : l’Apps SDK et la spécification MCP Authorization exigent explicitement le code d’autorisation + PKCE, des jetons de courte durée et des scopes corrects.

5. Pourquoi, dans l’univers ChatGPT App, nous raisonnons en OAuth 2.1 + PKCE

Maintenant que nous avons le contexte historique, regardons cela à travers le prisme de ChatGPT et MCP.

ChatGPT en tant que public client

ChatGPT (et des clients comme MCP Jam) par rapport à votre Auth Server — c’est un public client typique :

  • Il n’a pas — et ne peut pas avoir — de client_secret stocké de manière fiable.
  • Il s’exécute dans l’infrastructure d’OpenAI, que vous ne contrôlez pas.

Donc le seul choix raisonnable est Authorization Code Flow + PKCE, où la sécurité repose non pas sur le secret client, mais sur la vérification du code challenge et du code verifier.

La documentation officielle de l’Apps SDK indique clairement que ChatGPT, agissant en tant que client MCP, exécute le flow avec Authorization Code + PKCE (S256) et refusera de terminer l’autorisation si votre Authorization Server n’annonce pas la prise en charge de PKCE dans ses métadonnées : code_challenge_methods_supported: ["S256"].

À quoi ressemble le flux du point de vue MCP

Très grossièrement, mais utile à imaginer ainsi (séquence pour une ressource protégée) :

sequenceDiagram
    participant U as Utilisateur
    participant C as ChatGPT (client MCP)
    participant AS as Serveur d’autorisation
    participant RS as Serveur MCP (ressource)

    U->>C: "Montre-moi mes commandes"
    C->>RS: call_tool(list_orders) sans jeton
    RS-->>C: Erreur + _meta["mcp/www_authenticate"]
    C->>AS: Ouvre login/consentement (Authorization Code + PKCE)
    U->>AS: Se connecte et donne son consentement (scopes)
    AS-->>C: Code d’autorisation
    C->>AS: Échange du code contre un Access Token (+ vérification PKCE)
    AS-->>C: Access Token (Bearer)
    C->>RS: call_tool(list_orders) avec Authorization: Bearer <token>
    RS->>RS: Vérification de la signature, de l’issuer, de l’audience et des scopes
    RS-->>C: Liste des commandes de l’utilisateur
    C-->>U: Affiche les données

Le serveur utilise alors :

  • Les métadonnées de ressource protégée (/.well-known/oauth-protected-resource) — il s’y déclare comme ressource et indique quel Authorization Server dessert cette ressource.
  • Le jeton transmis dans l’en-tête Authorization: Bearer <token>, qu’il vérifie soit comme JWT via JWK, soit par introspection auprès de l’Authorization Server.
  • Si le jeton ne correspond pas en audience ou en scopes — le serveur peut rejeter la requête et renvoyer un challenge WWW-Authenticate dans _meta["mcp/www_authenticate"], pour que ChatGPT relance l’autorisation avec les bons paramètres.

Côté code, tout cela reste humain : vous recevez en entrée un AuthContext déjà vérifié et vous travaillez avec.

Mini-exemple : comment un MCP tool distingue un utilisateur anonyme d’un utilisateur authentifié

Sans SDK OAuth précis pour l’instant, juste le concept :

import type { McpToolHandler } from "./types";

export const listOrders: McpToolHandler = async (_args, context) => {
  const auth = context.auth; // supposons que nous mettons ici le résultat de la vérification du jeton

  if (!auth) {
    return {
      content: [{ type: "text", text: "Vous devez vous connecter pour voir les commandes." }],
      _meta: {
        // Challenge pour ChatGPT : lance le flow OAuth
        "mcp/www_authenticate": [
          'Bearer resource_metadata="https://mcp.giftgenius.app/.well-known/oauth-protected-resource", error="insufficient_scope", error_description="Login required to view orders"'
        ]
      },
      isError: true
    };
  }

  const orders = await db.orders.findMany({ where: { userId: auth.userId } });

  return {
    content: [{ type: "text", text: `Commandes trouvées: ${orders.length}` }],
    structuredContent: orders
  };
};

C’est exactement ce type d’indication _meta["mcp/www_authenticate"] qui est décrit dans la documentation officielle de l’Apps SDK comme déclencheur de l’UI OAuth côté ChatGPT.

6. Que signifient « jeton de courte durée, scopes minimaux » en pratique

Des spécifications et guides découlent quelques principes importants qu’il faut garder en tête dès maintenant, avant la prochaine leçon sur la configuration concrète d’un IdP.

Durée de vie courte du jeton

Un access token doit vivre peu de temps. Pourquoi ?

  • S’il fuite, un attaquant sera de toute façon limité dans le temps.
  • Vous pouvez modifier en toute sécurité les droits de l’utilisateur, et, rapidement, le jeton « périme » et un nouveau est demandé.

En général, c’est de l’ordre de quelques minutes ou dizaines de minutes. En échange, vous obtenez des refresh tokens et/ou des autorisations répétées, mais dans le contexte de ChatGPT, la plupart de la routine est gérée côté client.

Les scopes comme moyen de restreindre les droits

Les scopes sont des chaînes comme gifts.read, gifts.write, orders.read, orders.checkout. Ils indiquent précisément les droits de l’utilisateur sur cette ressource.

Pour une ChatGPT App, c’est particulièrement important :

  • Vous pouvez délivrer un jeton uniquement avec gifts.read lorsque l’utilisateur consulte simplement des wishlists.
  • Et pour des opérations ACP/Instant Checkout, il est logique de demander un ensemble de droits plus strict — par exemple, orders.checkout, et de l’afficher explicitement à l’utilisateur.

Dans la description MCP des tools, il est déjà possible de déclarer des securitySchemes avec des scopes spécifiques pour les outils, afin que ChatGPT sache quels droits sont nécessaires pour appeler un tool donné.

Audience : le jeton doit être « pour cette » ressource MCP

Un autre détail important — aud (audience). Le serveur MCP doit vérifier que le jeton a bien été émis pour lui, et non pour un service voisin.

La documentation de l’Apps SDK indique clairement que ChatGPT transmettra le paramètre resource et s’attend à ce que l’Authorization Server le reflète dans le jeton (généralement dans aud), et que le serveur MCP vérifie ce champ.

Il est très probable que, pendant la review de votre application, on lui injecte de faux auth_token pour vérifier l’absence de failles dans votre implémentation de sécurité. Faites donc correctement dès le départ.

7. Comment cela s’applique à notre application GiftGenius

Concentrons-nous de nouveau sur notre App pédagogique. À présent, nous avons à peu près la situation suivante :

  • Il existe un MCP tool get_gift_ideas, qui, à partir de la description du destinataire et du budget, propose des idées de cadeaux. Cela peut fonctionner en anonyme.
  • Il existe un MCP tool save_gift_list, qui enregistre une liste en base. Nous voulons qu’il soit rattaché à un utilisateur spécifique.
  • Il existe un MCP tool list_saved_lists, qui affiche toutes les listes enregistrées par l’utilisateur. Celui-ci nécessite clairement une authentification.

Le widget affiche de jolies cartes de cadeaux, permet de cliquer « enregistrer » et « marquer comme acheté » — tout cela est en fait une interface face à des outils MCP protégés.

Au niveau des types, cela peut ressembler à ceci :

// Typage du contexte d'appel d'un tool (simplifié)
interface ToolContext {
  auth: AuthContext | null;
}

// Exemple d'outil protégé
async function listSavedGiftLists(_input: {}, context: ToolContext) {
  if (!context.auth) {
    // Ici, on utilisera le même mécanisme mcp/www_authenticate que ci-dessus
    throw new Error("Authentication required");
  }

  return db.giftLists.findMany({
    where: { ownerId: context.auth.userId }
  });
}

Et dès que vous écrivez de telles fonctions, cela devient évident : « simplement une clé API dans .env » ne servira à rien. Il faut un AuthContext complet, construit à partir d’un jeton OAuth vérifié.

Quelles parties de l’application peuvent fonctionner en anonyme, et lesquelles non

Un bon exercice avant de configurer OAuth — passer en revue les fonctionnalités et les classer honnêtement en deux catégories.

Par exemple, dans GiftGenius :

Anonyme :

  • Génération d’idées cadeaux à partir d’une description.
  • Affichage d’exemples et mode démo avec données fictives.

Uniquement pour les utilisateurs authentifiés :

  • Consultation et modification des wishlists personnelles.
  • Historique des commandes.
  • Toutes les opérations de paiement, Instant Checkout, liaison avec ACP.

Dans les prochaines leçons, nous allons configurer l’Authorization Server (par exemple, Keycloak ou la paire Hydra+Kratos) et le serveur MCP de sorte que les jetons pour ces actions aient les scopes requis, et que les MCP tools sachent refuser correctement et demander à ChatGPT de se réauthentifier.

8. Erreurs typiques de compréhension de l’authentification dans ChatGPT App

Erreur n° 1 : « ChatGPT connaît déjà l’utilisateur, pourquoi aurais-je besoin de mon propre login ? »
Beaucoup pensent : « ChatGPT a le compte de l’utilisateur, pourquoi ne pas simplement l’utiliser comme userId ? ». Mais ChatGPT ne vous révèle pas la véritable identité de l’utilisateur et ne donne pas accès à ses comptes. Dans les métadonnées MCP, vous ne voyez guère plus qu’un subject anonyme _meta["openai/subject"], destiné au rate limiting et à l’identification de session, avec indication explicite qu’il ne faut pas l’utiliser pour l’autorisation ou l’association à de vrais comptes.

Erreur n° 2 : « Une clé API pour tous — c’est normal, ce n’est qu’une “intégration” »
L’approche « on a intégré dans le serveur MCP une clé API vers notre backend et on est contents » ne fonctionne que pour des scénarios où tous les utilisateurs de ChatGPT partagent un même compte dans votre service. Dès que des données personnelles, du commerce, des ACL apparaissent — vous vous heurtez à l’impossibilité de distinguer les utilisateurs et de gérer leurs droits. Une clé API — c’est l’identité d’un service, pas d’un utilisateur.

Erreur n° 3 : « Implémentons un password grant, c’est le plus simple »
L’habitude de transmettre le login/mot de passe de l’utilisateur à votre backend et de l’échanger contre un jeton (Resource Owner Password Credentials Grant) — c’est un modèle obsolète et peu sûr des débuts d’OAuth 2.0. Dans les recommandations modernes et dans le contexte de OAuth 2.1, c’est un antipattern. Les clients publics comme ChatGPT ne doivent pas voir les mots de passe de vos utilisateurs — c’est précisément le rôle d’Authorization Code + PKCE.

Erreur n° 4 : « PKCE — c’est une complexité inutile, faisons sans »
PKCE (en particulier S256) — ce n’est pas du marketing à la mode, mais un mécanisme obligatoire de protection de l’Authorization Code Flow pour les clients publics. Sans PKCE, un authorization code volé peut être réutilisé. La spécification MCP Authorization et l’Apps SDK indiquent clairement que ChatGPT exige l’annonce du support de PKCE dans les métadonnées de l’Authorization Server et utilise ce mécanisme. Si vous le désactivez, le flow ne fonctionnera tout simplement pas.

Erreur n° 5 : « Demandons tous les scopes possibles — au cas où »
Parfois, on veut créer un jeton avec des droits « ouvre tout et formate C: ». Mais cela viole le principe de minimisation des privilèges (PoLP) et heurte les politiques d’OpenAI comme de la plupart des IdP. Il vaut mieux réfléchir précisément aux scopes réellement nécessaires à votre ChatGPT App : certains pour la lecture, d’autres pour l’écriture, et des scopes séparés pour le commerce. Cela améliore non seulement la sécurité, mais aussi l’UX du consentement : l’utilisateur voit un ensemble de droits clair et limité, et non une liste effrayante de vingt lignes obscures.

Erreur n° 6 : « Le serveur MCP va stocker les logins/mots de passe et dessiner une UI de login »
Le serveur MCP — c’est un Resource Server, pas un Auth Server. Il doit savoir vérifier les jetons, annoncer ses métadonnées .well-known et renvoyer des challenges WWW-Authenticate, mais pas gérer le login ni stocker les mots de passe. Pour le login/consentement, il vaut mieux utiliser un Authorization Server spécialisé (Keycloak, Hydra, Auth0, etc.), comme nous le verrons dans les prochaines leçons.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION