Auth0 + MCP CaptainDNS: il nostro REX completo
Di CaptainDNS
Pubblicato il 4 dicembre 2025
- #Auth0
- #MCP
- #OAuth2
- #DNS
- #Architettura

In questo ritorno di esperienza spieghiamo come abbiamo collegato Auth0 al server MCP di CaptainDNS senza rompere l'esistente:
- Un server MCP esposto su /stream (dev e prod) che parla JSON-RPC con i client MCP (MCP Inspector, domani ChatGPT).
- Una API backend già legata ad Auth0 via frontend, da allineare con un nuovo flusso OAuth2 MCP.
- Auth opzionale: gli strumenti devono funzionare senza login, ma usare l'identità quando è presente un Bearer.
- Una API Auth0 dedicata al MCP, un PRM (
/.well-known/oauth-protected-resource) e il Resource Parameter Compatibility Profile. - Propagazione fine dell'identità (sub, email, scopes) verso profiles e log api_requests, con la possibilità di irrigidire l'accesso più avanti con tool protetti.
1. Contesto: perché collegare Auth0 al MCP CaptainDNS?
CaptainDNS ha già:
- un frontend Next.js collegato ad Auth0 (login classico, token RS256 per la API),
- una API backend,
- un modello dati che conserva profili e richieste.
L'arrivo del server MCP cambia le carte:
- un nuovo entry point in dev e poi in prod;
- client MCP (MCP Inspector, poi ChatGPT) che vogliono recuperare un access token via OAuth2 per chiamare questo server MCP;
- la necessità di propagare l'identità fino al backend per legare le azioni a un profilo esistente.
Vincoli:
- non rompere l'esistente lato frontend (audience storica, token già in circolo);
- mantenere l'auth opzionale sugli strumenti v0 (lookup, propagation, audit e-mail);
- porre una base sana per futuri tool con "auth obbligatoria" (storico, resolve watch list, feature premium, ecc.).
2. Vista d'insieme Auth0 + MCP
Alla fine la catena assomiglia a questo:
-
Auth0 (
XXX-prod.eu.auth0.com): -
dichiara due API:
-
l'API frontend storica,
-
l'API MCP (dev e prod);
-
emette access token JWT RS256 per queste audiences.
-
Server MCP CaptainDNS:
-
endpoint JSON-RPC:
/stream(dev e prod); -
espone un PRM (Protected Resource Metadata) su
/.well-known/oauth-protected-resource; -
valida i token Auth0 via issuer e JWKS;
-
chiama la API backend propagando
Authorization: Bearer <access_token>e un headersource(frontend_mcp_anonymousofrontend_mcp_authenticated). -
API backend:
-
accetta più audiences (frontend + MCP);
-
mappa
subAuth0 →profiles; -
registra tutte le richieste in
apirequestsconuser_idesource. -
Client MCP:
-
MCP Inspector in dev,
-
ChatGPT (Connectors) in prod, come client OAuth2 MCP.

Obiettivo: per un tools/call CaptainDNS:
- in modalità anonima: la richiesta viene eseguita normalmente;
- in modalità autenticata: la richiesta è collegata a un profilo.
3. API Auth0 dedicata al MCP e allineamento delle audiences
3.1. API Auth0 MCP: audience = MCP /stream
Invece di riutilizzare l'audience storica, abbiamo creato una API Auth0 dedicata per il MCP:
- in dev:
- audience = XXX-audience-dev;
- in prod:
- audience = XXX-audience-prod;
- alg = RS256;
- formato access token = JWT (firmato, non cifrato).
Lato MCP:
AUTH0_AUDIENCEpunta a questa audience (dev o prod);- il PRM (
/.well-known/oauth-protected-resource) riprende questi valori inresource,audienceedefault_audience.
Lato API backend:
AUTH0_ALLOWED_AUDIENCEScontiene tutte le audiences accettate:- l'audience storica frontend,
- l'audience MCP.
3.2. Problema resource vs audience e JWE cifrato
Primo problema con MCP Inspector:
- il client inviava solo
resource=<url>nella URL di autorizzazione; - Auth0 si aspettava un
audience=<url>esplicito; - risultato tipico:
- o un access token cifrato (JWE a 5 parti,
alg=dir,enc=A256GCM) inutilizzabile dal MCP, - o un errore Auth0 ("service not found") se l'audience non corrispondeva a nessuna API.
La soluzione è stata attivare il Resource Parameter Compatibility Profile in Auth0:
- Auth0 tratta quindi
resource=<url>comeaudience=<url>per i client che conoscono soloresource; - il token ottenuto è un JWT RS256 classico a 3 parti con
aud= URL della risorsa MCP (/stream).
Passaggio chiave per ottenere un flusso OAuth2 che MCP Inspector capisca senza cambiare il suo comportamento.
4. PRM MCP (/.well-known/oauth-protected-resource)
Perché i client MCP sappiano come fare OAuth2 verso il server, il MCP espone un PRM (Protected Resource Metadata) all'URL:
/.well-known/oauth-protected-resource
Questo documento JSON descrive in particolare:
- il
resource/audience/default_audience; - gli endpoint OAuth2 da usare in questo contesto:
authorization_endpoint,token_endpoint,- eventuale
registration_endpoint; jwks_uri(via la configurazione OpenID di Auth0);scopes_supported:openid,profile,email,offline_access,- e gli scopes applicativi (
captaindns:dns:read,captaindns:email:read, ecc.); default_scope:- tipicamente
"openid profile email captaindns:dns:read".
Il PRM è quindi un contratto OAuth2 specifico per il MCP, complementare allo standard OpenID discovery di Auth0.
5. Validazione del JWT lato MCP server
Una volta definito il flusso OAuth2, il server MCP deve verificare ogni Bearer ricevuto su /stream:
- leggere
Authorization: Bearer <access_token>(se c'è); - recuperare la configurazione OpenID di Auth0:
issuer=XXX;jwks_uri(chiave pubblica);- validare:
- firma RS256 via JWKS,
iss= tenant Auth0,aud= audience MCP (dev o prod),expnon scaduto.
In più, il server MCP può:
- ispezionare la claim
scope(stringa, es."openid profile email captaindns:dns:read"); - verificare che gli scopes richiesti (di default
profile,email) siano presenti se si vuole filtrare già a livello MCP.
Se il Bearer è invalido:
- in modalità "auth opzionale", il MCP considera l'utente anonimo e non invia challenge;
- in modalità "auth obbligatoria" di un tool (vedi più sotto), il MCP restituisce un errore JSON-RPC con
_meta["mcp/www_authenticate"]per far partire il login lato client.

6. Propagare l'identità verso la API backend
Dopo la validazione del JWT lato MCP, il server deve propagare l'identità verso la API:
- aggiungere l'header
Authorization: Bearer <access_token utente>; - aggiungere un header
source(o equivalente), tipizzato: frontend_mcp_anonymousse non c'è Bearer o è invalido;frontend_mcp_authenticatedse il Bearer è valido.
I log MCP aggiungono anche un evento mcp_outbound_api con:
path=/...;has_bearer=true/false;source=frontend_mcp_*.
Lato API, il middleware optional_auth:
- ri-valida il token (issuer, audience ∈
AUTH0_ALLOWED_AUDIENCES, scopes richiesti); - se il token è valido:
- lega la richiesta a un profilo (vedi sezione successiva);
- altrimenti:
- tratta la richiesta come anonima, registrando comunque l'origine (
source).
Questo schema di "doppia validazione" (MCP + API) mantiene la API indipendente dalla modalità di chiamata (frontend vs MCP).
7. Integrazione con profiles e api_requests
7.1. Creare / aggiornare i profili
La API backend espone POST /profile che:
- legge
sub(subject Auth0) edemaildalle claim; - crea o aggiorna una riga in
profiles: - chiave primaria:
auth0_sub; - email aggiornata;
- timestamp
created_at,last_seen_at.
Precondizione lato API:
emaildeve essere valorizzato, altrimenti 422 (impossibile creare un profilo incompleto).
Problema riscontrato: l'access token emesso per MCP non conteneva email di default.
7.2. Claim email namespacata tramite Action Auth0
Per garantire un email utilizzabile abbiamo aggiunto una Auth0 Post-Login Action:
exports.onExecutePostLogin = async (event, api) => {
const ns = "NAMESPACE";
if (event.user && event.user.email) {
api.accessToken.setCustomClaim(`${ns}/email`, event.user.email);
}
};
Spiegazione:
- Auth0 impone che le custom claim degli access token siano namespaziate (URL o URN);
- la claim
NAMESPACE.emailviene quindi aggiunta a ogni access token se l'utente ha un email.
Backend:
/profilelegge primaemail(se presente di default);- altrimenti fallback su
NAMESPACE/email.
La combinazione di queste due fonti mantiene un codice robusto, compatibile con diversi tipi di token (frontend vs MCP).
7.3. api_requests: collegare le richieste al giusto user_id
Per ogni richiesta trattata dalla API, il middleware di logging:
- crea una riga in
api_requests; - se esiste
sube un profiloprofiles.GetByAuth0ID(sub): - valorizza
user_idinapirequests; - usa
source(frontend / MCP / job backend) per qualificare l'origine.

Risultato:
- le richieste anonime (MCP senza Bearer) hanno
user_id = NULL,source=frontend_mcp_anonymous; - le richieste autenticate sono legate a un profilo, con
user_idvalorizzato.
8. Scope e AUTH0_REQUIRED_SCOPES
8.1. Lato backend: AUTH0_REQUIRED_SCOPES
La API usa un parametro di configurazione:
AUTH0_REQUIRED_SCOPES(per esempio["profile", "email"]),
per verificare che la claim scope del token contenga tutti gli scope richiesti.
Il middleware optional_auth:
- se c'è Bearer:
- valida firma e audience;
- controlla che
scopecontenga tutti gli scope richiesti; - altrimenti marca l'auth come invalida (
missing required scope), ma lascia proseguire la richiesta come anonima (senzauser_id). - se non c'è Bearer:
- tratta direttamente la richiesta come anonima.
Bisognava quindi assicurarsi che i token MCP includessero profile ed email:
- tramite
scopes_supportededefault_scopenel PRM MCP; - tramite i parametri
scopeinviati dai client MCP durante il flow OAuth2.
8.2. Evitare soluzioni "magiche" in Auth0
È tecnicamente possibile aggiungere scope via una Action Auth0, ma in questo REX abbiamo preferito:
- una configurazione pulita via:
- il PRM MCP (
default_scope); - la richiesta
/authorize(scope=), - controlli espliciti degli scope lato API e MCP, senza "hardcoding" nel tenant.
9. Auth opzionale e tool protetti (RequiresAuth)
9.1. Problema iniziale: tutto diventava "auth obbligatoria"
In una prima versione, non appena il server MCP rilevava l'assenza di Bearer o un Bearer invalido, restituiva un challenge:
_meta["mcp/www_authenticate"]con unrealme gliscopeda ottenere.
Conseguenza:
- alcuni client MCP interpretavano la risposta come un fallimento globale,
- mentre l'obiettivo era mantenere gli strumenti v0 usabili in anonimo.
9.2. Flag RequiresAuth e doppio modo
La soluzione è stata introdurre un flag RequiresAuth a livello dei tool MCP:
-
per tutti i tool esistenti (
dns_lookup,dns_propagation,email_auth_audit): -
RequiresAuth = false; -
comportamento:
-
senza Bearer: esecuzione anonima, nessun challenge;
-
con Bearer valido: esecuzione + collegamento al profilo.
-
per i futuri tool "premium" (
request_history,resolve_watch_list, ecc.): -
RequiresAuth = true; -
comportamento:
-
se manca il Bearer o è invalido:
-
il MCP restituisce un errore JSON-RPC con:
-
isError: true, -
_meta["mcp/www_authenticate"]compilato per attivare il login; -
se il Bearer è valido:
-
esecuzione normale con collegamento al profilo.
Questo meccanismo permette di introdurre progressivamente tool protetti senza rompere l'esperienza dei tool pubblici.
9.3. Metadata di sicurezza in tools/list
Per essere espliciti verso i client MCP, i tool espongono ora una sezione di sicurezza:
- tipo
oauth2; - scope richiesti (per i tool protetti).
Sono stati aggiunti test per verificare:
- che
tools/listriporti le informazioni di sicurezza, - che i tool protetti restituiscano
_meta["mcp/www_authenticate"]in assenza di Bearer.
10. Gestire più audiences (frontend + MCP) in prod
10.1. Osservazioni
In produzione coesistono due tipi di token:
- token frontend:
aud = [XXX, YYY];- token MCP:
aud = "XXX".
In una fase intermedia, la API accettava solo l'audience MCP (AUTH0_ALLOWED_AUDIENCES="XXX"), rompendo il frontend (mismatch di audience).
10.2. Soluzione: AUTH0_ALLOWED_AUDIENCES multi-valore
La soluzione è stata consentire più audiences lato API:
AUTH0_ALLOWED_AUDIENCES="XXX, YYY"
Il middleware:
- divide la stringa su
,; - fa
TrimSpacesu ogni voce; - accetta token il cui
aud(stringa o array) contenga almeno una audience ammessa.
Risultato:
- la API accetta sia:
- i token frontend (audience storica),
- i token MCP (audience MCP);
- la transizione verso un mondo multi-client (frontend + MCP + altri) è gestita in modo pulito.

FAQ: domande frequenti su Auth0 + MCP CaptainDNS
Perché creare una API Auth0 dedicata per il MCP invece di riutilizzare l'audience storica dell'API?
L'audience storica era già usata dal frontend e da token esistenti. Dare la stessa audience al MCP avrebbe reso la configurazione più ambigua e difficile da auditare.
Creando una API Auth0 dedicata per il MCP (in dev e prod) otteniamo una separazione chiara degli usi, migliore tracciabilità e la possibilità di evolvere il contratto MCP in modo indipendente dal frontend.
Perché il token iniziale era cifrato (JWE a 5 parti) e non un JWT leggibile?
Senza il Resource Parameter Compatibility Profile, Auth0 interpreta male certi flussi dove si invia solo resource, soprattutto quando non trova una audience coerente.
Nel nostro caso, il client MCP inviava resource=... senza audience=... esplicito. Auth0 rispondeva allora a volte con un token JWE cifrato (5 parti) o con un messaggio di errore.
Attivando il Resource Parameter Compatibility Profile, Auth0 tratta resource come audience, permettendo di ottenere un JWT RS256 classico a 3 parti, utilizzabile dal server MCP.
A cosa serve esattamente il PRM (/.well-known/oauth-protected-resource) lato MCP?
Il PRM è un documento di metadati che descrive come un client deve fare OAuth2 per accedere a una risorsa protetta:
- qual è la resource / audience;
- quali authorization e token endpoint usare;
- quali scope sono supportati e raccomandati.
Per CaptainDNS, il PRM permette a client come MCP Inspector o ChatGPT di scoprire automaticamente come ottenere un token Auth0 per il MCP, quali scope richiedere e integrarsi senza configurazioni manuali complesse.
Come gestire gli scope senza rompere i client esistenti?
Strategia scelta:
- definire un set minimo di scope richiesti (
profile,email) lato API; - non rifiutare tutta la richiesta se mancano questi scope, ma passare in modalità anonima;
- riservare il fallimento netto (missing scope) ai tool MCP con
RequiresAuth = true.
In questo modo i tool v0 restano utilizzabili senza cambiare la configurazione Auth0 dei client esistenti, consentendo al contempo un irrigidimento progressivo per gli strumenti premium più avanti.
Come passare un tool MCP da 'auth opzionale' a 'auth obbligatoria'?
Il passaggio avviene in due step:
- marcare il tool MCP con
RequiresAuth = true; - lato server MCP:
- se manca il Bearer o è invalido → restituire
isError=truecon_meta["mcp/www_authenticate"]per avviare il login; - se il Bearer è valido e gli scope sufficienti → eseguire normalmente.
Questo approccio permette di rendere un tool "auth obbligatoria" senza modificare la API backend, concentrando la logica di sicurezza sul MCP.
Glossario Auth0 + MCP CaptainDNS
MCP (Model Context Protocol)
Protocollo che standardizza il dialogo di un modello di IA con strumenti esterni (API, servizi). In CaptainDNS consente a client come MCP Inspector o ChatGPT di chiamare strumenti DNS/email tramite un server MCP dedicato.
PRM (Protected Resource Metadata)
Documento JSON pubblicato da una risorsa protetta (qui il server MCP CaptainDNS) all'URL /.well-known/oauth-protected-resource. Descrive resource, audience, scope e gli endpoint di authorization e token.
Issuer (iss)
Claim di un token JWT che indica chi lo ha emesso. Il server MCP e la API backend verificano che iss corrisponda al tenant Auth0 atteso.
Audience (aud)
Claim che indica per quali risorse è destinato il token. Nel nostro caso: l'API frontend o l'API MCP. La API backend deve accettare più audiences per gestire client diversi.
Subject (sub)
Identificativo unico dell'utente nel tenant Auth0. È la chiave usata per trovare o creare il profilo nella tabella profiles.
Scope
Elenco di permessi associati a un token (es. openid profile email captaindns:dns:read). Gli scope sono usati per controllare l'accesso a certe funzionalità, in particolare ai tool MCP protetti.
JWS vs JWE
- JWS: JWT firmato (3 parti), leggibile e verificabile lato server.
- JWE: JWT cifrato (5 parti) che richiede una chiave di decrittazione. Per il MCP CaptainDNS abbiamo scelto JWS RS256, più semplice da validare via JWKS.
Namespaced claim
Claim custom in un access token il cui nome è una URL/URN, richiesta da Auth0 per evitare collisioni con le claim standard. Esempio: NAMESPACE per salvare l'email in un token MCP.
Resource Parameter Compatibility Profile
Opzione Auth0 che tratta resource come audience per i client OAuth che conoscono solo resource. Fondamentale perché MCP Inspector ottenga un JWT RS256 valido senza cambiare comportamento.
frontend_mcp_anonymous / frontend_mcp_authenticated
Valori del campo source usati nei log e nella tabella api_requests per distinguere le richieste MCP anonime (senza Bearer o Bearer invalido) da quelle autenticate (Bearer valido, profilo identificato).
