CodeGym /Corsi /ChatGPT Apps /Configurazione del server di autenticazione MCP: esempio ...

Configurazione del server di autenticazione MCP: esempio con Keycloak

ChatGPT Apps
Livello 10 , Lezione 2
Disponibile

1. Che cos’è un Auth Server nella pratica e perché usiamo Keycloak

Cominciamo con un breve promemoria: un Auth Server (IdP) è un servizio che:

  • mostra all’utente la schermata di login/registrazione e il consenso;
  • emette token OAuth/OIDC (access_token, id_token, refresh_token);
  • pubblica il documento di discovery e le chiavi JWKS, affinché i resource server possano verificare questi token.

Nel nostro stack:

  • ChatGPT / MCP Jam agisce come client OAuth (public client);
  • il tuo server MCP — come Resource Server;
  • Keycloak — come Auth Server.

Perché Keycloak è comodo per il corso e nella vita reale:

  • è open source, facile da avviare in locale/in Docker;
  • ha un modello di entità piuttosto chiaro: realm, clients, users, roles;
  • in sostanza, qualunque configurazione che impari in Keycloak poi si trasferisce quasi 1:1 in Auth0/Okta/Cognito: le stesse idee — client, scopes, redirect URIs, PKCE.

Idea importante: non stiamo configurando non “Keycloak per l’intero progetto”, ma un Realm specifico per la nostra ChatGPT App. È una sorta di «sandbox» per l’autenticazione dei client MCP.

In breve, alla fine della lezione avrai:

  • un tuo realm in Keycloak per l’app ChatGPT;
  • un public client configurato con Authorization Code + PKCE;
  • un set minimo di scope e claim;
  • la comprensione di come quel token vive nel tuo server MCP basato su Node.

2. Entità di base di Keycloak attraverso la lente di MCP

Per non perdersi poi nella console di amministrazione, mettiamo ordine nelle entità.

Realm: spazio di configurazioni e utenti

Un Realm in Keycloak è uno spazio isolato con propri utenti, client e policy. Un’analogia utile — “un ufficio in affitto in un business center”: ognuno ha i propri locali, il proprio elenco di dipendenti e le proprie regole d’ingresso.

Per il corso e per la tua prima app reale ha senso creare un realm separato, ad esempio giftgenius-mcp o mcp-course. Questo permette di:

  • non toccare il realm master, per non rompere per errore la console di amministrazione;
  • riutilizzare configurazioni e utenti tra ambienti diversi (dev / staging / prod) tramite export/import del realm.

Client: registrazione di un’applicazione (ChatGPT / MCP Jam)

In Keycloak un Client non è un “utente”, ma un’applicazione che chiede token all’Auth Server. Nel nostro caso questo non è il tuo backend Next.js, bensì il client MCP: ChatGPT, MCP Jam, eventualmente un tuo widget, se implementi un flusso OAuth manuale nell’UI.

Campi chiave del client:

  • client_id — identificatore stringa;
  • tipo (public / confidential / bearer-only);
  • flussi OAuth abilitati (Standard Flow, Client Credentials ecc.);
  • elenco dei redirect URI consentiti;
  • elenco di scope e protocol mapper (claim nel token).

Per ChatGPT/MCP Jam ci serve un public client, perché:

  • ChatGPT come client non può custodire in modo sicuro un client_secret;
  • MCP Jam, in quanto strumento desktop/browser, gira anch’esso in un ambiente non affidabile.

User: una persona reale

Un User è un utente “in carne e ossa”: ha username, password, email, attributi, gruppi, ruoli. Quando qualcuno effettua il login tramite Keycloak, il suo sub e altri dati finiscono nel token, che poi verifichi sul server MCP e mappi ai tuoi accountId / tenantId.

Per la nostra demo è più che sufficiente:

  • uno o due utenti di test (ad esempio, alice@example.com, bob@example.com);
  • eventualmente una coppia di attributi come tenant o plan, se vuoi mostrare come i claim del token influenzano il comportamento dei tool.

3. Scelta del tipo di client: public, PKCE e perché senza secret

Ora al punto: come configurare il client in Keycloak per ChatGPT/MCP.

Public vs Confidential: perché non usare un client_secret

In una classica applicazione web crei un backend, ci metti il client_secret e proprio il server contatta l’IdP per i token. Questo è un confidential client: può custodire un secret.

Nel mondo ChatGPT è il contrario:

  • il client OAuth è la piattaforma ChatGPT stessa o un’utility come MCP Jam;
  • non ne controlli il codice né l’ambiente;
  • qualsiasi client_secret che tu fornisca a ChatGPT va considerato immediatamente compromesso.

Perciò ChatGPT/Jam operano come public clients, quindi senza client_secret, e compensano con PKCE — Proof Key for Code Exchange.

PKCE spiegato in parole semplici

PKCE è un “segreto per sessione” monouso. L’obiettivo è impedire che qualcuno intercetti l’authorization code e lo scambi per un token da un altro luogo. Lo schema è questo:

  1. Il client genera una stringa casuale code_verifier.
  2. La hash (di solito SHA-256) e ottiene code_challenge.
  3. Nel redirect a /authorize invia code_challenge e code_challenge_method=S256.
  4. Dopo il login, l’utente torna con il code al redirect URI.
  5. Il client fa POST su /token, passando il code e l’originale code_verifier.
  6. Keycloak calcola l’hash del verifier, lo confronta con il challenge e, se tutto ok, emette il token.

Importante per noi: tutto questo lo fa ChatGPT/MCP Jam. Dobbiamo solo abilitare nel client Keycloak il supporto per Authorization Code + PKCE (S256) e non richiedere il client_secret.

4. Configurazione passo‑passo di Keycloak per lo scenario MCP

Dunque, per ChatGPT/MCP Jam ci serve un public client con Authorization Code Flow e PKCE (S256), senza client_secret. Vediamo come appare questa configurazione nelle impostazioni di Keycloak.

Supponiamo che tu abbia già un Keycloak funzionante (container Docker, installazione locale — non importa). Ora ci interessa la logica delle impostazioni, non dove cliccare esattamente.

Creiamo un nuovo realm per l’app

Creiamo il realm giftgenius-mcp: è un’area separata in cui saranno presenti:

  • utenti specifici per l’app ChatGPT;
  • client tramite cui ChatGPT/MCP Jam effettuerà l’OAuth;
  • policy proprie per password e token.

Suggerimento pratico: non mescolare il realm con cui autorizzi gli operatori dell’admin con il realm per i client ChatGPT. È sia più sicuro sia più semplice concettualmente.

Aggiungiamo un utente di test

Creiamo un utente, ad esempio alice:

  • username: alice;
  • email: alice@example.com;
  • imposta una password (per semplicità, senza policy complesse);
  • se vuoi, aggiungi l’attributo tenantId=demo-tenant o il ruolo ROLE_PREMIUM.

Più avanti, nel server MCP, potrai decodificare il token, estrarre sub, email, tenantId e collegarli al tuo modello utente.

Creiamo un public client per MCP Jam / ChatGPT

Ora la parte più interessante — il Client.

A livello concettuale, i parametri dovrebbero essere così:

  • Client ID: giftgenius-mcp-client (nome a tua scelta);
  • Tipo: public / Client Authentication off;
  • Standard Flow (Authorization Code) abilitato;
  • PKCE abilitato con metodo S256;
  • redirect URI configurati;
  • scope necessari configurati (openid + il tuo custom, ad esempio, mcp:tools).

Abilitare Standard Flow e PKCE

In pratica:

  • abilita l’Authorization Code Flow (spesso chiamato “Standard Flow Enabled”);
  • nella sezione PKCE imposta pkceRequired=true e, di solito, esplicitamente code_challenge_method=S256.

Perché S256: nella documentazione moderna di OAuth 2.1 e nelle raccomandazioni OpenAI/Model Context Protocol, proprio S256 è il metodo supportato e sicuro; il PKCE “plain” è considerato insicuro.

Redirect URI — il punto più delicato

I Redirect URI devono corrispondere letteralmente a quelli usati dal client. Altrimenti avrai un errore invalid_redirect_uri in fase di autorizzazione.

Nel nostro corso ci sono due client tipici:

  1. MCP Jam/Inspector per il debug. Di solito lavorano su http://localhost:PORT/.... Per lo scenario locale è logico consentire redirect del tipo:
    • http://localhost:5173/* o un percorso specifico usato da Jam.
  2. ChatGPT / Apps SDK in produzione. Qui il redirect URI è definito dalla piattaforma. Nell’integrazione reale consulterai la documentazione aggiornata di OpenAI e imposterai l’URL richiesto che ChatGPT userà come callback.

Nell’ambito della lezione è importante capire questo: ChatGPT non può usare un redirect qualsiasi; deve coincidere con quello registrato sull’Auth Server. Quindi:

  • non mettere mai * e “qualsiasi URL va bene”;
  • per lo sviluppo locale sono ammissibili wildcard nell’ambito di localhost, ma non in produzione.

Scope: il minimo, ma sufficiente

Gli scope sono un mini‑elenco di permessi richiesti dal client.

Per il nostro scenario MCP di solito servono:

  • openid — per attivare OpenID Connect e ricevere id_token con il campo sub, a volte email;
  • uno scope personalizzato, ad esempio mcp:tools, che indica “accesso consentito agli strumenti MCP”.

In Keycloak puoi farlo tramite i Client Scopes:

  • lasciare openid;
  • disattivare di default gli scope superflui come profile e email, se non servono;
  • aggiungere un nuovo scope mcp:tools, con cui poi limiterai l’accesso ai tool sul Resource Server.

È importante per due motivi:

  1. Senza openid non riceverai id_token e alcune claim OIDC standard.
  2. Senza uno scope personalizzato separato non potrai, lato server MCP, affermare chiaramente: “questo token si può usare per invocare i miei strumenti”.

5. Configurazione dei token: durata, firma e claim

Vediamo ora quali token emette Keycloak e come configurarli per lo scenario MCP.

Durata dell’access token

Nelle impostazioni del realm di Keycloak c’è la sezione Tokens, dove puoi configurare:

  • Access Token Lifespan;
  • Refresh Token Lifespan e altri timeout.

Per una ChatGPT App sono importanti access token di breve durata:

  • alcuni minuti o ore — un valore normale;
  • se il token scade, il server MCP risponde 401, ChatGPT rilancia il flusso OAuth e l’utente, se necessario, effettua di nuovo il login.

L’idea è la stessa delle linee guida OpenAI per l’Apps SDK: TTL breve + rinnovo dei token e la possibilità di “disconnettere” rapidamente un utente revocando il token lato IdP.

I refresh token per il client ChatGPT, di regola, o non sono critici o vengono emessi con una durata ridotta, per evitare sessioni eterne.

Quali claim vogliamo vedere nel token

Minimamente ci servono:

  • sub — identificatore univoco dell’utente in Keycloak;
  • iss — chi ha emesso il token (issuer);
  • aud — per quale risorsa è il token (usato più avanti sul server MCP);
  • exp — scadenza;
  • scope — elenco degli scope.

In aggiunta, spesso sono utili:

  • email — se vuoi vedere l’indirizzo dell’utente;
  • tenantId o claim simile — per scenari multi-tenant;
  • roles — per un’ulteriore autorizzazione.

In Keycloak questo si configura tramite i Protocol Mapper:

  • mapper standard per email, preferred_username ecc.;
  • mapper personalizzati per gli attributi utente (user.attributeclaim.name).

Esempio: un mapper che aggiunge l’email come claim nel token, impostando user.attribute=email, claim.name=email.

Lato server MCP potrai prendere questi claim dal JWT decodificato e:

  • collegare sub al tuo accountId;
  • usare tenantId per selezionare solo i dati appartenenti a quel tenant;
  • usare roles per un controllo dei permessi più granulare.

Firma del token e JWKS

Per impostazione predefinita Keycloak firma access/id token con un algoritmo asimmetrico (di solito RS256) e pubblica le chiavi pubbliche tramite l’endpoint JWKS dal documento di OpenID Discovery.

Per noi è importante perché il server MCP potrà:

  • leggere issuer dal token;
  • trovare l’endpoint JWKS tramite /.well-known/openid-configuration;
  • ottenere la chiave pubblica e verificare la firma del token localmente.

Approfondiremo questa parte nella lezione sul server MCP come resource server protetto, ma è già utile capire perché Keycloak espone queste metainformazioni.

6. Dynamic Client Registration (DCR): quando serve davvero

Questa sezione è più avanzata. Finora abbiamo configurato il client “a mano” dalla console, e questo basta ampiamente per avviare l’app. Ma il protocollo OAuth consente ai client di registrarsi dinamicamente tramite un endpoint dedicato.

Nel contesto di ChatGPT e MCP OpenAI afferma esplicitamente che la piattaforma può usare la Dynamic Client Registration. Cioè ChatGPT si registra sull’Auth Server “al volo”, tramite il registration_endpoint dal documento di discovery.

In Keycloak questo funziona così:

  • si abilita la DCR a livello di realm;
  • configuri una policy: chi può registrare nuovi client e con quali grant types/scope.

Esempio di JSON per registrare un public client con Authorization Code + PKCE e scope openid mcp:tools potrebbe essere così:

{
  "clientName": "My ChatGPT App",
  "redirectUris": ["https://jam.proxy.mcpapps.com/callback"],
  "grantTypes": ["authorization_code"],
  "responseTypes": ["code"],
  "scope": "openid mcp:tools",
  "tokenEndpointAuthMethod": "none"
}

Dove tokenEndpointAuthMethod: "none" significa proprio public client senza client_secret.

Per il corso basta sapere che:

  • la DCR è utile se i client sono molti o di breve durata;
  • ChatGPT può potenzialmente auto-registrarsi nel tuo IdP;
  • ma agli inizi puoi cavartela con un client statico, creato via UI.

7. Come si collega alla nostra applicazione didattica

Ricordiamo che abbiamo un server MCP didattico (ad esempio, GiftGenius) che sa:

  • elencare possibili regali;
  • memorizzare alcune wishlist dell’utente;
  • in seguito — contattare la parte commerce, effettuare ordini, ecc.

Finché il server MCP è aperto, non sa chi lo sta chiamando:

  • una richiesta di ChatGPT può essere logicamente “da Alice” o “da Bob”, ma il server MCP non lo distingue;
  • non puoi mostrare la cronologia regali privata;
  • non puoi addebitare con sicurezza sul conto corretto.

Dopo aver configurato Keycloak come Auth Server, la situazione cambia:

  1. ChatGPT capisce da .well-known della tua risorsa MCP che è protetta e richiede un token.
  2. ChatGPT invia l’utente a Keycloak seguendo il flow Authorization Code + PKCE.
  3. L’utente effettua il login (la nostra alice).
  4. ChatGPT riceve un access token, in cui ci sono sub, email, mcp:tools e altri claim.
  5. ChatGPT invoca lo strumento GiftGenius già con Authorization: Bearer <token>.
  6. Il server MCP, verificando il token, capisce: “Ok, è Alice con sub=... e tenantId=demo-tenant” — e risponde di conseguenza.

Questo collegamento si completa nella prossima lezione, in cui renderemo il server MCP un “vero” resource server: implementeremo l’endpoint dei metadati, la verifica del token e l’associazione all’utente.

8. Piccoli esempi pratici (nostro stack: TypeScript + Node)

Tutto ciò che segue non è “l’unico modo giusto”, ma un riferimento su come potrebbe apparire in uno stack tipico Node/TypeScript. Se ora sei focalizzato sul cliccare in Keycloak, scorri velocemente questa sezione e torna qui quando collegherai il server MCP.

Sebbene la configurazione di Keycloak avvenga per lo più dalla UI o tramite il suo Admin REST API, è utile mostrare qualche frammento di codice intorno a ciò, per capire come userai tutto questo lato server MCP.

Supponiamo di avere già un server MCP Node.js (TypeScript) basato sull’SDK ufficiale.

Config di autorizzazione (issuer e audience)

Creiamo un piccolo modulo authConfig.ts:

// authConfig.ts
export const authConfig = {
  issuer: 'https://auth.my-company.com/realms/giftgenius-mcp',
  audience: 'https://mcp.my-company.com', // URL del tuo server MCP
  requiredScopes: ['mcp:tools'],          // minimo che ci aspettiamo nel token
};

Qui issuer — l’URL del realm di Keycloak, audience — l’identificatore della risorsa (lo useremo anche nella configurazione del token e di MCP).

Verifica di base del JWT tramite JWKS

Nella pratica userai probabilmente una libreria come jsonwebtoken + jwks-rsa o utility pronte dell’MCP SDK. Uno scheletro minimale potrebbe essere così:

// verifyToken.ts
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { authConfig } from './authConfig';

const client = jwksClient({
  jwksUri: `${authConfig.issuer}/protocol/openid-connect/certs`,
});

function getKey(header: any, callback: any) {
  client.getSigningKey(header.kid, (err, key) => {
    const signingKey = key?.getPublicKey();
    callback(err, signingKey);
  });
}

export function verifyAccessToken(token: string): Promise<any> {
  return new Promise((resolve, reject) => {
    jwt.verify(
      token,
      getKey,
      {
        audience: authConfig.audience,
        issuer: authConfig.issuer,
      },
      (err, decoded) => (err ? reject(err) : resolve(decoded)),
    );
  });
}

Ovviamente la gestione degli errori e il caching delle chiavi andrebbero curati meglio, ma l’idea è chiara: Keycloak pubblica le chiavi JWKS, noi le scarichiamo e verifichiamo la firma.

Verifica degli scope ed estrazione dell’identità

Nel middleware per i tool MCP potresti fare qualcosa del genere:

// authMiddleware.ts
import { verifyAccessToken } from './verifyToken';
import { authConfig } from './authConfig';

export async function requireAuth(bearerToken: string) {
  const token = bearerToken.replace(/^Bearer\s+/i, '');
  const decoded: any = await verifyAccessToken(token);

  const scopes = (decoded.scope as string).split(' ');
  const hasScope = authConfig.requiredScopes.every(s => scopes.includes(s));
  if (!hasScope) {
    throw new Error('Insufficient scope');
  }

  return {
    userId: decoded.sub,
    email: decoded.email,
    tenantId: decoded.tenantId,
  };
}

E poi, nei gestori degli strumenti MCP, userai userId e tenantId, per caricare le corrette wishlist dell’utente. Gli strumenti li abbiamo già implementati nei moduli precedenti; qui è importante vedere come il token di Keycloak si trasforma in un’identità comprensibile al tuo backend.

9. Errori tipici nella configurazione di Keycloak come MCP Auth Server

Errore n. 1: si usa un confidential client con client_secret.
A volte, per abitudine, si crea un client di tipo confidential e si prova a inserire il client_secret nella config di MCP/ChatGPT. Nell’ecosistema delle ChatGPT App questo non dovrebbe funzionare e non sarebbe sicuro: ChatGPT è un public client, non può conservare un secret. La via giusta — public client + PKCE.

Errore n. 2: scope troppo ampi di default.
Lasciare attivi profile, email e un mucchio di scope standard — e poi distribuire tali token a ogni chat — non è l’idea migliore. Meglio minimizzare: openid e uno specifico mcp:tools (o un paio di scope applicativi) sono sufficienti per le prime versioni. Questo riduce il rischio di divulgare dati superflui e rende il comportamento più prevedibile.

Errore n. 3: redirect URI non corretto.
Classico: in Keycloak è indicato http://localhost:5173/callback, ma MCP Jam usa http://localhost:5173/. O viceversa. Risultato — invalid_redirect_uri e un debug estremamente frustrante. Controlla sempre il valore esatto del redirect URI nella documentazione di Jam/ChatGPT e registralo alla lettera.

Errore n. 4: PKCE non abilitato o abilitato con metodo errato.
Alcune versioni di Keycloak richiedono di abilitare separatamente “PKCE required” e indicare il metodo S256. Se non lo fai, ChatGPT/Jam, che si aspetta PKCE, può ricevere un errore invalid_request con un reclamo su code_challenge. Controlla sempre le impostazioni PKCE per i public client.

Errore n. 5: claim errate o assenti nel token.
Capita che nel token manchi sub o email, perché lo scope o il protocol mapper non sono stati configurati. Di conseguenza, sul server MCP vedi il token ma non puoi mapparlo su un utente reale. Soluzione: assicurati che i campi necessari (al minimo sub, meglio anche email/tenantId) siano mappati negli access/id token.

Errore n. 6: TTL troppo lungo per gli access token.
Dal punto di vista della sicurezza, emettere access token validi per un giorno/settimana è una cattiva idea. In caso di fuga del token, l’attaccante otterrà un accesso di lunga durata alla risorsa MCP. Meglio emettere access token a breve durata (minuti o ore) e ricorrere a una ri‑autorizzazione quando necessario.

Errore n. 7: confusione con i realm e uso di master.
A volte la prima cosa che si fa è creare client e utenti direttamente nel realm master. Poi ci si attacca anche un paio di progetti — e alla fine non è chiaro chi sia dove. Meglio creare subito un realm separato per la singola applicazione/corso. Ti semplificherà la vita, anche al tuo DevOps.

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