CodeGym /Corsi /ChatGPT Apps /Perché serve l’autenticazione in ChatGPT App e breve evol...

Perché serve l’autenticazione in ChatGPT App e breve evoluzione di OAuth

ChatGPT Apps
Livello 10 , Lezione 0
Disponibile

1. Perché serve davvero l’autenticazione in ChatGPT App

Partiamo dalla cosa principale: l’utente in ChatGPT ≠ l’utente nel tuo servizio.

ChatGPT ha il proprio account utente. Il tuo servizio ha i propri userId, tenantId, ruoli, billing, ordini. Tra loro non c’è alcun collegamento magico per impostazione predefinita. Se hai semplicemente avviato un server MCP e descritto alcuni tools, ChatGPT li chiamerà come un client astratto.

Ricordiamo il nostro esempio ipotetico dell’app GiftGenius — una ChatGPT App che aiuta a scegliere regali e a gestire le liste dei desideri. Che cosa vogliamo poter fare:

  • Mostrare all’utente le sue liste di regali salvate.
  • Permettere di segnare i regali come «acquistati» o «ricevuti».
  • Mostrare la cronologia degli ordini (soprattutto se poi andremo verso commerce/ACP).

Senza autenticazione il server MCP non sa affatto «chi è questa persona». Al massimo vede alcuni identificatori tecnici della connessione e un subject anonimo che OpenAI fornisce per l’identificazione e i rate limit, ma avverte esplicitamente di non usarlo per l’autorizzazione.

Autenticazione vs autorizzazione

È molto utile distinguere subito due concetti.

  • L’autenticazione (AuthN) risponde alla domanda: chi è?
  • L’autorizzazione (AuthZ) risponde: che cosa è permesso fare a questo «chi»?

Per una ChatGPT App lo schema è più o meno questo:

  1. Prima, tramite OAuth, confermi che l’utente è effettivamente autenticato nel tuo Identity Provider (IdP) (ad esempio, Keycloak/Auth0) e ottieni un token con il suo identificatore. Questa è l’autenticazione.
  2. Poi il server MCP legge il token, estrae da esso sub, ruoli e altri claim e decide se a quell’utente è consentito invocare uno specifico strumento (list_orders, delete_profile, ecc.). Questa è l’autorizzazione.

A livello di codice si può immaginarlo così (semplificato):

// Tipo di dati che il server MCP vuole conoscere sull'utente
export interface AuthContext {
  userId: string;
  roles: string[];
}

// Esempio d'uso in un handler di tool
async function listGiftLists(auth: AuthContext | null) {
  if (!auth) {
    throw new Error("User is not authenticated");
  }

  // Recuperiamo dal DB solo le liste di questo utente
  return db.giftLists.findMany({ where: { ownerId: auth.userId } });
}

Senza userId e ruoli semplicemente non riuscirai a scrivere correttamente la logica di business. Tutto si trasformerà in «un unico grande account comune per tutti».

2. Perché «un API key nello .env» non è la soluzione

Noi, come sviluppatori, abbiamo un riflesso naturale: «Creo un API key, lo metto in .env e tutto funzionerà». E in effetti, per integrazioni interne service‑to‑service gli API key sono uno strumento normale. Ma non appena entrano in gioco utenti reali e una ChatGPT App, l’approccio «un’unica chiave per tutti» crolla.

Vediamo un tipico codice dei moduli iniziali, dove chiamavamo semplicemente dal MCP il nostro backend:

// mcp/backendClient.ts
export const backendClient = new BackendClient({
  baseUrl: process.env.BACKEND_URL!,
  apiKey: process.env.BACKEND_API_KEY!, // un'unica chiave per tutto ChatGPT
});

Dal punto di vista del backend ora tutte le richieste sembrano uguali: «questa è l’integrazione ChatGPT». Nessuna differenza tra Masha e Pasha. Quindi:

  • Impossibile mostrare «area personale» — il server non sa di chi sia.
  • Impossibile distinguere i permessi: «questo utente può solo leggere, quell’altro anche acquistare».
  • Impossibile associare gli ordini a una persona nel tuo sistema principale.

Nel mondo MCP è anche insicuro. La specifica raccomanda di usare l’autenticazione HTTP (Bearer, API key, ecc.) tramite Streamable HTTP, ma sottolinea che il vero accesso degli utenti a risorse protette è meglio costruirlo tramite OAuth e token, non con una singola chiave di servizio.

Inoltre, secondo le policy di OpenAI, una buona app dovrebbe richiedere solo i dati effettivamente necessari e dare all’utente il controllo su ciò che condivide con l’App. Questo si sposa perfettamente con il modello degli scope OAuth, ma non si concilia affatto con l’approccio «una super‑chiave che può fare tutto».

Perché una chiave di servizio è problematica nel contesto di ChatGPT

Una API key di servizio esprime l’identità del servizio, non dell’utente. Con essa puoi firmare le chiamate dal tuo server MCP a servizi interni o ad API esterne (tipo OpenAI API), ma non puoi dire: «Ecco, questo è Vasya, mostrami la sua cronologia ordini».

Anti‑esempio semplice:

// Variante sbagliata: una «finta» per l'utente
async function getMyOrdersFromBackend() {
  // Il server MCP chiama /orders/me sul backend
  const res = await fetch(`${BACKEND_URL}/orders/me`, {
    headers: {
      Authorization: `Bearer ${process.env.BACKEND_API_KEY}`,
    },
  });

  // per il backend "me" è un servizio di integrazione, non una persona
  return res.json();
}

Anche se provassi a inserire artificialmente un qualche userId anonimo nel corpo della richiesta, resterebbe comunque un «artigianale fai‑da‑te». Ti serviranno comunque:

  • Un modo affidabile per dimostrare al backend che «questo è davvero Vasya, e non qualcun altro».
  • Un modo per limitare i permessi del singolo utente.
  • Un meccanismo di revoca (revoke) dell’accesso per il singolo utente, non per tutti insieme.

Ed è qui che entra in scena OAuth.

3. Mini-glossario: che cosa vogliamo da un sistema di accesso

Prima di tuffarci nella storia di OAuth, formuliamo semplicemente i requisiti per un sistema di autenticazione «normale» per una ChatGPT App.

Ci serve un modo per cui:

  1. Il nostro IdP esterno (Keycloak, Auth0, Hydra+Kratos, ecc.) conosce l’utente reale: login, email, userId, eventualmente tenant.
  2. Tale IdP rilascia un token di breve durata, che ChatGPT può passare in modo sicuro al server MCP nell’header Authorization: Bearer <token>.
  3. Il server MCP legge il token, ne verifica la firma, issuer, audience, scadenza e scope, estrae sub (l’identificatore dell’utente) e, sulla base di questo, mappa l’utente sulle proprie entità (accountId, tenantId).
  4. Gli stessi scope permettono di gestire finemente i permessi: un token dà solo read:gifts, un altro anche write:gifts o checkout.
  5. Se il token manca o non ha gli scope corretti, il server può restituire un errore con _meta["mcp/www_authenticate"], in modo che ChatGPT mostri all’utente la UI di autorizzazione e/o ottenga nuovamente il token.

Insomma, ci serve un protocollo standard e collaudato che sappia fare tutto questo. Spoiler: è OAuth 2.1 (e i suoi fratelli maggiori/minori).

4. Breve evoluzione di OAuth: dai dinosauri a PKCE

Ora ripercorriamo con calma l’evoluzione di OAuth, senza immergerci troppo negli RFC, ma capendo perché ci interessano i pattern moderni.

OAuth 1.0 / 1.0a: crypto‑fitness

Storicamente è nato prima OAuth 1.0. Consentiva ai siti web di dare ad altri servizi accesso alle proprie risorse senza passare la password dell’utente (già buono). Però:

  • Le firme delle richieste erano complesse: firma HMAC quasi su ogni richiesta, base string in rotazione, normalizzazione dei parametri.
  • Ogni richiesta andava firmata, bisognava conservare il consumer secret e saper formare correttamente la firma.

La maggior parte degli sviluppatori moderni non ha alcuna voglia di ripetere manualmente tutte queste danze.

La specifica 1.0a ha corretto alcune vulnerabilità, ma l’ingombro generale è rimasto.

OAuth 2.0: un framework, non «un unico protocollo»

OAuth 2.0 ha molto semplificato la vita: al posto di uno schema rigidamente definito è apparso un insieme di flow (authorization code, implicit, resource owner password, client credentials, ecc.). Questo ha dato flessibilità, ma ha generato anche uno zoo di implementazioni.

Vantaggi:

  • Più facile integrare SPA, mobile e applicazioni server‑side.
  • È emersa una chiara separazione dei ruoli: Resource Owner, Client, Resource Server, Authorization Server.

Svantaggi:

  • Nel mondo reale sono comparite molte scorciatoie pericolose. Il flow implicit (che consegnava il token direttamente al browser senza scambio server‑side del codice) si è rivelato insicuro.
  • Il flow password grant (quando il client invia login/password dell’utente in cambio di un token) contraddice la stessa filosofia di OAuth — ed è diventato un anti‑pattern.

La specifica, di per sé, ha lasciato troppa scelta «a discrezione», perciò sono nate molte raccomandazioni e best practice, ospitate in RFC separati e in vari blog.

OAuth 2.1: ci siamo messi in riga

OAuth 2.1 è un tentativo di documentare le best practice che nel frattempo si erano consolidate nella community:

  • Focus quasi interamente sull’Authorization Code Flow come opzione principale.
  • Uso obbligatorio di PKCE (Proof Key for Code Exchange) per i public client — quelli che non possono custodire un secret (ad esempio, app mobile, SPA e… client ChatGPT/MCP).
  • Flow obsoleti e insicuri come implicit e password grant vengono semplicemente esclusi dalla specifica.
  • Raccomandazioni per una breve durata degli access token e per l’uso dei refresh token per sessioni di lunga durata.

Perché è importante per te? Perché l’ecosistema attorno a MCP e ChatGPT si orienta chiaramente a queste best practice: l’Apps SDK e la specifica MCP Authorization richiedono esplicitamente authorization code + PKCE, token di breve durata e scope sensati.

5. Perché nel mondo delle ChatGPT App ragioniamo in termini di OAuth 2.1 + PKCE

Ora che abbiamo il contesto storico, guardiamo alla cosa con la lente di ChatGPT e MCP.

ChatGPT come public client

ChatGPT (e client come MCP Jam) rispetto al tuo Authorization Server è un tipico public client:

  • Non ha e non può avere un client_secret conservato in modo affidabile.
  • Gira nell’infrastruttura di OpenAI, che non controlli.

Quindi l’unica scelta sensata è Authorization Code Flow + PKCE, dove la sicurezza non si basa sul secret del client, ma sul controllo del code challenge e del code verifier.

La documentazione ufficiale dell’Apps SDK afferma chiaramente che ChatGPT, agendo come client MCP, esegue il flow con Authorization Code + PKCE (S256) e rifiuterà di completare l’autorizzazione se il tuo Authorization Server non dichiara il supporto di PKCE nei metadata: code_challenge_methods_supported: ["S256"].

Com’è il flow dal punto di vista di MCP

Molto a grandi linee, ma utile da immaginare così (sequenza per una risorsa protetta):

sequenceDiagram
    participant U as Utente
    participant C as ChatGPT (MCP Client)
    participant AS as Auth Server
    participant RS as Server MCP (Risorsa)

    U->>C: "Mostrami i miei ordini"
    C->>RS: call_tool(list_orders) senza token
    RS-->>C: Errore + _meta["mcp/www_authenticate"]
    C->>AS: Apre login/consenso (Authorization Code + PKCE)
    U->>AS: Esegue login e dà il consenso (scope)
    AS-->>C: Authorization Code
    C->>AS: Scambia il code per un Access Token (+ verifica PKCE)
    AS-->>C: Access Token (Bearer)
    C->>RS: call_tool(list_orders) con Authorization: Bearer <token>
    RS->>RS: Verifica firma, issuer, audience, scope
    RS-->>C: Elenco ordini dell’utente
    C-->>U: Mostra i dati

Il server utilizza:

  • I metadata della risorsa protetta (/.well-known/oauth-protected-resource) — lì dichiara sé stesso come risorsa e indica quale Authorization Server serve tale risorsa.
  • Il token che arriva nell’header Authorization: Bearer <token>, che verifica come JWT via JWK o introspeziona tramite l’Authorization Server.
  • Se il token non è valido per audience o scope — il server può rifiutare la richiesta e restituire di nuovo un WWW-Authenticate challenge in _meta["mcp/www_authenticate"], in modo che ChatGPT ripeta l’autorizzazione con i parametri necessari.

Dal punto di vista del tuo codice, tutto appare ragionevole: ricevi in ingresso un AuthContext già verificato e ci lavori.

Mini‑esempio: come un MCP tool distingue un utente anonimo da uno autenticato

Per ora senza uno specifico SDK OAuth, solo il concetto:

import type { McpToolHandler } from "./types";

export const listOrders: McpToolHandler = async (_args, context) => {
  const auth = context.auth; // supponiamo di mettere qui il risultato della verifica del token

  if (!auth) {
    return {
      content: [{ type: "text", text: "Devi effettuare l’accesso per vedere gli ordini." }],
      _meta: {
        // Challenge per ChatGPT: avvia l'OAuth flow
        "mcp/www_authenticate": [
          'Bearer resource_metadata="https://mcp.giftgenius.app/.well-known/oauth-protected-resource", error="insufficient_scope", error_description="Login required to view orders"'
        ]
      },
      isError: true
    };
  }

  const orders = await db.orders.findMany({ where: { userId: auth.userId } });

  return {
    content: [{ type: "text", text: `Ordini trovati: ${orders.length}` }],
    structuredContent: orders
  };
};

Proprio questo suggerimento _meta["mcp/www_authenticate"] è descritto nella documentazione ufficiale dell’Apps SDK come trigger per la UI OAuth da parte di ChatGPT.

6. Che cosa significa in pratica «token di breve durata, scope minimi»

Dalle specifiche e dalle guide derivano altri principi importanti da tenere a mente già ora, prima della prossima lezione sulla configurazione concreta dell’IdP.

Durata breve del token

Un access token deve vivere poco. Perché?

  • Se trapela, l’attaccante sarà comunque limitato nel tempo.
  • Puoi cambiare in sicurezza i permessi dell’utente e, dopo poco, il token «scade» e ne viene richiesto uno nuovo.

Di solito parliamo di minuti o decine di minuti. In cambio ottieni refresh token e/o autorizzazioni ripetute, ma nel contesto di ChatGPT gran parte della routine è gestita dal lato client.

Gli scope come modo per limitare i permessi

Gli scope sono stringhe come gifts.read, gifts.write, orders.read, orders.checkout. Indicano a cosa l’utente ha diritto nell’ambito della risorsa in questione.

Per una ChatGPT App è particolarmente importante:

  • Puoi rilasciare un token solo con gifts.read quando l’utente visualizza semplicemente le liste dei desideri.
  • Per operazioni ACP/Instant Checkout ha senso richiedere un set di permessi più restrittivo — ad esempio, orders.checkout, e metterlo in chiara evidenza per l’utente.

Nella descrizione dei tool di MCP c’è già la possibilità di dichiarare securitySchemes con scope specifici per gli strumenti, affinché ChatGPT sappia quali permessi servono per invocare un determinato tool.

Audience: il token deve essere «per questa» risorsa MCP

Un altro dettaglio importante è aud (audience). Il server MCP deve verificare che il token sia stato effettivamente emesso per lui, e non per qualche servizio adiacente.

Nella documentazione dell’Apps SDK è scritto chiaramente che ChatGPT passerà il parametro resource e si aspetta che l’Authorization Server lo rifletta nel token (di solito in aud), e che il server MCP verifichi tale campo.

C’è una buona probabilità che durante il review della tua app le vengano forniti auth_token falsi per verificare che non ci siano buchi nella tua implementazione di sicurezza. Quindi fai tutto correttamente fin da subito.

7. Come si applica alla nostra app GiftGenius

Torniamo a concentrarci sulla nostra app didattica. Al momento abbiamo più o meno questo quadro:

  • C’è il tool MCP get_gift_ideas, che in base alla descrizione del destinatario e al budget propone idee regalo. Può funzionare in modo anonimo.
  • C’è il tool MCP save_gift_list, che salva la lista nel DB. Vorremmo che fosse legato a uno specifico utente.
  • C’è il tool MCP list_saved_lists, che mostra tutte le liste salvate dall’utente. Questo richiede sicuramente autenticazione.

Il widget mostra belle card di regali, consente di cliccare «salva» e «segna come acquistato» — tutto ciò è di fatto un front end per strumenti MCP protetti.

A livello di tipi potrebbe apparire così:

// Tipizzazione del contesto di chiamata del tool (semplificata)
interface ToolContext {
  auth: AuthContext | null;
}

// Esempio di strumento protetto
async function listSavedGiftLists(_input: {}, context: ToolContext) {
  if (!context.auth) {
    // Qui useremo lo stesso trucco con mcp/www_authenticate visto sopra
    throw new Error("Authentication required");
  }

  return db.giftLists.findMany({
    where: { ownerId: context.auth.userId }
  });
}

E non appena scrivi funzioni di questo tipo, diventa chiaro: «semplicemente un API key in .env» non aiuta. Serve un AuthContext completo, costruito a partire da un token OAuth verificato.

Quali parti dell’app possono funzionare in anonimo e quali no

Un buon esercizio, prima di configurare OAuth, è passare in rassegna le funzionalità e separarle onestamente in due categorie.

Per esempio, in GiftGenius:

Anonime:

  • Generazione di idee regalo basata su una descrizione.
  • Mostra di esempi e modalità demo con dati fittizi.

Solo per utenti autenticati:

  • Visualizzazione e modifica delle liste dei desideri personali.
  • Cronologia degli ordini.
  • Qualsiasi operazione di pagamento, Instant Checkout, collegamento ad ACP.

Nelle prossime lezioni configureremo l’Authorization Server (ad esempio, Keycloak o la coppia Hydra+Kratos) e il server MCP in modo che i token per queste azioni abbiano gli scope necessari e i tool MCP sappiano rifiutare correttamente e chiedere a ChatGPT di ri‑autorizzare.

8. Errori tipici nel comprendere l’autenticazione in una ChatGPT App

Errore n. 1: «ChatGPT conosce già l’utente, perché dovrei avere un mio login?»
Molti pensano: «ChatGPT ha l’account dell’utente, perché non usare semplicemente quello come userId?». Ma ChatGPT non ti rivela la reale identità dell’utente e non ti dà accesso ai suoi account. Nei metadata MCP vedi al massimo un subject anonimo _meta["openai/subject"], destinato ai rate limit e all’identificazione della sessione, ma è chiaramente indicato che non va usato per autorizzazione o per legare utenti ad account reali.

Errore n. 2: «Un unico API key per tutti va bene, è solo un’“integrazione”»
L’approccio «abbiamo inserito nel server MCP l’API key del nostro backend e siamo a posto» funziona solo in scenari in cui tutti gli utenti di ChatGPT condividono lo stesso account nel tuo servizio. Appena compaiono dati personali, commerce, ACL — ti scontri con l’impossibilità di distinguere gli utenti e gestirne i permessi. Un API key è l’identità del servizio, non dell’utente.

Errore n. 3: «Implementiamo un password grant, è la cosa più semplice»
L’abitudine di passare login/password dell’utente al tuo backend e scambiarli per un token (Resource Owner Password Credentials Grant) è un pattern obsoleto e insicuro dei primi tempi di OAuth 2.0. Nelle raccomandazioni moderne e nel contesto di OAuth 2.1 è considerato un anti‑pattern. I public client come ChatGPT non dovrebbero vedere affatto le password dei tuoi utenti — per questo esiste Authorization Code + PKCE.

Errore n. 4: «PKCE è complessità inutile, facciamone a meno»
PKCE (in particolare S256) non è marketing di moda, ma un meccanismo di protezione obbligatorio dell’Authorization Code Flow per i public client. Senza PKCE, un authorization code rubato può essere riutilizzato. Nella specifica MCP Authorization e nell’Apps SDK è scritto chiaramente che ChatGPT richiede di dichiarare il supporto di PKCE nei metadata dell’Authorization Server e usa proprio questo meccanismo. Se lo disattivi, il flow semplicemente non funzionerà.

Errore n. 5: «Richiediamo subito tutti gli scope possibili — per sicurezza»
A volte viene voglia di ottenere un token con permessi tali da «aprire anche il formato C:». Ma questo viola il principio di minimizzazione dei privilegi (PoLP) e cozza con le policy sia di OpenAI sia della maggior parte degli IdP. Meglio pensare con precisione a quali scope servano davvero alla tua ChatGPT App: alcuni per la lettura, altri per la scrittura, altri separati per il commerce. Questo non solo aumenta la sicurezza, ma influisce anche sulla UX del consenso: l’utente vede un set di permessi chiaro e limitato, non una lista spaventosa di venti stringhe incomprensibili.

Errore n. 6: «Il server MCP conserverà lui stesso login/password e disegnerà la UI di login»
Il server MCP è un Resource Server, non un Authorization Server. Deve saper verificare i token, dichiarare i propri metadata .well-known e restituire i challenge WWW-Authenticate, ma non occuparsi di login e memorizzazione delle password. Per login/consenso è meglio usare un Authorization Server specializzato (Keycloak, Hydra, Auth0, ecc.), come vedremo nelle prossime lezioni.

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