Pular para o conteúdo principal

Autenticação

A API requer duas camadas independentes de autenticação, validadas nesta ordem:

  1. mTLS (transport-level) — verificado pelo AWS CloudFront na frente do Vercel.
  2. Bearer JWT (application-level) — verificado pela aplicação Next.js.

Falha em qualquer camada rejeita a requisição.

mTLS​

  • Terminator: AWS CloudFront com MutualTlsAuthenticationMode=require, cobrindo o host da API inteiro (api.inbox.*). O endpoint de ingest do SNS nesse host (/ingest) é a única exceção. A aplicação e o painel admin ficam em hosts separados, servidos por distributions sem mTLS.
  • CA: uma CA por ambiente (development, sandbox, production). Certificados emitidos em uma CA não validam em outra.
  • Certificado cliente: emitido por workspace/consumer. Validade típica de um ano com renovação automatizada.
  • CN/SAN: CN={consumer-slug}.inbox.kobana.com.br, com o account_public_id incluso como SAN.
  • Revogação: implementada no app via tabela RevokedClientCerts, indexada pela fingerprint SHA-256. O CloudFront não consulta CRL/OCSP automaticamente.

Headers injetados pelo CloudFront​

Quando o mTLS é aceito, o CloudFront injeta os seguintes headers na requisição que chega ao origin:

HeaderSignificado
cloudfront-viewer-mtls-client-cert-subjectSubject DN do certificado cliente
cloudfront-viewer-mtls-client-cert-issuerIssuer DN
cloudfront-viewer-mtls-client-cert-serial-numberSerial number
cloudfront-viewer-mtls-client-cert-fingerprintFingerprint SHA-256
cloudfront-viewer-mtls-client-cert-validity-not-afterExpiração
cloudfront-viewer-mtls-client-certPEM completo (se habilitado)

A aplicação valida: presença do header, expiração (defense-in-depth) e lookup de revogação por fingerprint.

Um header adicional X-Origin-Shared-Secret é obrigatório no middleware para impedir requisições diretas no *.vercel.app que burlem o CloudFront. Ausente/divergente → 403.

Bearer JWT​

O token é um access token emitido pelo Authorization Server da Kobana (account.kobana.com.br). Esta API apenas o verifica: não guarda chave de assinatura e não consegue emitir token nenhum.

  • Algoritmo: RS256, e só RS256.
  • Chave: pública, e descoberta, não configurada. O identificador do issuer é uma URL, e o endpoint das chaves (jwks_uri) é lido do documento de metadata RFC 8414 publicado sob ela (/.well-known/oauth-authorization-server). Quando o Authorization Server rotaciona a chave, esta API acompanha sozinha — sem redeploy, sem janela coordenada.
  • Quando o Authorization Server é chamado: nunca para verificar um token — a checagem da assinatura é local. O que vem dele é o conjunto de chaves, e ele é cacheado. Uma busca só acontece quando o cache está vazio (processo recém-iniciado), quando o conjunto em cache tem mais de uma hora, ou quando um token traz um kid que não está no cache (uma rotação; no máximo uma rebusca a cada 30 segundos). Cada busca tem timeout de 5 segundos. Fora desses momentos o Authorization Server pode estar fora do ar sem que nenhuma requisição perceba.

Isto substituiu um segredo HS512 compartilhado. Naquele esquema o mesmo valor assinava e verificava, então um vazamento em qualquer um dos lados era vazamento da própria emissão. Nada muda em como você envia o token; o que muda é que um token assinado com o segredo antigo deixa de ser aceito.

Claims​

ClaimOrigemDescrição
issAuthorization ServerO identificador dele, como URL: https://account[.{env}].kobana.com.br.
iatTimestamp UnixEmissão.
audEsta API, como URLhttps://api.inbox[.{env}].kobana.com.br. Pode ser uma lista — a regra é contém, não igualdade, então um token pode endereçar mais de uma API Kobana. Qualquer outra audience é rejeitada.
jtipublic_id do AccessTokenID único, usado para revogação.
subusers.public_idA pessoa, nunca o tenant. Resolve o operador e as permissões dele quando src=user; em token de máquina fica para auditoria.
orgaccounts.public_idO tenant, e é obrigatória. É o que escopa toda resposta. Token sem ela é recusado, em vez de cair no sub.
srcuser / system / apiOrigem do token.
expobrigatóriaExpiração Unix. Token sem ela é recusado: "vive até alguém revogar" não é política de expiração, e a rotina de limpeza de revogações acaba apagando a linha sem expiração, reabrindo um token vazado.
scopestringScopes separados por espaço.

Scopes​

Os scopes têm dois eixos ortogonais: o recurso, sempre prefixado pelo app (inbox/emails), e o verbo, um scope global sem prefixo de app (write). Recurso sem verbo significa leitura. Escrever exige os dois: o scope do recurso e o verbo.

ScopeConcede
inbox/workspacesLeitura de Workspaces
inbox/inboxesLeitura de Inboxes e destinatários permitidos
inbox/emailsLeitura de Emails (e download do .eml)
inbox/agentsLeitura de Agents, versões de prompt e catálogo de modelos (/models)
inbox/agent_runsLeitura de AgentRuns
inbox/usageLeitura de uso e custo agregados (/usage/*)
inbox/webhooksLeitura de Webhooks e suas entregas
inbox/system_eventsLeitura de SystemEvents
inbox/secretsLeitura de Secrets — chave, notas e metadados. O valor nunca é devolvido por endpoint nenhum
inbox/workflow_runsLeitura de WorkflowRuns e de cada passo de cada execução
inbox/allCatch-all do eixo de recurso — lê todos os recursos acima e não concede verbo nenhum
writeVerbo global: toda escrita (POST, PUT, PATCH, DELETE) nos recursos que o token carrega — criar, atualizar e excluir, além de enviar .eml, reprocessar, testar webhook, rotacionar segredo e reenviar entrega

Dois exemplos: inbox/emails lê e-mails e não escreve; inbox/emails write também envia, reprocessa e exclui e-mails, e continua sem alcançar webhooks.

A forma granular inbox/emails:write é exceção da gramática, não o formato padrão: satisfaz os dois eixos daquele recurso sozinha e existe para "lê um recurso, escreve outro". inbox/* é curinga de cadastro e nunca é emitido num token — a comparação aqui é sempre literal.

read é scope default do Doorkeeper e não significa nada aqui: leitura é o recurso sem verbo.

Tokens sem o scope exigido pelo endpoint recebem 403 insufficient_scope.

Pipeline de validação​

O middleware de auth executa os passos abaixo nesta ordem:

  1. Verifica X-Origin-Shared-Secret — 403 se ausente/divergente.
  2. Verifica headers mTLS (cloudfront-viewer-mtls-client-cert-fingerprint presente).
  3. Extrai Authorization: Bearer <jwt>.
  4. jwtVerify com allowlist de exatamente um algoritmo (RS256) contra a chave pública que o Authorization Server publica. Rejeita se a assinatura falhar. O typ não é conferido de propósito: o Authorization Server estampa at+jwt (RFC 9068), e exigir "JWT" recusaria todo token real.
  5. Valida iss, que o aud contém o identificador desta API, e que exp está presente e no futuro.
  6. Resolve a organização por org e então o workspace dentro dela (X-Workspace-Id escolhe um quando a organização tem mais de um).
  7. Cheque scope vs endpoint.
  8. Checagem opcional de revogação por jti.
  9. Anexa auth_context = { workspace, jti, sub, src, scopes } à requisição.

Isolamento multi-tenant​

Toda query é automaticamente filtrada pelo Workspace derivado do org. Clientes não podem sobrescrever o workspace via body ou query — qualquer campo desse tipo é ignorado. Recursos fora do workspace resolvido retornam 404 not_found (em vez de 403 forbidden) para não vazar existência.

Modos de falha​

HTTPcodeCausa
401mtls_requiredCertificado cliente ausente/inválido
401invalid_tokenJWT malformado ou assinatura inválida
401invalid_audienceaud não corresponde ao ambiente
401token_expiredexp no passado
401token_revokedjti listado em RevokedTokens
403insufficient_scopeToken sem o scope exigido
403insufficient_permissionA pessoa por trás do token (src=user) não tem a permissão que o endpoint exige nesta organização. message é o slug da permissão que falta, para você saber o que pedir ao administrador
403forbiddenmessage diz o motivo: unknown_organization (o claim org aponta uma account que esta API não conhece), no_membership (a pessoa não tem membership viva nessa organização), unknown_workspace (X-Workspace-Id aponta um workspace fora da organização — indistinguível de um inexistente), ou o X-Origin-Shared-Secret estava ausente/errado
400workspace_requiredA organização tem mais de um workspace e nenhum foi escolhido. Não é falha de autorização — o token está bom; envie X-Workspace-Id
500as_unavailableAs chaves de assinatura não puderam ser buscadas no Authorization Server: a metadata ou o endpoint JWKS deu timeout, respondeu erro, redirecionou ou serviu um documento que não bate com o issuer. A verificação em si é local, então isso só acontece nos momentos listados em Quando o Authorization Server é chamado acima — cache frio, conjunto de chaves com mais de uma hora, ou um kid que ainda não vimos. Não é problema do seu token — tente de novo em instantes. É 5xx de propósito: um 401 aqui mandaria você depurar uma credencial perfeita durante a nossa queda.

Veja Erros para o envelope completo.