mcp-stepup-gateway
mcp-stepup-gateway
Um gateway MCP que exige passkey (WebAuthn) sob demanda -- "step-up
auth" -- antes de permitir que um cliente remoto (Claude.ai, via Custom
Connector) leia ou escreva num vault Obsidian protegido pelo
enquire-mcp. Login Google e
allowlist (como no mcp-oauth-gateway) decidem quem pode conectar;
este projeto decide, tool a tool, o que essa pessoa pode fazer sem
reprovar identidade de novo, e o que exige um toque fresco na passkey.
Nasceu de um caso concreto: o mcp-oauth-gateway/enquire-mcp-gateway ja
resolvem "autenticar quem conecta" (OAuth + allowlist). O que faltava era
uma segunda camada: mesmo dentro da allowlist, nem toda tool call deveria
ser igualmente livre. Ler uma nota e barato; apagar ou reescrever conteudo
do vault via uma LLM que pode estar sob prompt injection não é. Este
gateway adiciona essa distincao sem tocar no enquire-mcp em si.
Por que isso existe
Um cliente MCP remoto autenticado por OAuth ainda e, do ponto de vista do vault, "uma LLM com acesso total". Isso e um problema em dois eixos:
A LLM pode ser manipulada. Conteudo malicioso numa nota, ou numa resposta de ferramenta, pode tentar instruir o agente a apagar ou sobrescrever coisas -- prompt injection nao e hipotetico.
"Autenticado uma vez" nao deveria significar "autorizado para sempre". Uma sessao OAuth de longa duracao nao deveria dar a mesma LLM permissao irrestrita de escrita indefinidamente, sem nenhuma prova fresca de presenca humana.
A solucao aqui e um modelo de niveis de risco por tool, com um handle de capacidade de vida curta (15 min) que autoriza leitura, e uma confirmacao com passkey por chamada que autoriza qualquer escrita ou delecao -- renderizada a partir dos argumentos reais que o servidor recebeu, nunca de texto que a LLM controla.
Arquitetura
Cliente MCP remoto (Claude.ai, via Custom Connector)
│ HTTPS (OAuth Google + allowlist -- fora do escopo deste
│ README; ver mcp-oauth-gateway/enquire-mcp-gateway)
▼
┌───────────────────────────────────────────────────────────┐
│ gateway │
│ │
│ StepUpMiddleware -- por tool call: │
│ 1. policy.yaml decide o nivel (0/1/2) da tool │
│ 2. L0 (tools de auth) -- sempre passa │
│ 3. L1 (leitura) -- exige handle de sessao valido │
│ (senao devolve AUTH_REQUIRED + URL de unlock) │
│ 4. L2 (escrita/delete) -- exige confirmacao fresca │
│ por chamada (args_digest HMAC liga a aprovacao aos │
│ argumentos EXATOS; senao devolve CONFIRMATION_REQUIRED) │
│ │
│ Tools injetadas (nivel 0, sempre disponiveis): │
│ vault_auth_unlock / vault_auth_check / vault_auth_status │
└──────────────────────────┬───────────────────────────────────┘
│ Streamable HTTP + bearer
▼
┌───────────────────────────────────────────────────────────┐
│ auth-service │
│ │
│ WebAuthn (passkey) -- registro, challenges de unlock e de │
│ confirmacao, sessoes (SQLite), audit log append-only. │
│ So alcancavel via rotas /internal (X-Gateway-Key) do │
│ gateway, ou pelas telas publicas /unlock, /confirm, │
│ /register (esta ultima so com token de bootstrap). │
└──────────────────────────┬───────────────────────────────────┘
│ nunca fala com o backend
│ diretamente -- so autentica
▼
(o handle/token volta ao Claude via
gateway, que entao repassa a chamada
original ao backend)
│
▼
┌───────────────────────────────────────────────────────────┐
│ backend │
│ enquire-mcp (serve-http, vault Obsidian) │
└───────────────────────────────────────────────────────────┘O gateway nunca guarda credencial nenhuma -- so fala com o auth-service
(rotas internas, autenticadas por GATEWAY_KEY) para perguntar "este
handle autoriza esta tool?" ou "esta confirmacao aprovou exatamente estes
argumentos?". O humano nunca digita nem cola nada no chat: toda a
ceremonia de passkey acontece no navegador, numa URL que o auth-service
serve.
Niveis de risco
Nivel | O que exige | Exemplo |
L0 | Nada -- sempre liberado |
|
L1 | Handle de sessao valido (TTL absoluto 15 min, idle 5 min) |
|
L2 | Confirmacao com passkey por chamada, ligada aos argumentos exatos via |
|
policies/policy.yaml mapeia cada tool do backend para um nivel.
Deny-by-default: qualquer tool nao mapeada explicitamente cai no nivel
mais restritivo (default_level: 2) -- se o enquire-mcp ganhar uma tool
nova numa atualizacao (o backend roda npx -y, entao pode mudar de versao
a qualquer subida), ela chega protegida, nao aberta. Ver os comentarios
no proprio policies/policy.yaml para a proveniencia dos nomes de tool
usados e o que ainda precisa ser verificado ao vivo antes de producao.
Setup
Requer Docker e Docker Compose. Os tres servicos (gateway, auth-service,
backend) sobem juntos.
1. Variaveis de ambiente
cp .env.example .env # Windows: Copy-Item .env.example .envPreencha, na raiz do repo:
Google OAuth (
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,PUBLIC_BASE_URL,ALLOWED_EMAILS) -- mesmo padrao domcp-oauth-gateway; veja o README daquele projeto para o passo a passo de criar o OAuth Client no Google Cloud Console.WebAuthn (
WEBAUTHN_RP_ID,WEBAUTHN_RP_NAME,PUBLIC_ORIGIN,GATEWAY_KEY,DIGEST_KEY) -- ver a advertencia abaixo antes de definirWEBAUTHN_RP_ID. GereGATEWAY_KEYeDIGEST_KEYcomopenssl rand -hex 32.Backend (
BACKEND_BEARER_TOKEN,OBSIDIAN_VAULT_PATH) -- o token compartilhado entregatewayebackend, e o caminho no host do vault Obsidian a proteger.
WEBAUTHN_RP_IDe PERMANENTE. E o dominio (sem porta, sem protocolo) que fica embutido na propria assinatura WebAuthn de cada passkey registrada. Mudar esse valor depois do primeiro registro invalida TODAS as passkeys -- todo mundo precisa registrar de novo, com um novo bootstrap. Decida o dominio definitivo (o mesmo host quePUBLIC_BASE_URL, semhttps://) antes de registrar a primeira passkey, nao depois. Oauth-servicese recusa a subir sem esta variavel definida (src/authsvc/config.py) -- de proposito: um default silencioso aqui seria pior que falhar no boot.
2. Subir o stack
docker compose --env-file .env -f docker/docker-compose.yml up -d --build
docker compose --env-file .env -f docker/docker-compose.yml logs -f auth-service--env-file .env nao e opcional -- o Docker Compose resolve ${VAR}
do compose relativo ao diretorio do proprio arquivo (docker/), nao da
raiz do repo. Rodar sem essa flag faz OBSIDIAN_VAULT_PATH cair num
fallback silencioso (docker/vault, vazio) em vez do vault real, sem
nenhum erro visivel. Ver o comentario Uso: no topo de
docker/docker-compose.yml para o detalhe completo (achado do review da
Task 17).
3. Registrar a primeira passkey (bootstrap)
Nos logs do auth-service, procure:
[bootstrap] token de registro (10 min): <token>Abra <PUBLIC_BASE_URL>/register?t=<token> no navegador de um dispositivo
com passkey (celular, ou um gerenciador de senhas compativel) e complete o
registro. O token expira em 10 minutos; se perder o prazo, reinicie o
auth-service (docker compose restart auth-service) para gerar outro --
isso tambem zera sessoes/challenges pendentes
(SESSION_PURGE_ON_START=true por padrao).
Registre pelo menos duas passkeys (celular + gerenciador de senhas, por exemplo) enquanto o token de bootstrap ainda vale -- e a mitigacao deste projeto para "perdi o dispositivo": nao ha codigo de recuperacao (decisao deliberada; ver a spec de design, secao de decisoes abertas).
4. Conectar como Custom Connector
Em claude.ai -> Settings -> Connectors -> Add custom connector, cole
<PUBLIC_BASE_URL>/mcp. Deixe os campos de OAuth Client vazios (registro
dinamico). Depois de logar com uma conta Google presente em
ALLOWED_EMAILS, o roteiro completo de verificacao (unlock, leitura,
escrita com confirmacao, e o teste de duas conversas) esta em
tests/integration/test_e2e_manual.md.
Limitações conhecidas
A8 -- Pessoa B abrindo a mesma conversa dentro da janela de 15 minutos herda o handle. Este e o furo real, ja documentado e aceito por design, do modelo de handle: o handle de sessao (L1) nao esta ligado a identidade de quem esta lendo a conversa naquele momento, so a conversa onde ele nasceu. Se a conta Claude e compartilhada e a Pessoa B abre a mesma conversa que a Pessoa A destravou -- nao uma conversa nova -- dentro dos 15 minutos de TTL absoluto (ou 5 min de idle), B herda a capacidade de leitura (L1) que A obteve. Mitigado por TTL curto, idle timeout, e binding adicional ao
Mcp-Session-Idquando o cliente o fornece de forma estavel -- mas nao eliminado. Escrita (L2) permanece inatingivel para B em qualquer caso, porque exige uma assinatura de passkey fresca por chamada. Ver a secao A8 da spec de design (docs/superpowers/specs/2026-08-16-mcp-stepup-auth-proxy-design.md) para a analise completa de ameaca. Isto nao e um bug a ser corrigido silenciosamente -- e uma limitacao conhecida do modelo de handle compartilhado por conversa, e o Passo 7 do roteiro emtests/integration/test_e2e_manual.mdexiste justamente para provar que o caso distinto (conversa nova) esta corretamente bloqueado.Rate limiting nao esta conectado a nenhum caminho de requisicao. O modulo
src/authsvc/ratelimit.py(janela deslizante em memoria, classeJanela) existe e tem testes proprios, mas nenhuma rota doauth-servicenem dogatewayo instancia ou chama -- ele nao esta "wireado". Na pratica, isso significa que a mitigacao de "brute force de handle" e "varredura sistematica do vault" descrita na secao 20 (Testes de seguranca) e na secao 14 (Protecao contra prompt injection, item 4) da spec de design ainda nao existe em producao, apesar do codigo base estar pronto. Isto e uma lacuna real, nao coberta por nenhum outro controle deste projeto --policies/policy.yamltem uma secaorate_limitscom valores de exemplo (level_1: { calls: 60, window_s: 300 }), mas nada nogateway_main.pyou nosrc/stepup/middleware.pyatual le esses valores para de fato limitar chamadas. Antes de expor este gateway a um uso com volume real (nao so um unico usuario confiavel), conectarratelimit.Janelaao caminho de L1 (e, idealmente, tambem a tentativas de challenge/confirmacao noauth-service) deveria ser tratado como prioridade, nao como polimento.Demais limitacoes estruturais (sem supervisao de processo, segredo
BACKEND_BEARER_TOKENcompartilhado sem escopo por chamador, exposicao publica exige seu proprio tunel) sao as mesmas domcp-oauth-gateway, do qual este projeto herda a camada de OAuth/allowlist -- ver o README daquele projeto para os detalhes.
Testes
# Windows
.venv\Scripts\pytest.exe -v
# Linux/macOS
.venv/bin/pytest -vCobrem: a politica de autorizacao (src/stepup/policy.py), o middleware de
step-up (niveis, AUTH_REQUIRED/CONFIRMATION_REQUIRED), o auth-service
(WebAuthn, sessoes, challenges, confirmacoes, audit log, digest HMAC), e a
resolucao de configuracao do docker-compose.yml (incluindo os dois modos
de erro do --env-file .env ausente).
O roteiro ponta a ponta contra um cliente MCP real e uma passkey fisica
nao esta nesta suite -- ver tests/integration/test_e2e_manual.md.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Remote MCP for Copilot CLI switch gate MCP, structured receipts, audit logs, and reviewer-ready evid
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
MCP server teaching AI agents to implement TideCloak: auth, E2EE, IGA, security analysis
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/joaorura/mcp-stepup-gateway'
If you have feedback or need assistance with the MCP directory API, please join our Discord server