Phase 13 - Lesson 18

Autenticação MCP em Produção — DCR, Rotação de JWKS, Tokens Vinculados ao Público em Primitivos iii

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

A Lição 16 estabeleceu a máquina de estados OAuth 2.1 em memória. Até 2026, cada servidor MCP que você implantar em uma organização real estará protegido por autenticação de produção: registro dinâmico de clientes (RFC 7591), descoberta de metadados do servidor de autorização (RFC 8414), rotação de JWKS que não quebra a validação de tokens às 3 da manhã, e tokens vinculados ao público que recusam o reuso por deputado confuso (confused-deputy). Esta lição conecta tudo isso por meio de primitivos iii — iii.registerTrigger para HTTP e cron, iii.registerFunction para lógica de autenticação, state::set/get para chaves em cache — de modo que a superfície de autenticação seja observável, reiniciável e reproduzível como qualquer outra carga de trabalho no motor.

Tipo: Build Linguagens: Python (stdlib, primitivos iii simulados para o ambiente da lição) Pré-requisitos: Phase 13 · 16 (OAuth 2.1 state machine), Phase 13 · 17 (gateways) Tempo: ~90 minutos

Objetivos de Aprendizado

  • Descobrir um servidor de autorização por meio de metadados da RFC 8414 e verificar o contrato.
  • Implementar o registro dinâmico de clientes (DCR) da RFC 7591 para que os clientes MCP se inscrevam sem intervenção do administrador.
  • Colocar em cache e rotacionar as chaves JWKS usando um trigger cron para que a verificação de assinatura sobreviva à rotação de chaves.
  • Vincular tokens a um único recurso MCP usando indicadores de recurso da RFC 8707 e recusar o reuso por deputado confuso.
  • Conectar cada endpoint e tarefa em segundo plano como primitivos iii — triggers HTTP, triggers cron, funções nomeadas e leituras de state::* — para que uma única reinicialização reconstrua a superfície de autenticação.
  • Ler uma matriz de capacidades de IdP e recusar a implantação quando o IdP não puder satisfazer o perfil de autenticação do MCP.

O Problema

O simulador da Lição 16 executa o OAuth 2.1 em memória. A produção possui três lacunas operacionais que um simulador apenas em memória não percebe.

A primeira lacuna é a inscrição. Uma organização real executa centenas de servidores MCP e milhares de clientes MCP. Os operadores não registram manualmente cada usuário do Cursor como um cliente OAuth. O registro dinâmico de clientes da RFC 7591 permite que um cliente envie um POST /register contra o servidor de autorização e receba um client_id (e opcionalmente um client_secret) imediatamente. O servidor publica o registration_endpoint em seus metadados da RFC 8414; o cliente o descobre sem configuração fora de banda.

A segunda lacuna é a rotação de chaves. A validação de JWT depende das chaves de assinatura do servidor de autorização, publicadas como um JSON Web Key Set (JWKS). O servidor de autorização rotaciona essas chaves de acordo com um cronograma (frequentemente de hora em hora, às vezes mais rápido sob resposta a incidentes). Um servidor MCP que busca o JWKS uma única vez na inicialização realiza a validação perfeitamente até a janela de rotação — depois disso, cada requisição falha até que haja uma reinicialização. A produção estrutura o JWKS como um valor em cache com uma tarefa de atualização que sobrescreve o cache antes que as chaves anteriores expirem, além de uma busca de contingência (fallback) em caso de perda de cache (cache miss) para a situação em que chega um token assinado por uma chave mais nova que o cache.

A terceira lacuna é a vinculação de público (audience binding). A Lição 16 introduziu os indicadores de recurso da RFC 8707. Em produção, esse indicador torna-se uma verificação rígida de declaração (claim) em cada requisição. O servidor MCP compara o token.aud com sua própria URL de recurso canônica e rejeita divergências com HTTP 401. Essa é a única defesa contra um servidor MCP upstream (ou um cliente malicioso portando um token destinado a um servidor) que tente replicar esse token contra outro servidor na mesma malha de confiança.

Esta lição trata cada uma dessas lacunas como um primitivo iii. O documento de metadados é um trigger HTTP que retorna a saída de uma função. A rotação de JWKS é um trigger cron que chama auth::rotate-jwks, o qual escreve em state::set("auth/jwks/<issuer>", ...). A validação de JWT é uma função que outros chamam via iii.trigger("auth::validate-jwt", token). O próprio servidor MCP é apenas mais um trigger HTTP que faz chamadas para validação antes do envio. Reinicie o motor: o registro de triggers é reconstruído; o estado sobrevive; a superfície de autenticação torna-se operacional sem reconciliação manual.

O Conceito

RFC 8414 — OAuth Authorization Server Metadata

Um documento em /.well-known/oauth-authorization-server descreve tudo o que um cliente precisa:

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/authorize",
  "token_endpoint": "https://auth.example.com/token",
  "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
  "registration_endpoint": "https://auth.example.com/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["mcp:tools.read", "mcp:tools.invoke"],
  "token_endpoint_auth_methods_supported": ["none", "private_key_jwt"]
}

Um cliente que recebe uma URL de recurso MCP encadeia a descoberta: oauth-protected-resource da RFC 9728 (o documento do servidor de recursos) nomeia o emissor, então oauth-authorization-server (esta RFC) nomeia cada endpoint. O cliente nunca codifica diretamente uma URL de autorização.

O contrato que você verifica antes de confiar em um IdP para MCP:

  • code_challenge_methods_supported inclui S256 (PKCE conforme a RFC 7636).
  • grant_types_supported inclui authorization_code e rejeita password e implicit.
  • registration_endpoint está presente (suporte à RFC 7591).
  • response_types_supported é exatamente ["code"] para OAuth 2.1.

Se algum destes estiver ausente, o servidor MCP recusa-se a ser implantado contra este IdP. O manifesto de implantação está incorreto, não o código.

RFC 9728 (recap) — Protected Resource Metadata

A Lição 16 cobriu a RFC 9728. A diferença em produção: este documento é o único local onde um cliente procura para encontrar os servidores de autorização confiáveis por este servidor MCP. Um único servidor MCP pode aceitar tokens de múltiplos IdPs (um para funcionários, outro para parceiros). A RFC 9728 declara esse conjunto; a RFC 8414 documenta o que cada IdP suporta.

{
  "resource": "https://notes.example.com",
  "authorization_servers": ["https://auth.example.com", "https://partners.example.com"],
  "scopes_supported": ["mcp:tools.invoke"],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://notes.example.com/docs"
}

RFC 7591 — Dynamic Client Registration

Sem DCR, cada cliente MCP (Cursor, Claude Desktop, um agente personalizado) precisa de uma troca fora de banda com o administrador do IdP. Com DCR, o cliente envia:

POST /register
Content-Type: application/json

{
  "redirect_uris": ["http://127.0.0.1:7333/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "mcp:tools.invoke",
  "client_name": "Cursor",
  "software_id": "com.cursor.cursor",
  "software_version": "0.42.0"
}

O servidor responde com client_id e um registration_access_token para atualizações posteriores:

{
  "client_id": "c_3e7f1a",
  "client_id_issued_at": 1769472000,
  "redirect_uris": ["http://127.0.0.1:7333/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "registration_access_token": "regt_b2...",
  "registration_client_uri": "https://auth.example.com/register/c_3e7f1a"
}

token_endpoint_auth_method: none é o padrão correto para clientes MCP que são executados no dispositivo do usuário. Eles obtêm apenas um client_id — nenhum client_secret para ser exfiltrado. O PKCE fornece a prova de posse de que os clientes públicos precisam.

Três armadilhas de produção:

  • O endpoint de registro deve limitar a taxa (rate-limit) por IP de origem. Sem isso, um ator hostil cria scripts para milhões de registros falsos e esgota o espaço de nomes de client_id. O iii torna isso trivial: o trigger HTTP de registro chama uma função auth::rate-limit antes de despachar para o registrador.
  • O software_statement (um JWT assinado que atesta a identidade do cliente) é exigido por alguns IdPs corporativos. A simulação da lição o ignora; a produção conecta uma etapa de verificação que rejeita registros não assinados de qualquer origem que não seja as URLs de redirecionamento de localhost.
  • O registration_access_token deve ser armazenado como um hash, não como texto simples. O roubo desse token significa que o atacante pode reescrever as URLs de redirecionamento do cliente.

RFC 8707 (recap) — Resource Indicators

A Lição 16 estabeleceu a estrutura. A regra de produção: cada requisição de token inclui resource=<canonical-mcp-url>, e o servidor MCP verifica se o token.aud corresponde à sua própria URL de recurso em cada chamada. Se o servidor MCP for acessível em https://notes.example.com/mcp, a URL canônica será https://notes.example.com — o componente de caminho é excluído para que um único servidor hospede múltiplos caminhos sob uma única audiência.

RFC 7636 (recap) — PKCE

O PKCE é obrigatório no OAuth 2.1. O fluxo de código de autorização da lição sempre carrega code_challenge e code_verifier. O servidor rejeita qualquer requisição de token sem um verificador ou com um verificador que não corresponda ao hash do desafio armazenado.

Perfil de Autenticação da Especificação MCP 25-11-2025

A especificação MCP (25-11-2025) é precisa sobre o que a camada de autorização de um servidor MCP deve fazer:

  • Publicar /.well-known/oauth-protected-resource (RFC 9728).
  • Aceitar tokens apenas via Authorization: Bearer ....
  • Validar aud, iss, exp e escopos exigidos por requisição.
  • Responder com WWW-Authenticate carregando Bearer error=... para cada 401 e 403, incluindo os parâmetros scope= e resource= onde aplicável.
  • Rejeitar tokens cujo aud não corresponda ao recurso canônico.
  • Rejeitar tokens cujo iss não esteja na lista authorization_servers dos metadados do recurso protegido.

O rascunho do OAuth 2.1 é a base; RFC 8414/7591/8707/9728 + RFC 7636 formam a superfície; a especificação MCP é o perfil.

Matriz de capacidades de IdP

Nem todo IdP suporta o perfil MCP completo. A matriz abaixo documenta declarações de capacidades fatuais a partir da especificação de 25-11-2025. É uma barreira de implantação, não uma recomendação.

Categoria do IdP Metadados RFC 8414 RFC 7591 DCR Recurso RFC 8707 RFC 7636 S256 PKCE Notas
Auto-hospedado (Keycloak) sim sim sim (desde 24.x) sim IdP de referência para o perfil MCP nesta lição; suporta todas as RFCs de ponta a ponta.
SSO Corporativo (Microsoft Entra ID) sim sim (níveis premium) sim sim A disponibilidade de DCR difere conforme o nível do tenant; verifique no tenant alvo antes de implantar.
SSO Corporativo (Okta) sim sim (Okta CIC / Auth0) sim sim DCR disponível no Auth0 (agora Okta CIC); organizações clássicas do Okta exigem pré-registro do administrador.
IdPs de login social (genérico) varia raramente raramente sim A maioria dos IdPs sociais trata os clientes como parceiros estáticos; não dependa de DCR. Use apenas como fonte de identidade, inserindo seu próprio servidor de autorização compatível com MCP no topo.
Personalizado / caseiro depende depende depende depende Se você distribuir o seu próprio, distribua o perfil completo. Ignorar qualquer uma das quatro RFCs acima quebra o contrato de autenticação do MCP.

Regra de recusa para o manifesto de implantação: se o IdP escolhido não retornar registration_endpoint e não listar S256 em code_challenge_methods_supported, o servidor MCP se recusará a iniciar. Não há modo degradado.

Padrão de rotação de JWKS com iii

O modo de falha de produção é um cache de JWKS desatualizado. Resolva isso com um trigger cron e um cache state::*:

iii.registerTrigger(
    "cron",
    {"schedule": "0 */6 * * *", "name": "auth::jwks-refresh"},
    "auth::rotate-jwks",
)

A cada seis horas, o trigger cron chama auth::rotate-jwks, que busca <issuer>/.well-known/jwks.json e grava em state::set("auth/jwks/<issuer>", {keys, fetched_at}). O validador lê a partir de state::get. Um token cujo kid esteja ausente no cache aciona uma chamada síncrona de contingência (fallback) para auth::rotate-jwks. Isso resolve dois casos ao mesmo tempo: rotação agendada (cron) e janelas de sobreposição de chaves (fallback síncrono).

O formato do estado:

{
  "auth/jwks/https://auth.example.com": {
    "keys": [
      {"kid": "k_2026_03", "kty": "RSA", "n": "...", "e": "AQAB", "alg": "RS256", "use": "sig"},
      {"kid": "k_2026_04", "kty": "RSA", "n": "...", "e": "AQAB", "alg": "RS256", "use": "sig"}
    ],
    "fetched_at": 1772668800
  }
}

Duas chaves simultâneas é o estado estável. Os servidores de autorização realizam a rotação introduzindo a próxima chave (k_2026_04) antes de aposentar a anterior (k_2026_03), de modo que os tokens emitidos com a chave antiga permaneçam válidos até expirarem. O cache mantém a união; o validador escolhe pelo kid.

iii primitive wiring (the part this lesson is actually about)

Cinco primitivos compõem a superfície de autenticação:

# 1. Documento de metadados da RFC 8414
iii.registerTrigger(
    "http",
    {"path": "/.well-known/oauth-authorization-server", "method": "GET"},
    "auth::serve-asm",
)

# 2. Registro dinâmico de clientes da RFC 7591
iii.registerTrigger(
    "http",
    {"path": "/register", "method": "POST"},
    "auth::register-client",
)

# 3. Validação de JWT como uma função chamável (o servidor de recursos a aciona)
iii.registerFunction("auth::validate-jwt", validate_jwt_handler)

# 4. Emissão de step-up para escopo incremental (SEP-835 da L16)
iii.registerFunction("auth::issue-step-up", issue_step_up_handler)

# 5. Rotação de JWKS orientada por cron
iii.registerTrigger(
    "cron",
    {"schedule": "0 */6 * * *"},
    "auth::rotate-jwks",
)
iii.registerFunction("auth::rotate-jwks", rotate_jwks_handler)

O próprio servidor MCP nunca chama a validação diretamente. Ele faz:

result = iii.trigger("auth::validate-jwt", {"token": bearer_token, "resource": self.resource})
if not result["valid"]:
    return {"status": 401, "WWW-Authenticate": result["www_authenticate"]}

Essa indireção é a aposta do iii. Amanhã você pode trocar o validador por um fanout que consulta dois IdPs em paralelo, ou adicionar um emissor de span, ou colocar em cache as validações positivas. O servidor MCP não muda.

Confused-deputy walkthrough with audience binding

O Servidor A (notes.example.com) e o Servidor B (tasks.example.com) registram-se no mesmo servidor de autorização. O Servidor A está comprometido. O atacante pega um token de notas de um usuário e o replica contra o Servidor B.

Validador do Servidor B:

  1. Decodifica o JWT, busca o JWKS por kid e verifica a assinatura.
  2. Verifica o iss contra a lista authorization_servers de seus metadados de recurso protegido. (Passa — mesmo IdP.)
  3. Verifica se aud == "https://tasks.example.com". (Falha — o aud do token é https://notes.example.com.)
  4. Retorna 401 com WWW-Authenticate: Bearer error="invalid_token", error_description="audience mismatch".

A reivindicação de audiência é a única defesa contra esse ataque na camada do protocolo. Ignorá-la por motivos de desempenho é o erro de produção mais comum; o validador deve ser executado em cada requisição, e não apenas no início da sessão.

Failure modes

  • Stale JWKS. O validador rejeita tokens válidos após a rotação de chaves. A correção é o padrão cron+fallback acima. Nunca coloque o JWKS em cache sem uma tarefa de atualização.
  • Missing aud claim. Alguns IdPs omitirão aud por padrão a menos que resource esteja presente na requisição de token. O validador deve rejeitar tokens com aud ausente, em vez de tratar a ausência como um caractere curinga (wildcard).
  • Scope upgrade race. Dois fluxos de step-up concorrentes para o mesmo usuário podem ser bem-sucedidos e produzir dois tokens de acesso com escopos diferentes. O validador deve usar o token apresentado na requisição, em vez de buscar "o escopo atual do usuário" — isso cria uma janela TOCTOU.
  • Registration token theft. Um registration_access_token vazado permite que o atacante reescreva as URLs de redirecionamento. Faça o hash desses tokens em repouso; exija que o cliente apresente o texto simples em cada atualização; rotacione em caso de suspeita.
  • iss not pinned. Um validador que aceita qualquer iss permite que um atacante configure seu próprio servidor de autorização, registre um cliente para o público-alvo e emita tokens. A lista authorization_servers dos metadados do recurso protegido é a lista de permissões; imponha-a.

Use It

code/main.py percorre todo o fluxo de produção usando a biblioteca padrão do Python e um pequeno registro iii_mock que simula iii.registerFunction, iii.registerTrigger, iii.trigger e state::set/get. O fluxo:

  1. O servidor de autorização publica os metadados da RFC 8414 em /.well-known/oauth-authorization-server.
  2. O cliente MCP chama o endpoint de metadados e descobre o endpoint de registro.
  3. O cliente MCP envia uma requisição para /register (RFC 7591) e recebe um client_id.
  4. O cliente MCP executa o fluxo de código de autorização protegido por PKCE (RFC 7636) com indicador de recurso resource (RFC 8707).
  5. O cliente MCP chama uma ferramenta no servidor MCP com Authorization: Bearer ....
  6. O servidor MCP aciona auth::validate-jwt, que lê o JWKS de state::get.
  7. O trigger cron dispara auth::rotate-jwks, substituindo o JWKS no estado.
  8. A próxima chamada valida contra as novas chaves sem necessidade de reinicialização.
  9. Uma tentativa de deputado confuso contra um recurso MCP diferente recebe 401 com incompatibilidade de audiência.

O JWT simulado aqui utiliza HS256 com um segredo compartilhado (para que a lição seja executada apenas na biblioteca padrão). A produção utiliza RS256 ou EdDSA com o padrão JWKS acima; a lógica de validação é idêntica em outros aspectos.

Ship It

Esta lição produz outputs/skill-mcp-auth-iii.md. Dado uma configuração de servidor MCP e um conjunto de capacidades de IdP, a habilidade emite os primitivos iii para registrar, o cronograma de rotação do JWKS, o mapeamento de escopo e as regras de recusa a serem aplicadas quando o IdP não suportar o perfil RFC completo.

Exercícios

  1. Execute code/main.py. Rastreie o fluxo de 9 etapas. Observe onde state::get retorna dados desatualizados imediatamente antes de auth::rotate-jwks sobrescrevê-los, e como a próxima requisição passa a validar contra a nova chave.

  2. Adicione um novo IdP à lista authorization_servers dos metadados de recurso protegido. Emita um token assinado pelo novo IdP e confirme que o validador o aceita. Emita um token assinado por um IdP não listado e confirme que o validador o rejeita com WWW-Authenticate: Bearer error="invalid_token", error_description="iss not allowed".

  3. Implemente auth::rate-limit como uma função iii e chame-a de dentro do trigger HTTP de registro antes da execução do registrador. Use um token-bucket por IP de origem mantido em state::set("auth/ratelimit/<ip>", ...).

  4. Leia a RFC 7591 e identifique dois campos que o manipulador /register da lição não valida. Adicione a validação. (Dica: software_statement e esquema de URI de redirect_uris.)

  5. Leia a seção de autorização da especificação MCP de 25-11-2025. Encontre a única exigência normativa sobre os cabeçalhos WWW-Authenticate que o validador da lição atualmente não emite. Adicione-a.

Key Terms

Termo O que as pessoas dizem O que realmente significa
ASM "Documento de metadados OAuth" JSON do documento da RFC 8414 em /.well-known/oauth-authorization-server
DCR "Registro de cliente por autoatendimento" Fluxo POST /register da RFC 7591
JWKS "Chaves públicas para validação de JWT" JSON Web Key Set, buscado em jwks_uri, indexado por kid
Indicador de recurso "Parâmetro de audiência" Parâmetro resource da RFC 8707 que vincula o token a um único servidor
Declaração aud "Audiência" Declaração do JWT que o validador compara com a URL de recurso canônica
Deputado confuso "Replicação de token" Ataque no qual um token emitido para o Servidor A é apresentado ao Servidor B
Lista de permissões de iss "Servidores de autorização confiáveis" O conjunto nomeado em authorization_servers nos metadados de recurso protegido
Rotação de chaves "JWKS rotativo" Substituição periódica de chaves de assinatura com janelas de sobreposição
Cliente público "Cliente nativo ou de navegador" Cliente OAuth sem client_secret; o PKCE compensa isso
WWW-Authenticate "Cabeçalho de resposta 401/403" Carrega diretivas Bearer error=... que orientam a recuperação do cliente

Leituras Adicionais

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).