CodeGym /Cursos /ChatGPT Apps /Controle de acesso e minimização de privilégios: scopes, ...

Controle de acesso e minimização de privilégios: scopes, segmentação, permissões por ferramenta

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

1. Por que pensar em permissões no ChatGPT‑App (e qual é o risco específico)

Em um aplicativo web “comum”, entre o usuário e seu banco de dados há apenas algumas camadas: frontend, API, BD. No ChatGPT‑App, entre o usuário e a API aparece mais um participante ativo — a LLM. E ela não é apenas um “filtro de texto”, mas uma entidade que:

  • escolhe sozinha quais ferramentas chamar e com quais argumentos;
  • pode ser enganada por prompt injection nos dados;
  • pode “confundir” ferramentas ou inventar argumentos que você não esperava.

Se você der permissões demais à LLM, terá o problema clássico do Confused Deputy: o modelo executa de boa-fé aquilo que, ao que parece, o usuário ou um texto nos documentos pedem, mas acaba chamando delete_all_orders em vez de get_last_order.

Portanto, nosso objetivo é:

  1. Minimizar os privilégios dos auth_tokens (quais dados e ações estão disponíveis em geral).
  2. Restringir quais ferramentas ficam disponíveis ao modelo em um cenário específico.
  3. Adicionar controle humano onde as consequências são especialmente críticas.

E tudo isso precisa ser feito sem paranoia e sem proibições totais, caso contrário o App se torna inútil. O equilíbrio entre conveniência e segurança é nossa principal missão neste módulo.

2. Modelo de acesso no ecossistema: quem acessa o quê

Para não nos confundirmos, vamos olhar o sistema como um todo. Temos vários níveis, cada um com sua zona de responsabilidade e suas permissões.

flowchart TD
  U[Usuário no ChatGPT] --> C[ChatGPT UI + LLM]
  C --> A["Seu App (plano visual + widget)"]
  A --> G[MCP Gateway / API Edge]
  G --> S[Servidores MCP e microsserviços]
  S --> D[Bancos de dados, filas, APIs externas]

Resumo dos papéis:

  • ChatGPT UI e LLM: gerenciados pela OpenAI. Você fornece instruções (system‑prompt, descrições de ferramentas), mas não controla os tokens internos e as permissões da plataforma.
  • Seu App (plano, tools, widget): você decide quais ferramentas estão disponíveis, como são descritas, quais confirmações de UX são necessárias, quais dados o widget pode exibir.
  • MCP Gateway / API Edge: aqui ocorre a verificação do token, o mapeamento de userId, tenantId, a lista de scopes e a roteirização para o serviço correto.
  • Servidores MCP e microsserviços: executam as ferramentas, fazem consultas ao BD e a APIs externas. Aqui devem existir as verificações mais rigorosas: scopes, isolamento por tenant, validação de entrada.
  • Armazenamentos e APIs externas: a última linha de defesa (restrições no nível do BD e das contas dos serviços externos).

A ideia-chave: a LLM não é uma fonte de permissões. Tudo o que chega ao servidor MCP é tratado como “requisição do usuário, formulada pelo modelo”. Decidir se a operação pode mesmo ser executada é responsabilidade do seu backend, e não do prompt.

3. AuthN vs AuthZ: o que você já sabe e o que adicionaremos

No módulo de autenticação você já fez:

  • AuthN (Authentication) — descobrir quem é esse usuário. Pelo OAuth 2.1/PKCE, o ChatGPT obtinha do IdP um token, que depois era anexado às chamadas MCP. Nele vinham sub, user_id ou similar, às vezes tenant_id.
  • AuthZ básico — possivelmente você já diferenciava papéis user/admin e checava pelo menos “se é usuário” ou “se é admin”.

Agora vamos complicar o quadro:

  • cada auth_token deve carregar um conjunto de scopes — permissões textuais no formato resource:action, por exemplo catalog:read, orders:write, payments:create;
  • seu servidor MCP deve verificar a correspondência desses scopes para cada ação, não apenas “uma vez na entrada”;
  • ferramentas diferentes e até operações diferentes dentro da mesma ferramenta podem exigir scopes distintos.

Em termos de OAuth 2.1, o ChatGPT é um “public client”, o MCP é um “resource server”, e seu servidor OAuth sabe quais scopes são suportados e o que significam. Os metadados do recurso MCP normalmente declaram scopes_supported para que o ChatGPT possa solicitar ao usuário exatamente as permissões necessárias.

4. Projetando scopes para o GiftGenius

Vamos pegar nosso GiftGenius didático e ver quais domínios de dados e ações ele tem. Em termos de funcionalidade, algo como:

  • visualizar o catálogo e os cards de presentes;
  • recomendações com base no histórico;
  • criar pedidos;
  • iniciar checkout / cobrança;
  • edição do catálogo por admin.

Em vez de criar um giftgenius:full_access onipotente, é melhor decompor isso em scopes razoáveis.

Convenção de nomenclatura: resource:action

Funciona bem a estratégia resource:action, onde:

  • resource descreve o domínio: catalog, recommendations, orders, payments, admin.
  • action descreve o tipo de ação: read, write, às vezes mais específico: create, delete, manage.

Exemplo para o GiftGenius:

Scope O que permite
catalog:read
Ler o catálogo público de presentes
recommendations:read
Ler o histórico de recomendações do usuário
orders:write
Criar novos pedidos
orders:read
Ler o histórico de pedidos do usuário
payments:create
Iniciar pagamento / checkout
catalog:admin
Editar o catálogo (somente para UI/admin de suporte)

Um usuário comum do GiftGenius precisará de algo como (listados separados por espaço): catalog:read recommendations:read orders:write orders:read payments:create. Para o administrador, adicionamos catalog:admin.

Importante: não crie um *:* ou admin:all universal. Quanto mais granular, mais fácil revogar um direito específico sem quebrar todo o aplicativo.

Tipos de scopes: read vs write vs critical

É útil marcar mentalmente os scopes por categoria:

  • seguros (read): não alteram estado, no máximo expõem dados;
  • mutáveis (write): criam/alteram entidades, incrementam contadores, mas não mexem com dinheiro nem fazem deleções massivas;
  • críticos (critical): pagamentos, exclusão de conta, deleção em massa de dados.

Para direitos críticos, é possível aplicar controles mais rigorosos:

  • concedê-los ao menor número possível de usuários;
  • solicitar consentimento separado do usuário no UI do ChatGPT ao emitir o token;
  • no MCP, exigir confirmação adicional (por exemplo, um PIN de uso único — cenários avançados).

Scopes no código: RequestContext e requireScope

No nível do MCP, é conveniente definir um tipo de contexto unificado:

// mcp/context.ts
export interface RequestContext {
  userId: string;        // quem
  tenantId: string;      // no contexto de qual organização
  scopes: string[];      // quais permissões outorgadas ao token
}

// Helper simples para checar permissões
export function requireScope(
  ctx: RequestContext,
  needed: string
) {
  if (!ctx.scopes.includes(needed)) {
    throw new Error(`Missing scope: ${needed}`);
  }
}

Supõe-se que o RequestContext seja formado no MCP Gateway após a validação do token: decodificou o JWT, verificou assinatura/expiração, extraiu sub, tenant, scope — e então anexa esse contexto a todas as chamadas de ferramentas.

Depois, no handler da tool:

// mcp/tools/createOrder.ts
import { requireScope, RequestContext } from "../context";

export async function createOrder(
  input: CreateOrderInput,
  ctx: RequestContext
) {
  requireScope(ctx, "orders:write");
  // em seguida – lógica de criação do pedido
}

Agora, mesmo que o modelo invoque createOrder em um momento em que, do ponto de vista do UX, você não esperava, sem orders:write a ferramenta simplesmente não será executada.

securitySchemes no nível da ferramenta

A especificação MCP permite que cada ferramenta declare quais esquemas de autorização e scopes ela necessita. Nos exemplos oficiais, securitySchemes é acoplado diretamente à descrição da ferramenta.

Exemplo hipotético:

// mcp/server.ts
server.registerTool(
  "createOrder",
  {
    title: "Create order",
    description: "Creates a new order for current user",
    inputSchema: {/*...*/},
    securitySchemes: [
      { type: "oauth2", scopes: ["orders:write"] }
    ]
  },
  async ({ input }, ctx: RequestContext) => {
    requireScope(ctx, "orders:write");
    // ...
  }
);

Aqui há dois níveis de proteção:

  • declarativo: o ChatGPT sabe que essa ferramenta precisa de orders:write e, se faltar permissão, iniciará o fluxo de auth (ou informará o usuário);
  • imperativo: seu código verifica tudo novamente antes da ação real.

Se o token existir, mas faltar scopes, o servidor deve retornar um erro com WWW-Authenticate: Bearer error="insufficient_scope", scope="orders:write" — e o ChatGPT poderá solicitar ao usuário a ampliação das permissões (step‑up authorization).

Insight

Nos exemplos oficiais é usado securitySchemes. Ele não foi aprovado na especificação oficial no formato apresentado nos exemplos do ChatGPT Apps SDK. Portanto, é preciso marcá-lo como extensão do protocolo oficial — envolvendo-o em _meta. Uma versão funcional do exemplo acima:

// mcp/server.ts
server.registerTool(
  "createOrder",
  {
    title: "Create order",
    description: "Creates a new order for current user",
    inputSchema: {/*...*/},
    _meta: {                                        // assim
      securitySchemes: [
        { type: "oauth2", scopes: ["orders:write"] }
      ]          
    }
  },
  async ({ input }, ctx: RequestContext) => {
    requireScope(ctx, "orders:write");
    // ...
  }
);

5. Per‑tool permissions e ferramentas “perigosas”

Scopes respondem à pergunta “o que este auth_token pode fazer em princípio”. Mas dentro do token há também a lista de ferramentas que o modelo pode usar. Elas também precisam ser projetadas com cuidado.

Classificação de ferramentas

Dividimos as ferramentas, de forma simplificada, em:

  • informacionais (informational / read‑only): leem dados, geram relatórios, fazem cálculos sem efeitos colaterais;
  • consequenciais (consequential): alteram estado, cobram dinheiro, excluem algo.

A documentação dos ChatGPT Apps recomenda que ferramentas read‑only sejam marcadas explicitamente como seguras e, para as perigosas, descrever consequências e incluir confirmações adicionais de UX.

Isso pode ser feito:

  • por meio de anotações na ferramenta (campos como readOnlyHint, destructiveHint);
  • por descrição textual: “Esta ferramenta exclui pedidos de forma irreversível”;
  • por uma flag separada confirmation_required, que seu plano do App usa para inserir um passo de confirmação no diálogo.

Confirmações de UX para ações críticas

Por exemplo, o GiftGenius tem a ferramenta chargeCustomer (inicia a cobrança). Você, obviamente, não quer que o modelo a chame sem o consentimento do usuário.

Como isso pode aparecer no nível do plano do App:

// app/plan/tools.ts (pseudocódigo)
export const tools = [
  {
    name: "giftgenius.list_catalog",
    description: "Mostrar catálogo de presentes",
    annotations: { readOnlyHint: true }
  },
  {
    name: "giftgenius.create_order",
    description: "Criar pedido sem pagamento",
    annotations: { consequential: true }
  },
  {
    name: "giftgenius.charge_customer",
    description: "Debitar o valor do pedido",
    annotations: {
      consequential: true,
      destructiveHint: true,
      confirmationRequired: {
        title: "Debitar do cartão?",
        message: "Um pagamento será realizado para o pedido N."
      }
    }
  }
];

Os nomes dos campos dependem da versão do SDK, mas a ideia coincide com as recomendações: marcamos as ferramentas read‑only como seguras e as perigosas como exigindo confirmação explícita e uma boa explicação na descrição.

Depois, seu widget pode reagir: se o modelo propuser chamar charge_customer, você mostra uma janela modal ao usuário com uma formulação clara e só após o clique em “Confirmar” executa de fato a chamada da ferramenta.

Exemplo de componente no widget (simplificado):

// widget/components/ConfirmCharge.tsx
export function ConfirmCharge(props: {
  orderId: string;
  onConfirm: () => void;
}) {
  return (
    <div>
      <p>Debitar o valor do pedido {props.orderId}?</p>
      <button onClick={props.onConfirm}>
        Sim, confirmar pagamento
      </button>
    </div>
  );
}

O modelo inicia a ideia de “hora de pagar”, mas o clique final é do humano. Isso é o human‑in‑the‑loop, tão valorizado pela área de segurança.

Ferramentas apenas para agentes/back‑office

Outro caso frequente: você tem ferramentas que apenas agentes (no sentido do Agents SDK) ou painéis internos podem usar — e não o ChatGPT App “comum” do usuário.

Por exemplo, rebuildSearchIndex ou syncCatalogFromERP. O ideal é:

  • não incluí-las na lista geral de tools do App comum;
  • configurá-las em um agente/orquestrador separado;
  • protegê-las com scopes próprios e, possivelmente, com um contorno de Auth separado.

Se você apenas adicioná-las à lista de ferramentas acessíveis do App, aumenta o risco de o modelo decidir de repente: “Vou recriar o índice agora; talvez isso ajude a encontrar o presente”.

6. Segmentação de rede e fronteiras de confiança

Permissões não são apenas scopes no token. O segundo eixo importante é a segmentação de rede e de serviços.

Cenário ideal:

  • existe exatamente uma entrada pública para o backend — MCP Gateway/Edge API;
  • tudo que armazena PII e dinheiro vive em rede privada/VPC e é acessível apenas por meio desse gateway;
  • o tráfego de saída (outbound) do backend é limitado a uma lista de domínios permitidos (allowlist: processador de pagamentos, CRM, seus microsserviços).

Esquematicamente:

flowchart LR
  ChatGPT -- HTTPS --> Edge[API Gateway / MCP Endpoint]
  Edge -- private network --> MCP[Servidor MCP]
  MCP -- private --> DB[(DB com PII)]
  MCP -- private --> SVC[Microsserviços internos]
  MCP -- HTTPS (allow) --> Stripe[Payments API]

Algumas regras importantes aqui:

  1. BDs e serviços internos não ficam expostos diretamente à internet. O acesso direto a eles ocorre apenas pela rede privada e somente a partir dos serviços que realmente precisam.
  2. Edge/Gateway realiza auth e rate limiting. É ele que verifica token e scopes, limita requisições muito frequentes e grava os principais audit logs.
  3. Controle de egress. O servidor MCP não deve conseguir acessar quaisquer URLs da internet (ataques SSRF, vazamento de dados). É melhor restringir explicitamente a lista de hosts externos.

Na prática, se você fizer deploy do MCP na Vercel, Render ou em um cluster Kubernetes, parte dessas coisas não é configurada manualmente, mas mesmo assim é possível separar:

  • projetos/cluster distintos para dev/staging/prod;
  • variáveis de ambiente e chaves diferentes para cada ambiente;
  • um serviço “edge” separado (HTTP wrapper do MCP) e serviços privados separados.

Assim, já temos dois eixos de proteção: permissões no token (scopes) e fronteiras de rede. Vamos adicionar mais um — multi‑tenant, quando o mesmo App atende várias organizações.

7. Multi‑tenant / contexto organizacional

Até agora, pensamos em um único usuário. Mas muitos aplicativos para ChatGPT são multi‑tenant: o mesmo App atende dezenas de empresas. O GiftGenius pode facilmente virar um serviço B2B para corporações: cada departamento com seus catálogos, orçamentos, pedidos.

O que é um tenant e onde obtê-lo

Tenant é, normalmente:

  • uma organização/empresa (Acme Corp);
  • um workspace (espaço de trabalho);
  • às vezes um projeto ou ambiente.

Propriedade principal: os dados de um tenant não devem ser visíveis a outro.

No fluxo de auth, o tenant costuma ser colocado em:

  • claim do token (tenant, org_id);
  • um parâmetro separado na requisição de autorização (menos confiável do que um claim assinado pelo IdP).

Importante: confiamos apenas no tenantId proveniente de um token verificado, e não nos argumentos das ferramentas. Se o modelo gerar {"tenantId": "acme"}, mas no token do usuário constar tenantId: "globex", isso deve ser tratado como tentativa de violação.

Tenant no contexto da requisição

Vamos adicionar tenantId ao nosso RequestContext (já fizemos isso acima) e não permitir que ele seja sobrescrito a partir dos dados de entrada.

Verificação básica:

// mcp/tenant.ts
import { RequestContext } from "./context";

export function enforceTenant<TInput>(
  input: TInput & { tenantId?: string },
  ctx: RequestContext
) {
  if (input.tenantId && input.tenantId !== ctx.tenantId) {
    throw new Error("Tenant mismatch");
  }
  return { ...input, tenantId: ctx.tenantId };
}

Depois, na ferramenta:

// mcp/tools/listOrders.ts
export async function listOrders(
  input: { limit?: number; tenantId?: string },
  ctx: RequestContext
) {
  const safe = enforceTenant(input, ctx);
  return db.order.findMany({
    where: { tenantId: safe.tenantId },
    take: safe.limit ?? 20
  });
}

Ignoramos o tenant vindo dos argumentos e o fixamos a partir do contexto. Assim, mesmo que a LLM ou um invasor tente “inserir” outro tenant, não terá efeito.

Isolamento por tenant no nível do BD

Arquiteturalmente, há diferentes opções:

  • um BD separado por tenant;
  • schemas separados;
  • um único BD com tenant_id em cada tabela e filtragem rigorosa.

Qualquer que seja a opção escolhida, existe uma regra de ouro: nenhuma consulta ao BD deve ser executada sem filtrar por tenant_id do contexto. Isso é particularmente importante em RAG/pesquisa vetorial: se você esquecer o filtro por tenant, o modelo pode começar a buscar documentos de outras organizações.

8. Como isso se conecta ao nosso aplicativo Next.js/Apps SDK

Agora vamos juntar tudo e ver como scopes, tenant e fronteiras de rede se materializam no nosso projeto Next.js com Apps SDK. Vamos adicionar mais concretude e olhar o código de Next.js e do Apps SDK.

Onde vivem scopes e tenant em nosso projeto

Layout típico para um projeto didático:

  • No aplicativo Next.js (Apps SDK), você tem a configuração do App/conector e as páginas para os callbacks do OAuth.
  • No servidor MCP — o código que aceita requisições HTTP/SSE do ChatGPT, verifica o token e invoca a ferramenta necessária.

Levamos para lá tudo o que discutimos:

  1. Nas configurações OAuth do recurso MCP, declaramos scopes_supported para o GiftGenius (catalog:read, orders:write, etc.).
  2. Na configuração do Apps SDK, descrevemos o App com a lista de ferramentas e suas anotações (read‑only, consequential, fluxos de confirmação).
  3. No servidor MCP, implementamos:
    • parse e verificação do token;
    • formação do RequestContext { userId, tenantId, scopes };
    • helpers requireScope, enforceTenant etc.;
    • consultas ao BD sempre usando o tenantId do contexto.

Exemplo de “caminho isolado” para criação de pedido

Vamos acompanhar um cenário end‑to‑end.

  1. O usuário escreve: “Faça o pedido deste kit para um orçamento de US$ 50”.
  2. O modelo decide que precisa chamar giftgenius.create_order com os argumentos { productId, budget, ... }.
  3. O ChatGPT verifica: o App tem a ferramenta create_order? Quais scopes e securitySchemes estão definidos para ela? Conclui que é necessário orders:write.
  4. Se o token já existir e contiver orders:write, a requisição segue; se não, o ChatGPT inicia a autorização OAuth solicitando o scope necessário.
  5. O MCP Gateway recebe a requisição, verifica o token e forma o RequestContext com userId=123, tenantId="acme", scopes=["catalog:read","orders:write",...].
  6. createOrder dentro do MCP:
    • executa requireScope(ctx, "orders:write");
    • fixa o tenant com enforceTenant;
    • cria o pedido apenas no âmbito de tenantId="acme".
  7. Se o pedido exigir pagamento imediato, o modelo ou o próprio backend iniciam charge_customer, onde:
    • a ferramenta no plano está marcada como confirmationRequired;
    • o widget renderiza ConfirmCharge e pede que o usuário confirme explicitamente a cobrança.

Assim, obtemos defesa em profundidade: prompts muito amplos, prompt injections ou mesmo bugs no UX não levam a ações incontroláveis, porque na base da pirâmide ainda existem verificações rigorosas de scopes, tenant e confirmações manuais para ações críticas.

9. Erros típicos ao projetar permissões e segmentação

Erro nº 1: Um scope “gordo” como app:full_access.
Essa abordagem é conveniente em demos, mas perigosa em produção. Perdeu um token — perdeu tudo. Não dá para revogar ou proibir uma operação sem quebrar as demais. Separe os direitos por domínios e tipos de operações (read/write/critical).

Erro nº 2: Verificar permissões apenas “na entrada” e não dentro das ferramentas.
Às vezes fazem assim: “se o ChatGPT obteve um token, então já pode tudo”. E então a ferramenta createOrder é chamada mesmo que aquele token não tenha orders:write. A abordagem correta é verificar scopes em cada ferramenta (ou ao menos por um middleware centralizado para operações mutáveis).

Erro nº 3: Não marcar ferramentas perigosas e não exigir confirmação.
Se a ferramenta cobra dinheiro, apaga dados ou muda acessos, ela não deve parecer ao modelo igual à listCatalog. A ausência de anotações explícitas e confirmações de UX aumenta a chance de o modelo chamá-la “porque parece lógico”. No mínimo, separe ferramentas read‑only e destrutivas e marque explicitamente as segundas.

Erro nº 4: Confiar no tenantId dos argumentos da ferramenta.
Um antipadrão comum: a ferramenta getOrders({ tenantId }), onde o tenantId vem do modelo. Se você usá-lo como está, um usuário do tenantA pode acessar dados do tenantB apenas informando outro identificador. O tenant deve vir do token verificado e ser imposto a todas as consultas ao BD e serviços externos; valores fornecidos pelo usuário são ignorados ou validados para coincidência.

Erro nº 5: MCP/BD acessíveis diretamente da internet.
Em protótipos simples, às vezes o servidor MCP e o BD ficam expostos na internet em HTTP/5432. Em produção isso é inadmissível: todo acesso deve passar por um gateway/proxy protegido, e o BD deve viver em rede privada. Caso contrário, qualquer endpoint vulnerável ou webhook mal configurado é caminho direto para os dados.

Erro nº 6: Usar os mesmos scopes/segredos em dev e prod.
É o jeito preferido de causar exclusão acidental de dados de produção durante uma demo local de dev. Cada ambiente deve ter suas próprias chaves, scopes e BDs. Mesmo que alguém obtenha acesso a um token de dev, não conseguirá prejudicar dados de prod.

Erro nº 7: Relutar em “negar ao modelo”.
Alguns desenvolvedores se preocupam: “Se eu retornar com frequência insufficient_scope ou forbidden, o modelo vai funcionar pior”. Na prática, isso é normal e esperado: o modelo aprende quais ações estão disponíveis e quais exigem permissões adicionais ou confirmação. Pior é quando ele “consegue” fazer o que não deveria — por exemplo, processar um segundo pagamento.

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