Auth0 + MCP CaptainDNS: unser vollständiger Erfahrungsbericht
Von CaptainDNS
Veröffentlicht am 4. Dezember 2025
- #Auth0
- #MCP
- #OAuth2
- #DNS
- #Architektur

In diesem Erfahrungsbericht zeigen wir, wie wir Auth0 an den MCP-Server von CaptainDNS angeschlossen haben, ohne das Bestehende zu zerstören:
- Ein MCP-Server auf /stream (dev und prod), der JSON-RPC mit MCP-Clients spricht (MCP Inspector, morgen ChatGPT).
- Eine Backend-API, die bereits per Frontend an Auth0 hängt und mit einem neuen MCP-OAuth2-Flow aligned werden musste.
- Bedarf an optionaler Auth: Tools müssen ohne Login laufen, aber die Identität soll genutzt werden, wenn ein Bearer vorliegt.
- Eine eigene Auth0-API für den MCP, ein PRM (
/.well-known/oauth-protected-resource) und das Resource Parameter Compatibility Profile. - Feingranulare Identitätsweitergabe (sub, email, scopes) in profiles und api_requests-Logs, mit der Option, Zugänge später über geschützte Tools zu verschärfen.
1. Kontext: warum Auth0 an den CaptainDNS-MCP hängen?
CaptainDNS hat bereits:
- ein Next.js-Frontend mit Auth0 (klassischer Login, RS256-Tokens für die API),
- eine Backend-API,
- ein Datenmodell, das Profile und Requests speichert.
Der MCP-Server ändert das Bild:
- ein neuer Einstieg in dev, dann prod;
- MCP-Clients (MCP Inspector, später ChatGPT), die über OAuth2 ein Access Token holen wollen, um den MCP-Server aufzurufen;
- die Notwendigkeit, die Identität bis zur Backend-API zu tragen, um Aktionen einem bestehenden Profil zuzuordnen.
Randbedingungen:
- das bestehende Frontend nicht brechen (historische Audience, Tokens im Umlauf);
- optionale Auth für die v0-Tools beibehalten (lookup, propagation, email audit);
- eine saubere Basis für spätere "Auth Pflicht"-Tools legen (Historie, Resolve-Watchlist, Premium-Features etc.).
2. Überblick Auth0 + MCP
Am Ende sieht die Kette so aus:
-
Auth0 (
XXX-prod.eu.auth0.com): -
deklariert zwei APIs:
-
die historische Frontend-API,
-
die MCP-API (dev und prod);
-
gibt RS256-JWT-Access-Tokens für diese Audiences aus.
-
CaptainDNS-MCP-Server:
-
JSON-RPC-Endpoint:
/stream(dev und prod); -
veröffentlicht ein PRM (Protected Resource Metadata) unter
/.well-known/oauth-protected-resource; -
validiert Auth0-Tokens über Issuer und JWKS;
-
ruft die Backend-API auf und reicht
Authorization: Bearer <access_token>plussource-Header (frontend_mcp_anonymousoderfrontend_mcp_authenticated) weiter. -
Backend-API:
-
akzeptiert mehrere Audiences (Frontend + MCP);
-
mappt Auth0-
sub→profiles; -
protokolliert alle Requests in
apirequestsmituser_idundsource. -
MCP-Clients:
-
MCP Inspector in dev,
-
ChatGPT (Connectors) in prod, als OAuth2-MCP-Client.

Ziel: Bei einem tools/call von CaptainDNS:
- anonymer Modus: Request läuft normal;
- authentifizierter Modus: Request wird einem Profil zugeordnet.
3. Eigene Auth0-API für den MCP und Audience-Abgleich
3.1. Auth0-API für MCP: Audience = MCP /stream
Statt die historische Audience wiederzuverwenden, haben wir eine dedizierte Auth0-API für den MCP angelegt:
- in dev:
- audience = XXX-audience-dev;
- in prod:
- audience = XXX-audience-prod;
- alg = RS256;
- Access-Token-Format = JWT (signiert, nicht verschlüsselt).
MCP-Seite:
AUTH0_AUDIENCEzeigt auf diese Audience (dev oder prod);- das PRM (
/.well-known/oauth-protected-resource) wiederholt die Werte inresource,audienceunddefault_audience.
Backend-API:
AUTH0_ALLOWED_AUDIENCESenthält alle akzeptierten Audiences:- die historische Frontend-Audience,
- die MCP-Audience.
3.2. Resource vs. Audience und verschlüsseltes JWE
Erstes Problem mit MCP Inspector:
- der Client sendete nur
resource=<url>in der Authorize-URL; - Auth0 erwartete ein explizites
audience=<url>; - typisches Ergebnis:
- entweder ein verschlüsseltes Access Token (5-teiliges JWE,
alg=dir,enc=A256GCM) unbrauchbar für den MCP, - oder ein Auth0-Fehler ("service not found"), wenn die Audience keiner API entsprach.
Lösung: das Resource Parameter Compatibility Profile in Auth0 aktivieren:
- Auth0 behandelt dann
resource=<url>wieaudience=<url>für Clients, die nurresourcekennen; - das Token ist ein klassisches 3-teiliges RS256-JWT mit
aud= MCP-Resource-URL (/stream).
Damit bekamen wir einen OAuth2-Flow, den MCP Inspector ohne Verhaltensänderung versteht.
4. MCP-PRM (/.well-known/oauth-protected-resource)
Damit MCP-Clients wissen, wie sie OAuth2 gegen den Server fahren, veröffentlicht der MCP ein PRM (Protected Resource Metadata) unter:
/.well-known/oauth-protected-resource
Dieses JSON beschreibt insbesondere:
resource/audience/default_audience;- die in diesem Kontext zu nutzenden OAuth2-Endpoints:
authorization_endpoint,token_endpoint,- optional
registration_endpoint; jwks_uri(über die OpenID-Konfiguration von Auth0);scopes_supported:openid,profile,email,offline_access,- sowie Anwendungsscopes (
captaindns:dns:read,captaindns:email:read, etc.); default_scope:- typischerweise
"openid profile email captaindns:dns:read".
Das PRM ist damit ein OAuth2-Vertrag speziell für den MCP, ergänzend zum standardmäßigen OpenID-Discovery von Auth0.
5. JWT-Validierung im MCP-Server
Steht der OAuth2-Flow, muss der MCP-Server jedes Bearer auf /stream prüfen:
Authorization: Bearer <access_token>lesen (falls vorhanden);- OpenID-Konfiguration von Auth0 holen:
issuer=XXX;jwks_uri(Public Key);- validieren:
- RS256-Signatur via JWKS,
iss= Auth0-Tenant,aud= MCP-Audience (dev oder prod),expnicht abgelaufen.
Zusätzlich kann der MCP-Server:
- die Claim
scopeprüfen (String, z. B."openid profile email captaindns:dns:read"); - prüfen, dass die geforderten Scopes (standard
profile,email) vorhanden sind, wenn direkt auf MCP-Ebene gefiltert werden soll.
Wenn der Bearer ungültig ist:
- im Modus "optionale Auth" gilt der Benutzer als anonym, kein Challenge;
- im Modus "Auth Pflicht" eines Tools (siehe unten) liefert der MCP einen JSON-RPC-Fehler mit
_meta["mcp/www_authenticate"], um den Login clientseitig auszulösen.

6. Identität zur Backend-API durchreichen
Nach JWT-Validierung im MCP muss der Server die Identität zur API weiterreichen:
- Header
Authorization: Bearer <Access Token Nutzer>hinzufügen; - einen
source-Header (oder ähnlich) hinzufügen, typisiert: frontend_mcp_anonymous, wenn kein Bearer oder Bearer ungültig;frontend_mcp_authenticated, wenn Bearer gültig.
MCP-Logs ergänzen ein Event mcp_outbound_api mit:
path=/...;has_bearer=true/false;source=frontend_mcp_*.
In der API validiert das Middleware optional_auth erneut:
- Token (Issuer, Audience ∈
AUTH0_ALLOWED_AUDIENCES, erforderliche Scopes); - wenn das Token gültig ist:
- Request an ein Profil binden (siehe nächster Abschnitt);
- sonst:
- Request als anonym behandeln, Herkunft (
source) bleibt geloggt.
Diese "doppelte Validierung" (MCP + API) hält die API unabhängig vom Aufrufweg (Frontend vs. MCP).
7. Integration mit profiles und api_requests
7.1. Profile anlegen / aktualisieren
Die Backend-API bietet POST /profile, das:
sub(Auth0-Subject) undemailaus den Claims liest;- einen Eintrag in
profileserstellt oder aktualisiert: - Primärschlüssel:
auth0_sub; - aktuelle E-Mail;
created_at,last_seen_at.
Vorbedingung in der API:
emailmuss gesetzt sein, sonst 422 (kein unvollständiges Profil anlegbar).
Gefundener Fehler: Das Access Token für MCP enthielt email standardmäßig nicht.
7.2. Namespacete E-Mail-Claim via Auth0 Action
Um eine nutzbare E-Mail zu garantieren, haben wir eine Auth0 Post-Login Action ergänzt:
exports.onExecutePostLogin = async (event, api) => {
const ns = "NAMESPACE";
if (event.user && event.user.email) {
api.accessToken.setCustomClaim(`${ns}/email`, event.user.email);
}
};
Erklärung:
- Auth0 verlangt, dass Custom Claims im Access Token namespacet sind (URL oder URN);
- die Claim
NAMESPACE.emailwird daher bei jedem Access Token gesetzt, wenn der Nutzer eine E-Mail hat.
Backend-Seite:
/profileliest zuerstemail(falls Standard vorhanden);- sonst Fallback auf
NAMESPACE/email.
Die Kombination beider Quellen hält den Code robust, kompatibel mit unterschiedlichen Token-Typen (Frontend vs. MCP).
7.3. api_requests: Requests dem richtigen user_id zuordnen
Für jeden API-Request erstellt das Logging-Middleware:
- eine Zeile in
api_requests; - wenn es ein
subgibt undprofiles.GetByAuth0ID(sub)existiert: - wird
user_idinapirequestsgesetzt; source(Frontend / MCP / Backend-Job) qualifiziert die Herkunft.

Ergebnis:
- anonyme Requests (MCP ohne Bearer) haben
user_id = NULL,source=frontend_mcp_anonymous; - authentifizierte Requests werden einem Profil zugeordnet,
user_idist gesetzt.
8. Scopes und AUTH0_REQUIRED_SCOPES
8.1. Backend: AUTH0_REQUIRED_SCOPES
Die API nutzt einen Config-Parameter:
AUTH0_REQUIRED_SCOPES(z. B.["profile", "email"]),
um zu prüfen, dass die Claim scope alle geforderten Scopes enthält.
Das Middleware optional_auth:
- wenn ein Bearer vorhanden ist:
- validiert Signatur und Audience;
- prüft, dass
scopealle nötigen Scopes enthält; - sonst Auth als ungültig markieren (
missing required scope), Request aber anonym weiterlassen (keinuser_id). - wenn kein Bearer:
- Request direkt als anonym behandeln.
Daher mussten die MCP-Tokens profile und email enthalten:
- über
scopes_supportedunddefault_scopeim MCP-PRM; - über die
scope-Parameter der MCP-Clients im OAuth2-Flow.
8.2. Keine "magischen" Lösungen in Auth0
Scopes per Auth0 Action hinzuzufügen ist technisch möglich, aber wir haben bevorzugt:
- saubere Konfiguration über:
- das MCP-PRM (
default_scope); - den
/authorize-Request (scope=), - klare Scope-Prüfungen auf API- und MCP-Seite, ohne "Hardcoding" im Tenant.
9. Optionale Auth und geschützte Tools (RequiresAuth)
9.1. Anfangsproblem: alles wurde "Auth Pflicht"
In einer ersten Version schickte der MCP-Server sofort ein Challenge, sobald kein oder ein ungültiger Bearer auftauchte:
_meta["mcp/www_authenticate"]mitrealmund benötigtenscope.
Folge:
- einige MCP-Clients interpretierten das als generellen Fehler,
- obwohl die v0-Tools anonym nutzbar bleiben sollten.
9.2. RequiresAuth-Flag und Dual-Mode
Die Lösung: ein RequiresAuth-Flag auf MCP-Tools:
-
für alle bestehenden Tools (
dns_lookup,dns_propagation,email_auth_audit): -
RequiresAuth = false; -
Verhalten:
-
ohne Bearer: anonyme Ausführung, kein Challenge;
-
mit gültigem Bearer: Ausführung + Profil-Zuordnung.
-
für künftige "Premium"-Tools (
request_history,resolve_watch_list, etc.): -
RequiresAuth = true; -
Verhalten:
-
kein Bearer oder ungültiger Bearer:
-
MCP gibt einen JSON-RPC-Fehler zurück mit:
-
isError: true, -
_meta["mcp/www_authenticate"]gesetzt, um Login zu triggern; -
gültiger Bearer:
-
normale Ausführung mit Profil-Bindung.
So lassen sich geschützte Tools schrittweise einführen, ohne die öffentlichen Tools zu brechen.
9.3. Sicherheits-Metadaten in tools/list
Um für MCP-Clients klar zu sein, zeigen Tools jetzt einen Security-Abschnitt:
- Typ
oauth2; - erforderliche Scopes (für geschützte Tools).
Tests prüfen:
- dass
tools/listdie Security-Infos liefert, - dass geschützte Tools
_meta["mcp/www_authenticate"]zurückgeben, wenn der Bearer fehlt.
10. Mehrere Audiences (Frontend + MCP) in prod handhaben
10.1. Beobachtungen
In Produktion koexistieren zwei Arten von Tokens:
- Frontend-Tokens:
aud = [XXX, YYY];- MCP-Tokens:
aud = "XXX".
In einer Zwischenphase akzeptierte die API nur die MCP-Audience (AUTH0_ALLOWED_AUDIENCES="XXX"), wodurch das Frontend brach (Audience-Mismatch).
10.2. Lösung: mehrwertige AUTH0_ALLOWED_AUDIENCES
Die Lösung war, auf API-Seite mehrere Audiences zu erlauben:
AUTH0_ALLOWED_AUDIENCES="XXX, YYY"
Das Middleware:
- splittet den String auf
,; - trimmt Spaces;
- akzeptiert Tokens, deren
aud(String oder Array) mindestens eine erlaubte Audience enthält.
Ergebnis:
- die API akzeptiert sowohl:
- Frontend-Tokens (historische Audience),
- MCP-Tokens (MCP-Audience);
- der Übergang in eine Multi-Client-Welt (Frontend + MCP + weitere) wird sauber gehandhabt.

FAQ: Häufige Fragen zu Auth0 + MCP CaptainDNS
Warum eine eigene Auth0-API für den MCP statt der historischen API-Audience?
Die historische Audience wurde bereits vom Frontend und bestehenden Tokens genutzt. Dieselbe Audience dem MCP zu geben, hätte die Konfiguration unklarer und schwerer auditierbar gemacht.
Mit einer dedizierten Auth0-API für den MCP (dev und prod) erhalten wir eine klare Trennung der Nutzungen, bessere Nachverfolgbarkeit und können den MCP-Vertrag unabhängig vom Frontend weiterentwickeln.
Warum war das erste Token verschlüsselt (5-teiliges JWE) und kein lesbares JWT?
Ohne Resource Parameter Compatibility Profile interpretiert Auth0 manche Flows falsch, in denen nur resource gesendet wird, vor allem wenn keine passende Audience gefunden wird.
In unserem Fall sendete der MCP-Client resource=... ohne explizites audience=.... Auth0 antwortete dann teils mit einem verschlüsselten JWE (5 Teile) oder mit einer Fehlermeldung.
Durch Aktivieren des Resource Parameter Compatibility Profile behandelt Auth0 resource als audience und liefert ein klassisches 3-teiliges RS256-JWT, das der MCP-Server validieren kann.
Wozu dient genau das PRM (/.well-known/oauth-protected-resource) im MCP?
Das PRM ist ein Metadaten-Dokument, das beschreibt, wie ein Client OAuth2 nutzen soll, um auf eine geschützte Ressource zuzugreifen:
- was Resource / Audience ist;
- welche Authorization- und Token-Endpoints zu verwenden sind;
- welche Scopes unterstützt und empfohlen werden.
Für CaptainDNS ermöglicht das PRM Clients wie MCP Inspector oder ChatGPT, automatisch zu entdecken, wie sie ein Auth0-Token für den MCP bekommen, welche Scopes sie anfragen sollen und sich ohne aufwendige manuelle Konfiguration zu integrieren.
Wie lassen sich Scopes managen, ohne bestehende Clients zu brechen?
Vorgehen:
- ein minimales Set erforderlicher Scopes (
profile,email) in der API definieren; - die gesamte Anfrage nicht ablehnen, wenn diese Scopes fehlen, sondern in den anonymen Modus fallen;
- striktes Scheitern (missing scope) nur für MCP-Tools mit
RequiresAuth = true.
So bleiben die v0-Tools nutzbar, ohne die Auth0-Konfiguration bestehender Clients zu ändern, und ein schrittweises Härten für Premium-Tools wird möglich.
Wie stellt man ein MCP-Tool von 'optionaler Auth' auf 'Auth Pflicht' um?
Die Umstellung erfolgt in zwei Schritten:
- das MCP-Tool mit
RequiresAuth = truemarkieren; - MCP-Server-Seite:
- kein oder ungültiger Bearer →
isError=truemit_meta["mcp/www_authenticate"], um den Login auszulösen; - gültiger Bearer und ausreichende Scopes → normale Ausführung.
So lässt sich ein Tool auf "Auth Pflicht" heben, ohne die Backend-API zu ändern; die Security-Logik bleibt im MCP.
Glossar Auth0 + MCP CaptainDNS
MCP (Model Context Protocol)
Protokoll, das standardisiert, wie ein KI-Modell mit externen Tools (APIs, Services) spricht. Bei CaptainDNS ermöglicht es Clients wie MCP Inspector oder ChatGPT, DNS/E-Mail-Tools über einen dedizierten MCP-Server aufzurufen.
PRM (Protected Resource Metadata)
JSON-Dokument, das eine geschützte Ressource (hier, der CaptainDNS-MCP-Server) unter /.well-known/oauth-protected-resource veröffentlicht. Es beschreibt Resource, Audience, Scopes sowie Authorization- und Token-Endpoints.
Issuer (iss)
JWT-Claim, der angibt, wer es ausgestellt hat. MCP-Server und Backend-API prüfen, dass iss dem erwarteten Auth0-Tenant entspricht.
Audience (aud)
Claim, das angibt, für welche Ressource(n) das Token bestimmt ist. Hier: die Frontend-API oder die MCP-API. Die Backend-API muss mehrere Audiences akzeptieren, um verschiedene Clients zu bedienen.
Subject (sub)
Eindeutige Kennung des Nutzers im Auth0-Tenant. Sie ist der Schlüssel, um das Profil in der Tabelle profiles zu finden oder anzulegen.
Scope
Liste von Berechtigungen eines Tokens (z. B. openid profile email captaindns:dns:read). Scopes steuern den Zugriff auf Funktionen, insbesondere auf geschützte MCP-Tools.
JWS vs JWE
- JWS: signiertes JWT (3 Teile), serverseitig lesbar und verifizierbar.
- JWE: verschlüsseltes JWT (5 Teile), das einen Entschlüsselungs-Key braucht. Für den CaptainDNS-MCP nutzen wir RS256-JWS, einfacher via JWKS zu validieren.
Namespaced claim
Custom-Claim im Access Token, dessen Name eine URL/URN ist, von Auth0 gefordert, um Kollisionen mit Standard-Claims zu vermeiden. Beispiel: NAMESPACE zum Speichern der E-Mail in einem MCP-Token.
Resource Parameter Compatibility Profile
Auth0-Option, die resource als audience behandelt für OAuth-Clients, die nur resource kennen. Wichtig, damit MCP Inspector ohne Verhaltensänderung ein gültiges RS256-JWT erhält.
frontend_mcp_anonymous / frontend_mcp_authenticated
Werte des Felds source, das in Logs und der Tabelle api_requests genutzt wird, um anonyme MCP-Requests (kein oder ungültiger Bearer) von authentifizierten MCP-Requests (gültiger Bearer, identifiziertes Profil) zu trennen.
