Auth0 + MCP CaptainDNS : notre retour d'expérience complet

Par CaptainDNS
Publié le 4 décembre 2025

  • #Auth0
  • #MCP
  • #OAuth2
  • #DNS
  • #Architecture
Schéma d'architecture montrant Auth0, le serveur MCP CaptainDNS, l'API backend et les clients MCP
TL;DR

Dans ce retour d'expérience, nous expliquons comment nous avons branché Auth0 sur le serveur MCP de CaptainDNS sans casser l'existant :

  • Un serveur MCP exposé sur /stream (dev et prod) qui parle JSON-RPC avec les clients (MCP Inspector, demain ChatGPT).
  • Une API backend déjà intégrée à Auth0 côté frontend, qu'il fallait aligner avec un nouveau flux OAuth2 MCP.
  • Un besoin d'auth optionnelle : exécuter les tools sans login, mais exploiter l'identité quand un Bearer est présent.
  • La mise en place d'une API Auth0 dédiée au MCP, d'un PRM (/.well-known/oauth-protected-resource) et du Resource Parameter Compatibility Profile.
  • La propagation fine de l'identité (sub, email, scopes) jusqu'aux profils et aux logs apirequests, avec la capacité de durcir l'accès plus tard via des tools protégés.

1. Contexte : pourquoi intégrer Auth0 au MCP CaptainDNS ?

À la base, CaptainDNS dispose déjà :

  • d'un frontend Next.js relié à Auth0 (login classique, tokens RS256 pour l'API),
  • d'une API backend,
  • d'un modèle de données qui stocke les profils et les requêtes.

L'arrivée du serveur MCP change la donne :

  • un nouveau point d'entrée en dev, puis en prod ;
  • des clients MCP (MCP Inspector, puis ChatGPT) qui veulent récupérer un access token via OAuth2 pour appeler ce serveur MCP ;
  • la nécessité de propager l'identité jusqu'au backend pour rattacher les actions à un profil existant.

Contraintes :

  • ne pas casser l'existant côté frontend (audience historique, tokens déjà en circulation) ;
  • garder l'auth optionnelle sur les tools v0 (lookup, propagation, audit e-mail) ;
  • poser une base saine pour des tools "auth obligatoire" plus tard (historique, surveillances, features premium, etc.).

2. Vue d'ensemble de l'architecture Auth0 + MCP

Au final, la chaîne ressemble à ceci :

  • Auth0 (XXX-prod.eu.auth0.com) :

  • déclare deux APIs :

  • l'API historique frontend,

  • l'API MCP (en dev et prod) ;

  • émet des access tokens JWT RS256 pour ces audiences.

  • Serveur MCP CaptainDNS :

  • endpoint JSON-RPC : /stream (dev et prod) ;

  • expose un PRM (Protected Resource Metadata) sur /.well-known/oauth-protected-resource ;

  • valide les tokens Auth0 via l'issuer et les JWKS ;

  • appelle l'API backend en propageant Authorization: Bearer <access_token> et un header source (frontend_mcp_anonymous ou frontend_mcp_authenticated).

  • API backend :

  • accepte plusieurs audiences (frontend + MCP) ;

  • mappe sub Auth0 → profiles ;

  • journalise toutes les requêtes dans apirequests avec user_id et source.

  • Clients MCP :

  • MCP Inspector en dev,

  • demain ChatGPT (Connectors) en prod, qui se comportera comme un client OAuth2 MCP.

Diagramme d'architecture Auth0 + MCP CaptainDNS

L'objectif est que, pour un tools/call CaptainDNS :

  • en mode anonyme : la requête soit exécutée normalement ;
  • en mode authentifié : la requête soit liée à un profil.

3. API Auth0 dédiée MCP et alignement des audiences

3.1. API Auth0 MCP : audience = MCP /stream

Plutôt que d'essayer de réutiliser l'audience historique, nous avons créé une API Auth0 dédiée pour le MCP :

  • en dev :
  • audience = XXX-audience-dev ;
  • en prod :
  • audience = XXX-audience-prod ;
  • algo = RS256 ;
  • format d'access token = JWT (signé, pas chiffré).

Côté MCP :

  • AUTH0_AUDIENCE pointe sur cette audience (dev ou prod) ;
  • le PRM (/.well-known/oauth-protected-resource) reprend ces valeurs dans resource, audience et default_audience.

Côté API backend :

  • AUTH0_ALLOWED_AUDIENCES contient toutes les audiences acceptées :
  • l'audience historique frontend,
  • l'audience MCP.

3.2. Problème resource vs audience et JWE chiffré

Premier souci avec MCP Inspector :

  • le client passait uniquement resource=<url> dans l'URL d'autorisation ;
  • Auth0, lui, attendait un audience=<url> explicite ;
  • résultat typique :
  • soit un access token chiffré (JWE à 5 segments, alg=dir, enc=A256GCM) inutilisable côté MCP,
  • soit une erreur côté Auth0 ("service not found") si l'audience ne correspondait à aucune API.

La solution a été d'activer le Resource Parameter Compatibility Profile côté Auth0 :

  • Auth0 traite alors resource=<url> comme audience=<url> pour les clients qui ne connaissent que le paramètre resource ;
  • le token obtenu est un JWT RS256 classique à 3 segments, avec aud = URL de la ressource MCP (/stream).

Cette étape a été clé pour obtenir un flux OAuth2 conforme au MCP sans modifier MCP Inspector.

4. PRM MCP (/.well-known/oauth-protected-resource)

Pour que les clients MCP sachent comment parler OAuth2 au serveur, le MCP expose un PRM (Protected Resource Metadata) à l'URL :

  • /.well-known/oauth-protected-resource

Ce document JSON décrit notamment :

  • le resource / audience / default_audience ;
  • les endpoints OAuth2 à utiliser dans ce contexte :
  • authorization_endpoint,
  • token_endpoint,
  • registration_endpoint éventuel ;
  • jwks_uri (via la configuration OpenID d'Auth0) ;
  • scopes_supported :
  • openid, profile, email, offline_access,
  • ainsi que les scopes applicatifs (captaindns:dns:read, captaindns:email:read, etc.) ;
  • default_scope :
  • typiquement "openid profile email captaindns:dns:read".

Le PRM joue donc un rôle de contrat OAuth2 spécifique au MCP, complémentaire du discovery OpenID standard d'Auth0.

5. Validation du JWT côté MCP server

Une fois le flux OAuth2 en place, le serveur MCP doit vérifier chaque Bearer qu'il reçoit sur /stream :

  • lire le header Authorization: Bearer <access_token> (s'il existe) ;
  • récupérer la configuration OpenID d'Auth0 :
  • issuer = XXX ;
  • jwks_uri (clé publique) ;
  • valider :
  • signature RS256 via JWKS,
  • iss = tenant Auth0,
  • aud = audience MCP (dev ou prod),
  • exp non expiré.

En plus, le serveur MCP peut :

  • inspecter la claim scope (string, ex. "openid profile email captaindns:dns:read") ;
  • vérifier que les scopes requis (par défaut profile, email) sont bien présents, si l'on souhaite filtrer dès la couche MCP.

Si le Bearer est invalide :

  • dans le mode "auth optionnelle", le MCP considère simplement l'utilisateur comme anonyme et n'envoie pas de challenge ;
  • dans le mode "auth obligatoire" d'un tool (voir plus bas), le MCP renvoie une erreur JSON-RPC avec un _meta["mcp/www_authenticate"] pour déclencher le login côté client.

Séquence d'authentification MCP avec Auth0

6. Propagation de l'identité vers l'API backend

Une fois le JWT validé côté MCP, le serveur doit propager l'identité vers l'API :

  • ajout du header Authorization: Bearer <access_token utilisateur> ;
  • ajout d'un header source (ou équivalent), typé :
  • frontend_mcp_anonymous si aucun Bearer ou Bearer invalide ;
  • frontend_mcp_authenticated si Bearer valide.

Les logs MCP ajoutent aussi un évènement mcp_outbound_api avec :

  • path=/... ;
  • has_bearer=true/false ;
  • source=frontend_mcp_*.

Côté API, le middleware optional_auth :

  • valide à nouveau le token (issuer, audience ∈ AUTH0_ALLOWED_AUDIENCES, scopes requis) ;
  • si le token est valide :
  • lie la requête à un profil (voir section suivante) ;
  • sinon :
  • considère la requête comme anonyme, tout en loggant l'origine (source).

Ce pattern "double validation" (MCP + API) permet de garder l'API autonome vis-à-vis des modes d'appel (frontend vs MCP).

7. Intégration avec profiles et api_requests

7.1. Création / mise à jour des profils

L'API backend expose un endpoint POST /profile qui :

  • lit sub (subject Auth0) et email dans les claims ;
  • crée ou met à jour un enregistrement dans profiles :
  • clé principale : auth0_sub ;
  • email à jour ;
  • timestamps created_at, last_seen_at.

Précondition côté API :

  • email doit être non vide, sinon 422 (impossible de créer un profil incomplet).

Problème rencontré : l'access token émis pour MCP ne contenait pas email par défaut.

7.2. Claim email namespacée via Action Auth0

Pour garantir la présence d'un email exploitable, nous avons ajouté une Action Post-Login Auth0 :

exports.onExecutePostLogin = async (event, api) => {
  const ns = "NAMESPACE";
  if (event.user && event.user.email) {
    api.accessToken.setCustomClaim(`${ns}/email`, event.user.email);
  }
};

Explication :

  • Auth0 impose que les custom claims d'access token soient namespacées (URL ou URN) ;
  • la claim NAMESPACE.email est donc ajoutée à chaque access token si l'utilisateur a un email.

Côté backend :

  • /profile lit d'abord email (si présent en standard) ;
  • sinon, fallback

La combinaison de ces deux sources permet de garder un code robuste, compatible avec différents types de tokens (frontend vs MCP).

7.3. api_requests : rattacher les requêtes au bon user_id

Pour chaque requête traitée par l'API, le middleware de logging :

  • crée une ligne dans api_requests ;
  • s'il y a un sub et qu'un profil profiles.GetByAuth0ID(sub) existe :
  • renseigne user_id dans apirequests ;
  • utilise source (frontend / MCP / backend job) pour qualifier l'origine.

Flux anonyme vs authentifié côté MCP CaptainDNS

Résultat :

  • les requêtes en mode anonyme (MCP sans Bearer) ont user_id = NULL, source=frontend_mcp_anonymous ;
  • les requêtes authentifiées sont liées à un profil, avec user_id renseigné.

8. Scopes et AUTH0_REQUIRED_SCOPES

8.1. Côté backend : AUTH0_REQUIRED_SCOPES

L'API utilise un paramètre de configuration :

  • AUTH0_REQUIRED_SCOPES (par exemple ["profile", "email"]),

pour vérifier que la claim scope du token contient bien les scopes minimaux.

Le middleware optional_auth :

  • si un Bearer est présent :
  • valide la signature et l'audience ;
  • vérifie que scope contient tous les scopes requis ;
  • sinon, marque l'auth comme invalide (missing required scope), mais laisse la requête continuer en anonyme (pas de user_id).
  • si aucun Bearer :
  • traite directement la requête comme anonyme.

Il a donc été nécessaire de s'assurer que les tokens MCP contiennent bien profile et email :

  • via scopes_supported et default_scope dans le PRM MCP ;
  • via les paramètres scope envoyés par les clients MCP lors du flow OAuth2.

8.2. Éviter les solutions "magiques" côté Auth0

Il est techniquement possible d'ajouter des scopes via une Action Auth0, mais dans ce REX, nous avons privilégié :

  • la configuration propre via :
  • le PRM MCP (default_scope) ;
  • la requête /authorize (scope=),
  • un contrôle clair des scopes côté API et MCP, sans "hardcoding" dans le tenant.

9. Auth optionnelle et tools protégés (RequiresAuth)

9.1. Problème initial : tout devenait "auth obligatoire"

Dans une première version, dès que le serveur MCP détectait l'absence de Bearer ou un Bearer invalide, il renvoyait un challenge :

  • _meta["mcp/www_authenticate"] avec un realm et des scope à obtenir.

Conséquence :

  • certains clients MCP interprétaient cette réponse comme un échec global,
  • alors que l'objectif était de garder les tools v0 utilisables en anonyme.

9.2. Flag RequiresAuth et double mode

La solution a été d'introduire un flag RequiresAuth au niveau des tools MCP :

  • pour tous les tools existants (dns_lookup, dns_propagation, email_auth_audit) :

  • RequiresAuth = false ;

  • comportement :

  • sans Bearer : exécution anonyme, pas de challenge ;

  • avec Bearer valide : exécution + rattachement user.

  • pour les futurs tools "premium" (request_history, resolve_watch_list, etc.) :

  • RequiresAuth = true ;

  • comportement :

  • si pas de Bearer ou Bearer invalide :

  • le MCP renvoie une erreur JSON-RPC avec :

  • isError: true,

  • _meta["mcp/www_authenticate"] renseigné pour déclencher l'UI de login ;

  • si Bearer valide :

  • exécution normale avec rattachement user.

Ce mécanisme permet d'introduire progressivement des tools protégés sans casser l'expérience des tools publics.

9.3. Security metadata dans tools/list

Pour être explicite vis-à-vis des clients MCP, les tools exposent maintenant une section de sécurité :

  • type oauth2 ;
  • scopes requis (pour les tools protégés).

Des tests ont été ajoutés pour vérifier :

  • que tools/list remonte bien les informations de sécurité,
  • que les tools protégés renvoient _meta["mcp/www_authenticate"] en absence de Bearer.

10. Gérer plusieurs audiences (frontend + MCP) en prod

10.1. Constats

En production, deux types de tokens coexistent :

  • tokens frontend :
  • aud = [XXX, YYY] ;
  • tokens MCP :
  • aud = "XXX".

Dans une phase intermédiaire, l'API n'acceptait plus que l'audience MCP (AUTH0_ALLOWED_AUDIENCES="XXX"), ce qui a cassé le frontend (audience mismatch).

10.2. Solution : AUTH0_ALLOWED_AUDIENCES multi-valeurs

La solution a été de permettre plusieurs audiences côté API :

  • AUTH0_ALLOWED_AUDIENCES="XXX, YYY"

Le middleware :

  • découpe la chaîne sur , ;
  • applique un TrimSpace sur chaque entrée ;
  • accepte les tokens dont aud (string ou array) contient au moins une audience autorisée.

Résultat :

  • l'API accepte à la fois :
  • les tokens frontend (audience historique),
  • les tokens MCP (audience MCP) ;
  • la transition vers un monde multi-clients (frontend + MCP + autres) est gérée proprement.

Structure d'un JWT Auth0 pour MCP CaptainDNS

FAQ: Questions fréquentes sur Auth0 + MCP CaptainDNS

Pourquoi créer une API Auth0 dédiée pour le MCP au lieu de réutiliser l'audience API historique ?

L'audience historique était déjà utilisée par le frontend et par des tokens existants. Attribuer la même audience au MCP aurait rendu la configuration plus ambiguë et plus difficile à auditer.

En créant une API Auth0 dédiée pour le MCP (en dev et en prod), nous obtenons une séparation claire des usages, une meilleure traçabilité et la possibilité de faire évoluer le contrat MCP indépendamment du frontend.

Pourquoi le token initial était-il chiffré (JWE à 5 segments) et pas un JWT lisible ?

Sans le Resource Parameter Compatibility Profile, Auth0 interprète mal certains flux où seul resource est envoyé, notamment lorsqu'aucune audience cohérente n'est trouvée.

Dans notre cas, le client MCP envoyait resource=... sans audience=... explicite. Auth0 répondait alors parfois par un token JWE chiffré (5 segments) ou par un message d'erreur.

En activant le Resource Parameter Compatibility Profile, Auth0 traite resource comme audience, ce qui permet d'obtenir un JWT RS256 classique à 3 segments, exploitable par le serveur MCP.

À quoi sert exactement le PRM (/.well-known/oauth-protected-resource) côté MCP ?

Le PRM est un document de métadonnées qui décrit comment un client doit faire de l'OAuth2 pour accéder à une ressource protégée :

  • quelle est la resource / audience ;
  • quels endpoints d'autorisation et de token utiliser ;
  • quels scopes sont supportés et recommandés.

Pour CaptainDNS, le PRM permet à des clients comme MCP Inspector ou ChatGPT de découvrir automatiquement comment obtenir un token Auth0 pour le MCP, de savoir quels scopes demander et de s'intégrer sans configuration manuelle complexe.

Comment gérer les scopes sans casser les clients existants ?

La stratégie retenue est la suivante :

  • définir un ensemble minimal de scopes requis (profile, email) côté API ;
  • ne pas refuser toute la requête si ces scopes manquent, mais basculer en mode anonyme ;
  • réserver l'échec strict (missing scope) aux tools MCP qui ont RequiresAuth = true.

Ainsi, les tools v0 restent utilisables sans changer la configuration d'Auth0 pour les clients existants, tout en permettant un durcissement progressif pour les outils premium plus tard.

Comment basculer un tool MCP de 'auth optionnelle' à 'auth obligatoire' ?

La bascule se fait en deux étapes :

  • marquer le tool MCP avec RequiresAuth = true ;
  • côté serveur MCP :
  • si aucun Bearer ou Bearer invalide → renvoyer une erreur isError=true avec _meta["mcp/www_authenticate"] pour déclencher le login ;
  • si Bearer valide et scopes suffisants → exécuter normalement.

Cette approche permet de faire évoluer un tool en "auth obligatoire" sans modifier l'API backend, en concentrant la logique de sécurité côté MCP.

Glossaire Auth0 + MCP CaptainDNS

MCP (Model Context Protocol)

Protocole qui standardise la façon dont un modèle d'IA dialogue avec des outils externes (APIs, services). Dans CaptainDNS, il permet à des clients comme MCP Inspector ou ChatGPT d'appeler des tools DNS/e-mail via un serveur MCP dédié.

PRM (Protected Resource Metadata)

Document JSON publié par une ressource protégée (ici, le serveur MCP CaptainDNS) à l'URL /.well-known/oauth-protected-resource. Il décrit la resource, l'audience, les scopes, ainsi que les endpoints d'autorisation et de token.

Issuer (iss)

Claim d'un token JWT indiquant qui l'a émis. Le serveur MCP et l'API backend vérifient que le iss correspond bien au tenant Auth0 attendu.

Audience (aud)

Claim qui indique pour quelle(s) ressource(s) le token est destiné. Dans notre cas : l'API frontend ou l'API MCP. Le backend doit accepter plusieurs audiences pour gérer différents clients.

Subject (sub)

Identifiant unique de l'utilisateur dans le tenant Auth0. C'est la clé utilisée pour retrouver ou créer le profil dans la table profiles.

Scope

Liste de permissions associées à un token (ex. openid profile email captaindns:dns:read). Les scopes sont utilisés pour contrôler l'accès à certaines fonctionnalités, notamment les tools MCP protégés.

JWS vs JWE

  • JWS : JWT signé (3 segments), lisible et vérifiable côté serveur.
  • JWE : JWT chiffré (5 segments) qui nécessite une clé de déchiffrement. Pour le MCP CaptainDNS, nous avons choisi d'utiliser des JWS RS256, plus simples à valider via JWKS.

Namespaced claim

Claim custom dans un access token dont le nom est une URL/URN, exigé par Auth0 pour éviter les collisions avec les claims standard. Exemple : NAMESPACE pour stocker l'email dans un token MCP.

Resource Parameter Compatibility Profile

Option Auth0 qui permet de traiter le paramètre resource comme audience pour les clients OAuth qui ne connaissent que resource. Indispensable pour que MCP Inspector obtienne un JWT RS256 correct sans changer son comportement.

frontend_mcp_anonymous / frontend_mcp_authenticated

Valeurs du champ source utilisées dans les logs et dans la table api_requests pour distinguer les requêtes MCP anonymes (pas de Bearer ou Bearer invalide) des requêtes MCP authentifiées (Bearer valide, profil identifié).

Articles similaires

CaptainDNS · 27 novembre 2025

Schéma de l'architecture MCP CaptainDNS entre ChatGPT, le serveur MCP et l'API backend

Dans les coulisses du MCP CaptainDNS

Comment nous avons branché CaptainDNS sur des IA via MCP : architecture, transport HTTP+SSE, JSON-RPC, erreurs 424 et timeouts, et ce que nous avons appris en chemin.

  • #MCP
  • #Architecture
  • #DNS
  • #E-mail
  • #Intégrations IA

CaptainDNS · 21 novembre 2025

Un schéma illustrant une IA qui discute avec CaptainDNS à travers un connecteur MCP standardisé

Un MCP pour CaptainDNS ?

Avant de brancher CaptainDNS sur des IA, il faut comprendre ce qu'est le Model Context Protocol (MCP) et ce qu'il permet réellement. Petit ABC du MCP, puis premières pistes pour CaptainDNS.

  • #MCP
  • #IA
  • #DNS
  • #E-mail
  • #Architecture