Autenticação
A API requer duas camadas independentes de autenticação, validadas nesta ordem:
- mTLS (transport-level) — verificado pelo AWS CloudFront na frente do Vercel.
- 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 oaccount_public_idincluso 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:
| Header | Significado |
|---|---|
cloudfront-viewer-mtls-client-cert-subject | Subject DN do certificado cliente |
cloudfront-viewer-mtls-client-cert-issuer | Issuer DN |
cloudfront-viewer-mtls-client-cert-serial-number | Serial number |
cloudfront-viewer-mtls-client-cert-fingerprint | Fingerprint SHA-256 |
cloudfront-viewer-mtls-client-cert-validity-not-after | Expiração |
cloudfront-viewer-mtls-client-cert | PEM 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
kidque 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
| Claim | Origem | Descrição |
|---|---|---|
iss | Authorization Server | O identificador dele, como URL: https://account[.{env}].kobana.com.br. |
iat | Timestamp Unix | Emissão. |
aud | Esta API, como URL | https://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. |
jti | public_id do AccessToken | ID único, usado para revogação. |
sub | users.public_id | A pessoa, nunca o tenant. Resolve o operador e as permissões dele quando src=user; em token de máquina fica para auditoria. |
org | accounts.public_id | O tenant, e é obrigatória. É o que escopa toda resposta. Token sem ela é recusado, em vez de cair no sub. |
src | user / system / api | Origem do token. |
exp | obrigatória | Expiraçã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. |
scope | string | Scopes 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.
| Scope | Concede |
|---|---|
inbox/workspaces | Leitura de Workspaces |
inbox/inboxes | Leitura de Inboxes e destinatários permitidos |
inbox/emails | Leitura de Emails (e download do .eml) |
inbox/agents | Leitura de Agents, versões de prompt e catálogo de modelos (/models) |
inbox/agent_runs | Leitura de AgentRuns |
inbox/usage | Leitura de uso e custo agregados (/usage/*) |
inbox/webhooks | Leitura de Webhooks e suas entregas |
inbox/system_events | Leitura de SystemEvents |
inbox/secrets | Leitura de Secrets — chave, notas e metadados. O valor nunca é devolvido por endpoint nenhum |
inbox/workflow_runs | Leitura de WorkflowRuns e de cada passo de cada execução |
inbox/all | Catch-all do eixo de recurso — lê todos os recursos acima e não concede verbo nenhum |
write | Verbo 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:
- Verifica
X-Origin-Shared-Secret—403se ausente/divergente. - Verifica headers mTLS (
cloudfront-viewer-mtls-client-cert-fingerprintpresente). - Extrai
Authorization: Bearer <jwt>. jwtVerifycom allowlist de exatamente um algoritmo (RS256) contra a chave pública que o Authorization Server publica. Rejeita se a assinatura falhar. Otypnão é conferido de propósito: o Authorization Server estampaat+jwt(RFC 9068), e exigir"JWT"recusaria todo token real.- Valida
iss, que oaudcontém o identificador desta API, e queexpestá presente e no futuro. - Resolve a organização por
orge então o workspace dentro dela (X-Workspace-Idescolhe um quando a organização tem mais de um). - Cheque scope vs endpoint.
- Checagem opcional de revogação por
jti. - 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
| HTTP | code | Causa |
|---|---|---|
| 401 | mtls_required | Certificado cliente ausente/inválido |
| 401 | invalid_token | JWT malformado ou assinatura inválida |
| 401 | invalid_audience | aud não corresponde ao ambiente |
| 401 | token_expired | exp no passado |
| 401 | token_revoked | jti listado em RevokedTokens |
| 403 | insufficient_scope | Token sem o scope exigido |
| 403 | insufficient_permission | A 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 |
| 403 | forbidden | message 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 |
| 400 | workspace_required | A 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 |
| 500 | as_unavailable | As 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.