CodeGym /Cursos /ChatGPT Apps /Por que a autenticação é necessária no ChatGPT App e a br...

Por que a autenticação é necessária no ChatGPT App e a breve evolução do OAuth

ChatGPT Apps
Nível 10 , Lição 0
Disponível

1. Por que precisamos de autenticação no ChatGPT App

Comecemos pelo principal: o usuário no ChatGPT ≠ o usuário no seu serviço.

O ChatGPT tem a própria conta do usuário. Já o seu serviço tem seus próprios userId, tenantId, papéis (roles), billing, pedidos. Não há ligação mágica entre eles por padrão. Se você simplesmente levantou um servidor MCP e descreveu algumas tools, o ChatGPT vai chamá-las como um cliente abstrato.

Lembremos nosso exemplo hipotético, o aplicativo GiftGenius — um ChatGPT App que ajuda a escolher presentes e gerenciar wishlists. O que queremos ser capazes de fazer:

  • Mostrar ao usuário suas listas de presentes salvas.
  • Permitir marcar presentes como “comprados” ou “recebidos”.
  • Mostrar o histórico de pedidos (especialmente se depois formos para commerce/ACP).

Sem autenticação, o servidor MCP não sabe “quem é a pessoa”. No máximo, ele vê alguns identificadores técnicos da conexão e um subject anônimo que a OpenAI fornece para identificação e rate limits, mas avisa explicitamente que isso não deve ser usado para autorização.

Autenticação vs autorização

É muito útil separar esses dois conceitos desde o início.

  • Autenticação (AuthN) responde à pergunta: quem é?
  • Autorização (AuthZ) responde: o que esse “alguém” está autorizado a fazer?

Para o ChatGPT App, o esquema é mais ou menos assim:

  1. Primeiro, via OAuth, você confirma que o usuário realmente fez login no seu Identity Provider (IdP) (por exemplo, Keycloak/Auth0) e obtém um token com o identificador dele. Isso é autenticação.
  2. Depois, o servidor MCP lê o token, extrai dele o sub, as roles e outros claims e decide se esse usuário pode chamar uma ferramenta específica (list_orders, delete_profile etc.). Isso é autorização.

No nível do código, podemos representar assim (simplificado):

// Tipo de dados que o servidor MCP quer saber sobre o usuário
export interface AuthContext {
  userId: string;
  roles: string[];
}

// Exemplo de uso no handler da tool
async function listGiftLists(auth: AuthContext | null) {
  if (!auth) {
    throw new Error("User is not authenticated");
  }

  // Buscar no banco de dados apenas as listas deste usuário
  return db.giftLists.findMany({ where: { ownerId: auth.userId } });
}

Sem userId e roles você simplesmente não conseguirá escrever corretamente a lógica de negócio. Tudo vira “uma conta única compartilhada por todos”.

2. Por que “chave de API no .env” não é solução

Como desenvolvedores, temos um reflexo natural: “Vou criar uma chave de API, colocar no .env e tudo vai funcionar”. E de fato, para integrações internas serviço–serviço, chaves de API são uma ferramenta normal. Mas assim que entram usuários reais e o ChatGPT App, a abordagem “uma chave para todos” desmorona.

Vejamos um código típico de módulos iniciais, onde apenas chamávamos nosso backend a partir do MCP:

// mcp/backendClient.ts
export const backendClient = new BackendClient({
  baseUrl: process.env.BACKEND_URL!,
  apiKey: process.env.BACKEND_API_KEY!, // uma única chave para todo o ChatGPT
});

Do ponto de vista do backend, agora todas as requisições parecem iguais: “esta é a integração do ChatGPT”. Nenhuma diferença entre Maria e Paulo. Logo:

  • Não é possível mostrar um “painel pessoal” — o servidor não sabe de quem é.
  • Não é possível separar permissões: “este usuário só pode ler, aquele também pode comprar”.
  • Não é possível vincular pedidos a uma pessoa no seu sistema principal.

No mundo MCP, isso também é inseguro. A especificação recomenda usar autenticação HTTP (Bearer, chaves de API etc.) via Streamable HTTP, mas enfatiza que o acesso completo dos usuários a recursos protegidos deve ser construído com OAuth e tokens, não com uma única chave de serviço.

Além disso, do ponto de vista das políticas da OpenAI, um bom aplicativo deve solicitar apenas os dados de que realmente precisa e dar ao usuário controle sobre o que ele compartilha com o App. Isso se encaixa perfeitamente no modelo de escopos do OAuth, mas não combina nada com a abordagem “uma superchave que faz tudo”.

Por que a chave de serviço é ruim no contexto do ChatGPT

Uma chave de API de serviço expressa a identidade do serviço, não do usuário. Ela pode assinar chamadas do seu servidor MCP para serviços internos ou APIs externas (como a OpenAI API), mas não pode dizer: “Aqui está o João, mostre a ele o histórico de pedidos dele”.

Anti-exemplo simples:

// Opção ruim: "enganar" o usuário
async function getMyOrdersFromBackend() {
  // O servidor MCP faz chamada a /orders/me no backend
  const res = await fetch(`${BACKEND_URL}/orders/me`, {
    headers: {
      Authorization: `Bearer ${process.env.BACKEND_API_KEY}`,
    },
  });

  // o backend entende que "me" é um serviço de integração, não uma pessoa
  return res.json();
}

Mesmo que você tente encaixar artificialmente algum userId anônimo no corpo da requisição, isso continuará sendo um “jeitinho artesanal”. Você ainda precisará de:

  • Uma forma confiável de provar ao backend que “este é realmente o João, e não outra pessoa”.
  • Um modo de limitar as permissões de um usuário específico.
  • Um mecanismo de revogação (revoke) de acesso para um usuário específico, e não para todos de uma vez.

É aqui que entra o OAuth.

3. Mini glossário: o que queremos do sistema de login

Antes de mergulhar na história do OAuth, vamos formular os requisitos para um sistema “decente” de autenticação para um ChatGPT App.

Precisamos de um mecanismo em que:

  1. Nosso IdP externo (Keycloak, Auth0, Hydra+Kratos etc.) conheça o usuário real: login, e-mail, userId e, possivelmente, tenant.
  2. Esse IdP emita um token de curta duração, que o ChatGPT possa transmitir com segurança ao servidor MCP no cabeçalho HTTP Authorization: Bearer <token>.
  3. O servidor MCP leia o token, valide a assinatura, o emissor (issuer), a audience, o prazo de validade e os scopes, extraia o sub (identificador do usuário) e, com base nisso, mapeie o usuário para suas entidades (accountId, tenantId).
  4. Esses mesmos scopes permitam gerenciar permissões de forma granular: um token concede apenas read:gifts, outro também write:gifts ou checkout.
  5. Se o token estiver ausente ou com scopes inadequados, o servidor possa retornar um erro com _meta["mcp/www_authenticate"], para que o ChatGPT mostre ao usuário a interface de autorização e/ou obtenha um novo token.

Em resumo, precisamos de um protocolo padrão, aprovado pelo tempo, que faça tudo isso. Spoiler: é o OAuth 2.1 (e seus irmãos mais velhos/novos).

4. Breve evolução do OAuth: dos dinossauros ao PKCE

Agora vamos percorrer cuidadosamente a evolução do OAuth, sem aprofundar nos RFCs, mas entendendo por que nos interessam exatamente os padrões modernos.

OAuth 1.0 / 1.0a: ginástica criptográfica

Historicamente, o primeiro foi o OAuth 1.0. Ele permitia que sites concedessem a outros serviços acesso aos seus recursos sem compartilhar a senha do usuário (o que já era bom). Mas:

  • Assinaturas de requisições eram complicadas: assinar quase cada requisição com HMAC, lidar com base strings, normalização de parâmetros.
  • Cada requisição precisava ser assinada, era necessário armazenar o consumer secret e montar corretamente a assinatura.

A maioria dos desenvolvedores modernos não tem vontade de repetir manualmente todos esses malabarismos.

A especificação 1.0a corrigiu algumas vulnerabilidades, mas a complexidade geral permaneceu.

OAuth 2.0: um framework, não “um único protocolo”

O OAuth 2.0 simplificou bastante a vida: em vez de um único esquema rígido, surgiu um conjunto de fluxos (flows) (authorization code, implicit, resource owner password, client credentials etc.). Isso trouxe flexibilidade, mas também um zoológico de implementações.

Vantagens:

  • Mais fácil integrar SPA, aplicativos móveis e aplicações servidoras.
  • Apareceu uma separação clara de papéis: Resource Owner, Client, Resource Server, Authorization Server.

Desvantagens:

  • No mundo real surgiram muitos “atalhos” perigosos. O flow implicit (que entregava o token diretamente no navegador sem a troca de código no servidor) mostrou-se inseguro.
  • O flow password grant (quando o cliente simplesmente envia login/senha do usuário em troca de um token) contradiz a própria filosofia do OAuth — e virou um antipadrão.

A especificação, por si só, deixou opções demais “à escolha”, então surgiram muitas recomendações e boas práticas que viviam em RFCs e blogs separados.

OAuth 2.1: colocar a casa em ordem

OAuth 2.1 é uma tentativa de documentar as boas práticas que já haviam se consolidado na comunidade:

  • Foco quase integral no Authorization Code Flow como principal opção de trabalho.
  • Uso obrigatório de PKCE (Proof Key for Code Exchange) para clientes públicos — aqueles que não podem armazenar segredo (por exemplo, apps móveis, SPA e… clientes ChatGPT/MCP).
  • Fluxos obsoletos e inseguros como implicit e password grant são simplesmente excluídos da especificação.
  • Recomendações para vida curta do access token e uso de refresh tokens para sessões de longa duração.

Por que isso importa para você? Porque o ecossistema em torno do MCP e do ChatGPT se orienta claramente por essas boas práticas: o Apps SDK e a especificação MCP Authorization exigem explicitamente Authorization Code + PKCE, tokens de curta duração e escopos adequados.

5. Por que, no mundo do ChatGPT App, pensamos em padrões OAuth 2.1 + PKCE

Agora que temos o contexto histórico, vamos olhar por meio da lente do ChatGPT e do MCP.

ChatGPT como public client

O ChatGPT (e clientes como o MCP Jam), em relação ao seu Authorization Server, é um típico public client:

  • Ele não tem e não pode ter um client_secret armazenado com segurança.
  • Ele é executado na infraestrutura da OpenAI, que você não controla.

Portanto, a única escolha sensata é Authorization Code Flow + PKCE, onde a segurança não depende do segredo do cliente, e sim da verificação do code challenge e do code verifier.

A documentação oficial do Apps SDK diz explicitamente que o ChatGPT, atuando como cliente MCP, executa o fluxo com Authorization Code + PKCE (S256) e se recusará a concluir a autorização se o seu Authorization Server não declarar suporte a PKCE nos metadados: code_challenge_methods_supported: ["S256"].

Como o fluxo se parece do ponto de vista do MCP

De forma bem grosseira, mas útil, podemos imaginar assim (sequência para um recurso protegido):

sequenceDiagram
    participant U as Usuário
    participant C as ChatGPT (Cliente MCP)
    participant AS as Servidor de Autorização
    participant RS as Servidor MCP (Recurso)

    U->>C: "Mostre meus pedidos"
    C->>RS: call_tool(list_orders) sem token
    RS-->>C: Erro + _meta["mcp/www_authenticate"]
    C->>AS: Abre login/consentimento (Authorization Code + PKCE)
    U->>AS: Faz login e concede consentimento (scopes)
    AS-->>C: Authorization Code
    C->>AS: Troca o código por Access Token (+verificação PKCE)
    AS-->>C: Access Token (Bearer)
    C->>RS: call_tool(list_orders) com Authorization: Bearer <token>
    RS->>RS: Verificação de assinatura, issuer, audience, scopes
    RS-->>C: Lista de pedidos do usuário
    C-->>U: Exibe os dados

O servidor, ao mesmo tempo, usa:

  • Metadados do recurso protegido (/.well-known/oauth-protected-resource) — ali ele se declara como recurso e indica qual Authorization Server o atende.
  • O token que vem no cabeçalho Authorization: Bearer <token>, que ele valida como JWT via JWK ou introspecta via o Authorization Server.
  • Se o token não corresponder em audience ou scopes, o servidor pode rejeitar a requisição e novamente retornar um WWW-Authenticate challenge em _meta["mcp/www_authenticate"], para que o ChatGPT refaça a autorização com os parâmetros necessários.

Do ponto de vista do seu código, tudo fica humano: você recebe de entrada um AuthContext já verificado e trabalha com ele.

Mini-exemplo: como a tool MCP diferencia usuário anônimo de autenticado

Sem um SDK OAuth específico por enquanto, apenas o conceito:

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

export const listOrders: McpToolHandler = async (_args, context) => {
  const auth = context.auth; // digamos que aqui colocamos o resultado da validação do token

  if (!auth) {
    return {
      content: [{ type: "text", text: "É preciso fazer login para ver os pedidos." }],
      _meta: {
        // Challenge para o ChatGPT: inicie o fluxo OAuth
        "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: `Pedidos encontrados: ${orders.length}` }],
    structuredContent: orders
  };
};

Exatamente esse tipo de dica em _meta["mcp/www_authenticate"] está descrito na documentação oficial do Apps SDK como o gatilho para a UI de OAuth do lado do ChatGPT.

6. O que significa “token de curta duração, escopos mínimos” na prática

Das especificações e guias derivam alguns princípios importantes que vale manter em mente desde já, antes da próxima aula sobre a configuração do IdP.

Vida curta do token

O access token deve viver pouco. Por quê?

  • Se ele vazar, o invasor ainda ficará limitado no tempo.
  • Você pode alterar permissões do usuário com segurança e, em pouco tempo, o token “expira” e será solicitado outro.

Normalmente falamos de minutos ou dezenas de minutos. Em troca, você obtém refresh tokens e/ou reautenticações; no contexto do ChatGPT, grande parte da rotina fica do lado do cliente.

Scopes como forma de limitar permissões

Scopes são strings como gifts.read, gifts.write, orders.read, orders.checkout. Eles indicam o que exatamente o usuário pode fazer dentro daquele recurso.

Para o ChatGPT App isso é especialmente importante:

  • Você pode emitir um token apenas com gifts.read quando o usuário estiver apenas visualizando wishlists.
  • Para operações de ACP/Instant Checkout, faz sentido solicitar um conjunto mais rígido de permissões — por exemplo, orders.checkout, e destacar isso claramente ao usuário.

Na descrição de tools do MCP já existe a possibilidade de declarar securitySchemes com scopes específicos para as ferramentas, para que o ChatGPT saiba quais permissões são necessárias para chamar determinada tool.

Audience: o token deve ser “para este” recurso MCP

Outro detalhe importante é o aud (audience). O servidor MCP deve verificar se o token foi realmente emitido para ele, e não para algum serviço vizinho.

A documentação do Apps SDK afirma claramente que o ChatGPT encaminhará o parâmetro resource e espera que o Authorization Server o reflita no token (normalmente no aud), e que o servidor MCP valide esse campo.

É bem provável que, durante a avaliação do seu aplicativo, forneçam um auth_token forjado e verifiquem se não há brechas na sua implementação de segurança. Portanto, faça tudo corretamente desde o início.

7. Como isso se aplica ao nosso aplicativo GiftGenius

Vamos nos concentrar novamente no nosso App de estudo. Hoje temos aproximadamente o seguinte cenário:

  • Existe a tool MCP get_gift_ideas, que sugere ideias de presentes com base na descrição do destinatário e no orçamento. Isso pode funcionar anonimamente.
  • Existe a tool MCP save_gift_list, que salva uma lista no banco. Queremos que ela esteja vinculada a um usuário específico.
  • Existe a tool MCP list_saved_lists, que mostra todas as listas salvas pelo usuário. Isso certamente requer autenticação.

O widget mostra cartões de presentes bonitos, permite clicar em “salvar” e “marcar como comprado” — tudo isso é, na prática, uma interface para tools MCP protegidas.

No nível de tipos, isso pode parecer assim:

// Tipagem do contexto de chamada da tool (simplificado)
interface ToolContext {
  auth: AuthContext | null;
}

// Exemplo de ferramenta protegida
async function listSavedGiftLists(_input: {}, context: ToolContext) {
  if (!context.auth) {
    // Aqui vai o mesmo truque com mcp/www_authenticate mostrado acima
    throw new Error("Authentication required");
  }

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

E assim que você escreve funções assim, fica claro: “apenas uma chave de API no .env” não vai ajudar em nada. É preciso um AuthContext completo, construído a partir de um token OAuth validado.

Quais partes do aplicativo podem funcionar anonimamente e quais não

Um bom exercício antes de configurar o OAuth é percorrer as funcionalidades e separá-las honestamente em duas categorias.

Por exemplo, no GiftGenius:

Anônimo:

  • Geração de ideias de presentes com base na descrição.
  • Exibição de exemplos e modo demo com dados fictícios.

Somente para autenticados:

  • Visualização e edição de wishlists pessoais.
  • Histórico de pedidos.
  • Qualquer operação de pagamento, Instant Checkout, vínculo com ACP.

Nas próximas aulas, vamos configurar o Authorization Server (por exemplo, Keycloak ou a dupla Hydra+Kratos) e o servidor MCP de modo que os tokens para essas ações tenham os scopes necessários e as tools do MCP saibam recusar corretamente e solicitar que o ChatGPT reautorize quando preciso.

8. Erros comuns ao entender autenticação no ChatGPT App

Erro nº 1: “O ChatGPT já conhece o usuário, por que preciso do meu próprio login?”
Muita gente pensa: “O ChatGPT tem a conta do usuário, por que não usar isso como userId?”. Mas o ChatGPT não revela a identidade real do usuário a você e não dá acesso às suas contas. Nos metadados do MCP, você vê, no máximo, um _meta["openai/subject"], que é destinado a rate limits e identificação de sessão, e está claramente indicado que não deve ser usado para autorização ou vínculo a contas reais.

Erro nº 2: “Uma chave de API para todos é normal — é só uma ‘integração’”
A abordagem “colocamos no servidor MCP uma chave de API do nosso backend e ficamos felizes” só funciona para cenários em que todos os usuários do ChatGPT compartilham a mesma conta no seu serviço. Assim que surgem dados pessoais, commerce, ACL — você esbarra na incapacidade de diferenciar usuários e gerenciar suas permissões. A chave de API é a identidade do serviço, não do usuário.

Erro nº 3: “Vamos implementar password grant, é o mais simples”
O hábito de enviar login/senha do usuário ao seu backend e trocá-los por um token (Resource Owner Password Credentials Grant) é um padrão antigo e inseguro dos primórdios do OAuth 2.0. Nas recomendações modernas e no contexto do OAuth 2.1, ele é considerado um antipadrão. Clientes públicos como o ChatGPT não devem ver as senhas dos seus usuários — para isso existe Authorization Code + PKCE.

Erro nº 4: “PKCE é complexidade extra, vamos sem isso”
PKCE (especialmente S256) não é marketing da moda; é um mecanismo obrigatório de proteção do Authorization Code Flow para clientes públicos. Sem PKCE, um authorization code roubado pode ser reutilizado. Na especificação MCP Authorization e no Apps SDK está indicado claramente que o ChatGPT exige declarar suporte a PKCE nos metadados do Authorization Server e usa exatamente esse mecanismo. Se você desativá-lo, o fluxo simplesmente não funcionará.

Erro nº 5: “Vamos pedir todos os scopes possíveis — por via das dúvidas”
Às vezes dá vontade de criar um token com permissões “abrir tudo e formatar o drive C:”. Mas isso viola o princípio da minimização de privilégios (PoLP) e conflita com as políticas tanto da OpenAI quanto da maioria dos IdPs. É melhor pensar claramente quais scopes seu ChatGPT App realmente precisa: alguns para leitura, outros para escrita, outros para commerce. Isso não só aumenta a segurança, como também impacta o UX do consentimento: o usuário vê um conjunto de permissões claro e limitado, não uma lista assustadora de vinte linhas obscuras.

Erro nº 6: “O servidor MCP vai armazenar logins/senhas e desenhar a UI de login”
O servidor MCP é um Resource Server, não um Authorization Server. Ele deve saber validar tokens, declarar seus metadados .well-known e retornar challenges WWW-Authenticate, mas não cuidar do login e do armazenamento de senhas. Para login/consentimento, use um Authorization Server especializado (Keycloak, Hydra, Auth0 etc.), como veremos nas próximas aulas.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION