1. Di cosa parla questa lezione e cosa non contiene
Sarà una lezione molto interessante; in questa lezione:
- mettiamo insieme la “figura” del “triangolo della fiducia” tra MCP Client, MCP Server e MCP Auth Server — con l’utente umano che sta “sopra” questo triangolo come proprietario delle risorse;
- ripercorriamo il flow: chi invia il token a chi, dove l’utente effettua il login e perché il server MCP non vede mai la sua password;
- colleghiamo il tutto al nostro backend Next.js/MCP e alla futura configurazione di Keycloak/Auth0.
Cosa non faremo oggi:
- non spunteremo caselle in Keycloak e non configureremo uno specifico IdP;
- non scriveremo una verifica completa del JWT né un’introspezione — sono temi delle prossime lezioni (sull’Auth Server e sul MCP Server come risorsa protetta).
Obiettivo ora — che possiate prendere un foglio, tracciare frecce tra ChatGPT, il vostro server e Auth0/Keycloak e spiegare senza esitazioni: dove avviene il login, dove il token, dove i dati.
2. Il triangolo della fiducia: MCP Client, MCP Server, MCP Auth Server
Partiamo dai protagonisti. Il “triangolo della fiducia” tecnico è formato da MCP Client, MCP Server e MCP Auth Server; l’utente (User) — un ruolo a parte, proprietario delle risorse, che sta idealmente sopra questo triangolo e fornisce il consenso all’accesso. Nello specifico di MCP e Apps SDK questa architettura è piuttosto ben formalizzata.
User (Resource Owner)
È la persona dall’altra parte dello schermo. Fa:
- apre ChatGPT;
- scrive una richiesta “mostra i miei ordini / le mie liste di regali”;
- accetta di “collegare l’account” del vostro servizio a ChatGPT.
Punto chiave: è lui/lei a possedere le risorse (storico ordini, profili, liste di regali) ed è lui/lei a concedere il consenso per accedervi.
MCP Client
Qui per noi è:
- ChatGPT con Apps SDK;
- a volte — MCP Jam Inspector (durante il debug).
MCP Client può:
- leggere i metadati del vostro server MCP (tramite .well-known);
- avviare il flusso OAuth nel browser dell’utente;
- memorizzare e allegare i token alle chiamate degli strumenti MCP.
Importante ricordare che MCP Client è un public client. Non conserva il vostro client_secret, quindi comunica con l’Auth Server come una SPA pubblica: Authorization Code + PKCE.
MCP Server (Resource Server)
È il vostro backend che implementa MCP:
- stabilisce una connessione con ChatGPT;
- dichiara strumenti (tools), risorse, prompt;
- per ogni chiamata dello strumento controlla l’intestazione Authorization: Bearer <token>;
- verifica il token (firma, exp, aud, scope) e, se tutto è ok, esegue la logica di business.
Punto fondamentale: il server MCP non gestisce il login. Non vede password, non mostra una form di login, non invia email “conferma l’email” all’utente. Si fida solo di token firmati crittograficamente dall’Auth Server.
MCP Auth Server (Authorization Server / IdP)
È un servizio separato di autenticazione e autorizzazione: Keycloak, Auth0, Ory Hydra+Kratos, Okta, Cognito, Azure AD ecc.
Si occupa di:
- UI di login (email/password, SSO, 2FA);
- archiviazione degli account utente;
- emissione dei token (access token, refresh token);
- pubblicazione delle metainformazioni OAuth/OIDC (/authorize, /token, jwks_uri, /registration ecc.).
Per MCP deve supportare OAuth 2.1 per i public client (PKCE S256, dynamic client registration, ecc.).
Tabella di riepilogo dei ruoli
| Chi | Cosa fa | Cosa non fa |
|---|---|---|
| User | Inserisce login/password, dà il consenso all’accesso ai dati | Non comunica direttamente con MCP Server |
| MCP Client (ChatGPT/Jam) | Inizia OAuth, conserva il token, invoca strumenti MCP | Non verifica password, non verifica la firma del token |
| MCP Server | Verifica i token, esegue la logica di business degli strumenti | Non mostra form di login, non conserva password |
| MCP Auth Server | Esegue il login dell’utente, emette token | Non conosce i vostri strumenti MCP né la loro logica di business |
Se finora avete confuso tutto in un unico “grande server che fa tutto” — è il momento di separare i ruoli.
3. Com’è il flusso: da “nessun token” alla chiamata protetta degli strumenti
Vediamo ora il flusso dei messaggi. Nella specifica MCP questo processo è chiamato “The Flow”: discovery → redirect → code → token → chiamate autorizzate.
Passo 0. Tentativo di chiamare uno strumento protetto senza token
L’utente scrive: “Mostra le mie idee salvate per i regali”.
ChatGPT come MCP Client decide: “per questo serve chiamare lo strumento getUserGiftLists dal nostro server MCP”. Esegue la chiamata senza token (l’utente non ha ancora effettuato il login).
Il vostro server MCP:
- vede l’assenza o un’intestazione Authorization non corretta;
- risponde con 401 Unauthorized e aggiunge l’intestazione WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource" con un link ai metadati della risorsa protetta (resource metadata, vedi sotto).
Approssimativamente così (logica, non HTTP completo):
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource"
ChatGPT vede questa intestazione e capisce: “ok, la risorsa è protetta da OAuth, bisogna eseguire il flusso OAuth e collegare l’account”.
Discovery: .well-known/oauth-protected-resource
Poi MCP Client richiede al vostro server i metadati:
GET /.well-known/oauth-protected-resource
Il server risponde con un documento JSON che contiene l’identificatore della risorsa e l’elenco degli authorization server da cui ottenere i token.
Esempio minimo (configureremo i dettagli più avanti; ora conta il concetto):
{
"resource": "https://api.giftgenius.com",
"authorization_servers": [
"https://auth.giftgenius.com"
],
"scopes_supported": ["gifts.read", "gifts.write"]
}
Qui:
- resource — l’ID canonico della vostra risorsa; dovrà poi essere usato come audience o resource al momento dell’emissione del token;
- authorization_servers — l’elenco degli Auth Server da cui ChatGPT può richiedere il token;
- scopes_supported — quali “permessi” il vostro server MCP riconosce.
Richiesta di autorizzazione: redirect verso l’Auth Server
Ricevuti i metadati, MCP Client va sull’Auth Server. Apre una scheda nel browser:
GET https://auth.giftgenius.com/authorize
?response_type=code
&client_id=chatgpt-giftgenius
&redirect_uri=... (URL di callback del MCP Client)
&code_challenge=...
&code_challenge_method=S256
&scope=openid gifts.read
&resource=https://api.giftgenius.com
L’utente:
- vede una schermata di login familiare (ad esempio, Keycloak o Auth0);
- inserisce login/password, esegue il 2FA;
- conferma che ChatGPT può leggere le sue liste regalo (scope gifts.read).
Code → Token: scambio del code per il token con PKCE
Dopo il login riuscito, l’Auth Server reindirizza l’utente al MCP Client con un code. MCP Client:
- esegue un POST su /token;
- invia il code e il code_verifier (che corrisponde al code_challenge del passo precedente).
L’Auth Server verifica PKCE: calcola l’hash di code_verifier, lo confronta con l’originale code_challenge. Se tutto è ok e il client è davvero lo stesso che ha iniziato il flow, allora:
- emette un access_token di breve durata (di solito un JWT);
- al suo interno indica:
- sub — ID dell’utente nell’Auth Server;
- aud o resource — il vostro server MCP;
- scope — le azioni consentite (gifts.read, openid ecc.).
Authenticated Request: chiamata di uno strumento MCP con token
Ora MCP Client è pronto a invocare di nuovo il vostro strumento, ma con l’intestazione:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Il server MCP:
- verifica la firma del token (tramite le JWK dell’Auth Server) o via introspection;
- controlla la scadenza (exp);
- verifica aud / resource — che il token sia stato emesso davvero per https://api.giftgenius.com;
- controlla lo scope e decide se è possibile chiamare getUserGiftLists.
Dopodiché interroga il vostro DB in base a uno userId e restituisce le liste personali di regali.
Notate che fin qui abbiamo parlato solo del flusso di rete: come il token viene ottenuto e arriva al server MCP. È poi importante capire come da sub e altre claim nel token si ottiene lo specifico userId nel vostro DB — qui entra in gioco l’identity bridge.
4. Identity bridge: come l’utente di ChatGPT diventa lo userId nel vostro DB
Il pezzo più interessante dell’architettura è il “ponte d’identità” (identity bridge). Nella specifica MCP si sottolinea chiaramente: il server MCP non conosce gli utenti di ChatGPT, si basa sui dati nel token emesso dall’Auth Server.
Lo schema è approssimativamente questo:
flowchart TD User[Utente in ChatGPT] -->|Login/SSO| Auth[Auth Server] Auth -->|JWT: sub, email, tenant| MCP[MCP Server] MCP -->|userId/tenantId| DB[(Il vostro DB)]
Passo dopo passo appare così.
Primo, l’Auth Server conosce internamente i propri utenti: ha entità user, email, id, eventualmente tenant, roles. Al login riuscito inserisce queste informazioni nel token (nelle claim):
{
"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"
}
Secondo, il MCP Server durante la verifica del token estrae queste claim e decide chi sia nel suo mondo. Ad esempio:
- se sub è già presente nella tabella User.authProviderId — prende lo userId associato;
- se non c’è — crea una voce locale (provisioning on‑the‑fly) e la collega.
Un tipico frammento di codice TypeScript lato server MCP (semplificato, senza verifica della firma) può essere così:
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;
}
Terzo, in base al proprio userId il server MCP recupera tutto il necessario: liste regalo, storico ordini, impostazioni, piano tariffario.
In questo modo, l’Auth Server diventa “il ponte” tra il mondo esterno (ChatGPT, Google, SSO) e il vostro mondo interno (customer_id nel DB degli ordini).
5. Perché separare Auth Server e MCP Server
Può venire la tentazione: “Facciamo in modo che il mio server MCP mostri il login ed emetta i token da solo”. Formalmente è possibile (potete incorporare un mini‑IdP al suo interno), ma architettonicamente è una cattiva idea. Le ragioni sono molto concrete.
Primo, sicurezza e scalabilità. Un Auth Server è una macchina pesante: 2FA, social login, politiche password, blocco account, recupero accesso, audit degli accessi, magari certificazioni. Riscriverlo in ogni microservizio (in ogni server MCP) — è una via senza uscita e un problema per PCI‑DSS. Molto meglio delegare a Keycloak/Auth0 e limitarsi a verificare i loro token.
Secondo, intercambiabilità del client. Oggi avete solo ChatGPT. Domani collegherete Claude Desktop, il vostro frontend web in Next.js, un’app mobile. Tutti possono usare lo stesso Auth Server e lo stesso schema OAuth 2.1, e il vostro server MCP continuerà semplicemente a verificare i token. Non dovrete riscrivere la logica di business per ogni nuovo client.
Terzo, pulizia del codice. Idealmente, il server MCP:
- sa pubblicare /.well-known/oauth-protected-resource;
- sa verificare il token Bearer ed estrarne userId, scopes, tenant;
- implementa gli strumenti di business (orders, gifts, profiles).
Tutta la UI del login — form, layout, social login — vive nell’Auth Server e non appesantisce il backend.
6. Come appare nel nostro applicativo didattico GiftGenius
Torniamo all’applicazione che portiamo avanti nel corso. Supponiamo di avere:
- una ChatGPT App “GiftGenius” con widget (Apps SDK) che sa suggerire regali;
- un server MCP su Node/Next.js che espone strumenti:
- searchGifts — anonimo, non richiede login;
- getSavedGiftLists — personale, richiede autenticazione;
- un Auth Server (in seguito — Keycloak/Auth0), dove ogni utente ha un account.
Scenario utente anonimo e autenticato
Se l’utente scrive semplicemente “trova un regalo per mio fratello, 30 anni, amante dei giochi da tavolo”, la nostra App può:
- chiamare lo strumento anonimo searchGifts;
- mostrare raccomandazioni nell’interfaccia.
In questo caso:
- il token non serve;
- il server MCP esegue la richiesta (ad esempio, verso il vostro catalogo o un’API di terze parti).
Non appena l’utente dice “salva questo nelle mie liste” o “mostra le mie idee salvate”, il modello decide di chiamare lo strumento protetto getSavedGiftLists. Il server risponde con 401 + WWW-Authenticate con resource_metadata. ChatGPT avvia la procedura guidata OAuth “Collega l’account GiftGenius”, fa passare l’utente dal login e ottiene il token.
Da quel momento, a ogni chiamata protetta:
- il server MCP vede Authorization: Bearer ...;
- estrae lo userId dal token;
- filtra i dati in base a questo userId.
Grazie a questo possiamo:
- separare i dati degli utenti;
- mostrare in modo sicuro lo storico ordini, i preferiti;
- abilitare funzioni di commerce (più avanti nel corso).
Architettura del backend: middleware + gestori degli strumenti
In pratica, nel codice Node/Next.js spesso appare come una catena: “middleware di autenticazione → gestore di business dello strumento”. Nella lezione sui tool handler abbiamo già sottolineato che occorre passare il contesto: user_id, token, impostazioni.
Un frammento di codice può essere così:
// auth-context.ts
export type AuthContext = {
userId: string | null; // null per chiamate anonime
scopes: string[];
};
Un middleware attaccato a tutti gli endpoint 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: [] }; // utente anonimo
const claims = await verifyAndDecodeToken(token); // verifica del token
const userId = await mapClaimsToUserId(claims);
const scopes = (claims.scope || "").split(" ");
return { userId, scopes };
}
E il gestore dello strumento riceve questo contesto:
// 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 }
});
}
Il punto è che il gestore dello strumento non sa nulla di OAuth o PKCE. Lavora semplicemente con un “ovvio” userId. Tutta la magia OAuth è incapsulata prima: nel client MCP e nel middleware di Auth.
7. Schemi visivi: come convivono Client, Server e Auth
Abbiamo già analizzato passo passo il flusso nel paragrafo 3. A volte è più facile vedere una volta che spiegare sette, perciò mostriamo le stesse interazioni in due diagrammi.
Lo scheletro dell’interazione (The Triangle of Trust)
flowchart TD U[User] -->|1. Login / Consent| A[MCP Auth Server] U -->|2. Chatta| C["MCP Client (ChatGPT)"] C -->|3. OAuth Flow| A C -->|4. Bearer Token| S[MCP Server] S -->|5. Data| C
Lo schema si legge così.
Per prima cosa l’utente effettua il login tramite l’Auth Server, che in sostanza conferma la sua identità ed emette un token. MCP Client gestisce questo processo e poi usa il token per rivolgersi al server MCP. Il server MCP non vede login e password, vede solo il token e decide cosa è consentito.
Il flusso dalla richiesta alla risposta
sequenceDiagram participant User participant ChatGPT as MCP Client participant Auth as Auth Server participant MCP as MCP Server User->>ChatGPT: "Mostrami le mie liste di regali" ChatGPT->>MCP: callTool(getSavedGiftLists) (senza token) MCP-->>ChatGPT: 401 + WWW-Authenticate (resource_metadata) ChatGPT->>Auth: /authorize + PKCE User->>Auth: Inserisce login/password, concede il consenso Auth-->>ChatGPT: redirect + code ChatGPT->>Auth: /token + code_verifier Auth-->>ChatGPT: access_token (JWT) ChatGPT->>MCP: callTool(getSavedGiftLists) + Authorization: Bearer ... MCP-->>ChatGPT: JSON con liste personali ChatGPT-->>User: Lista renderizzata nel widget
Questo diagramma è ciò che dovreste saper “raccontare a occhi chiusi” alla fine del modulo.
8. Un po’ più in profondità: più risorse, più client, DCR
Il pregio di questa architettura — è che scala.
Primo, potete avere più server MCP (ad esempio, uno per i regali, un altro per gli ordini) e un unico Auth Server che emette token con diversi aud/resource. Ogni server di risorse deve verificare che il token sia effettivamente destinato a lui, altrimenti si ottiene il classico problema del “confused deputy”, quando un token per un servizio viene accettato da un altro.
Secondo, potete avere molti client:
- ChatGPT App;
- il vostro frontend;
- un’app mobile;
- l’integrazione di un partner tramite MCP Gateway.
Tutti loro:
- leggono /.well-known/oauth-protected-resource;
- scoprono dove si trova l’Auth Server;
- eseguono il flow OAuth 2.1;
- ottengono token e chiamano il server MCP.
Terzo, gli Auth Server moderni supportano sempre più spesso la Dynamic Client Registration (DCR) — la possibilità di registrare dinamicamente i client via API. La specifica MCP implica proprio questa possibilità: il client (ChatGPT/Jam) può registrarsi automaticamente sull’Auth Server tramite il suo registration_endpoint.
In questo modulo è importante capire che:
- MCP Client, MCP Server e Auth Server comunicano tramite documenti di discovery standardizzati e token;
- non dovete “codificare rigidamente” tutti i client nel backend;
- potete espandere l’ecosistema senza rompere il modello di autorizzazione esistente.
9. Errori tipici nella comprensione dell’architettura di autorizzazione MCP
Errore n. 1: “Il server MCP deve gestire lui stesso il login dell’utente”.
Talvolta gli sviluppatori provano a inserire la form di login direttamente nel server MCP, quindi a inviare login/password tramite gli strumenti. Questo infrange l’idea stessa di OAuth. Il server MCP non deve vedere la password in alcun caso. Login e consenso sono responsabilità dell’Auth Server. Il server MCP lavora solo con i token e le loro claim.
Errore n. 2: Confusione tra MCP Client e MCP Server.
Capita che ChatGPT venga percepito come “parte del mio backend”, e si tenti, per esempio, di conservarvi dei segreti o di aspettarsi che verifichi autonomamente i permessi di accesso. In realtà MCP Client si limita ad avviare OAuth e allegare i token. La verifica del token e dei permessi è compito del server MCP, non di ChatGPT.
Errore n. 3: “API key in .env invece di OAuth”.
Classico anti‑pattern: creare un grande SERVICE_API_KEY, metterlo nel .env del server MCP e pensare che il problema sia risolto. In tale variante non c’è separazione dei permessi per utente, non si possono mostrare in sicurezza i dati personali o eseguire acquisti, tutto avviene “a nome del servizio”, non dell’utente. È in pieno contrasto con gli obiettivi dell’autorizzazione nelle ChatGPT Apps.
Errore n. 4: Ignorare audience e resource.
Se il server MCP accetta qualunque JWT valido con firma corretta e non controlla aud/resource, allora qualunque token emesso per un altro servizio dallo stesso Auth Server può essere usato per chiamare i vostri strumenti. È una violazione diretta del modello di sicurezza OAuth. Il server è obbligato a verificare che il token sia emesso proprio per il suo resource.
Errore n. 5: Mescolare logica di auth e logica di business.
A volte nei tool handler si inizia a infilare tutta l’analisi del token, la verifica della firma, il lavoro con le JWK ecc. Il risultato è codice fragile e difficile da mantenere. È molto più corretto separare lo strato “verifica del token, mapping su userId” (middleware) dallo strato “logica dello strumento”, che riceve un AuthContext già chiaro.
Errore n. 6: Aspettarsi che ChatGPT “faccia tutto da solo” senza .well-known.
Senza il corretto endpoint /.well-known/oauth-protected-resource il client MCP semplicemente non sa dove si trovi il vostro Auth Server e quali scope servano. Il risultato — la chat “non sa fare il login”, e lo sviluppatore guarda a lungo log vuoti. La via giusta: il server MCP dichiara chiaramente i propri requisiti di autorizzazione tramite .well-known, il client li legge e costruisce il flow.
Errore n. 7: Dimenticare l’utente nella logica di business.
Capita che, anche impostando correttamente OAuth e il mapping del token su userId, gli sviluppatori non lo usino nelle query al DB: ad esempio, dimenticano di filtrare per ownerId = userId. Così qualunque utente autenticato può vedere dati altrui. La presenza del token è solo il primo passo; il secondo è usare sempre correttamente userId e scope nel codice di business.
GO TO FULL VERSION