Autenticación
La API requiere dos capas independientes de autenticación, validadas en este orden:
- mTLS (nivel de transporte) — verificado por AWS CloudFront frente a Vercel.
- Bearer JWT (nivel de aplicación) — verificado por la aplicación Next.js.
Un fallo en cualquiera de las dos capas rechaza la solicitud.
mTLS
- Terminador: AWS CloudFront con
MutualTlsAuthenticationMode=require, cubriendo todo el host de la API (api.inbox.*). El endpoint de ingest de SNS en ese host (/ingest) es la única excepción. La aplicación y el panel de administración están en hosts separados, servidos por distributions sin mTLS. - CA: una CA por entorno (
development,sandbox,production). Los certificados emitidos bajo una CA no validan contra otra. - Certificado de cliente: emitido por workspace/consumidor de la app. Validez típica de un año con renovación automatizada.
- CN/SAN:
CN={consumer-slug}.inbox.kobana.com.br, con elaccount_public_idincluido como SAN. - Revocación: implementada a nivel de aplicación mediante la tabla
RevokedClientCerts, indexada por fingerprint SHA-256. CloudFront no consulta CRL/OCSP automáticamente.
Headers inyectados por CloudFront
Una vez que el mTLS es aceptado, CloudFront inyecta los siguientes headers en la solicitud que llega al origin:
| Header | Significado |
|---|---|
cloudfront-viewer-mtls-client-cert-subject | Subject DN del certificado de cliente |
cloudfront-viewer-mtls-client-cert-issuer | Issuer DN |
cloudfront-viewer-mtls-client-cert-serial-number | Número de serie |
cloudfront-viewer-mtls-client-cert-fingerprint | Fingerprint SHA-256 |
cloudfront-viewer-mtls-client-cert-validity-not-after | Expiración del certificado |
cloudfront-viewer-mtls-client-cert | PEM completo (si está habilitado) |
La aplicación valida: presencia del header, expiración (defensa en profundidad) y una búsqueda de revocación local por fingerprint.
Un header adicional X-Origin-Shared-Secret es exigido por el middleware para evitar accesos directos a la URL subyacente *.vercel.app que eviten CloudFront. Ausente/incorrecto → 403.
Bearer JWT
El token es un access token emitido por el Authorization Server de Kobana
(account.kobana.com.br). Esta API solo lo verifica: no guarda ninguna clave
de firma y no puede emitir tokens.
- Algoritmo:
RS256, y soloRS256. - Clave: pública, y descubierta, no configurada. El identificador del
issuer es una URL, y el endpoint de claves (
jwks_uri) se lee del documento de metadata RFC 8414 publicado bajo ella (/.well-known/oauth-authorization-server). Cuando el Authorization Server rota su clave, esta API la adopta sola — sin redespliegue ni ventana coordinada. - Cuándo se contacta al Authorization Server: nunca para verificar un
token — la comprobación de la firma es local. Lo que viene de él es el
conjunto de claves, y se cachea. Una descarga solo ocurre cuando el caché
está vacío (un proceso recién iniciado), cuando el conjunto cacheado tiene más
de una hora, o cuando un token trae un
kidque no está en caché (una rotación; como máximo una descarga cada 30 segundos). Cada descarga tiene un timeout de 5 segundos. Fuera de esos momentos el Authorization Server puede estar caído sin que ninguna solicitud lo note.
Esto reemplazó un secreto HS512 compartido. En aquel esquema el mismo valor
firmaba y verificaba, así que una fuga en cualquiera de los dos lados era una
fuga de la propia emisión. Nada cambia en cómo envías el token; lo que cambia
es que un token firmado con el secreto anterior deja de aceptarse.
Claims
| Claim | Origen | Descripción |
|---|---|---|
iss | Authorization Server | Su identificador, como URL: https://account[.{env}].kobana.com.br. |
iat | Timestamp Unix | Momento de emisión. |
aud | Esta API, como URL | https://api.inbox[.{env}].kobana.com.br. Puede ser una lista — la regla es contiene, no igualdad, así que un token puede dirigirse a más de una API de Kobana. Cualquier otra audience es rechazada. |
jti | public_id del AccessToken | ID único de token, usado para revocación. |
sub | users.public_id | La persona, nunca el tenant. Resuelve al operador y sus permisos cuando src=user; en un token de máquina queda para auditoría. |
org | accounts.public_id | El tenant, y es obligatoria. Es lo que acota toda respuesta. Un token sin ella se rechaza, en lugar de caer en sub. |
src | user / system / api | Origen del token. |
exp | obligatoria | Expiración Unix. Un token sin ella se rechaza: "vive hasta que alguien lo revoque" no es una política de expiración, y la limpieza de revocaciones acaba borrando la fila sin expiración, rehabilitando un token filtrado. |
scope | string | Lista de scopes separados por espacio. |
Scopes
Los scopes tienen dos ejes ortogonales: el recurso, siempre prefijado por la app (inbox/emails), y el verbo, un scope global sin prefijo de app (write). Un recurso sin verbo significa lectura. Escribir exige ambos: el scope del recurso y el verbo.
| Scope | Concede |
|---|---|
inbox/workspaces | Lectura de Workspaces |
inbox/inboxes | Lectura de Inboxes y destinatarios permitidos |
inbox/emails | Lectura de Emails (y descarga del .eml) |
inbox/agents | Lectura de Agents, versiones de prompt y catálogo de modelos (/models) |
inbox/agent_runs | Lectura de AgentRuns |
inbox/usage | Lectura de uso y costo agregados (/usage/*) |
inbox/webhooks | Lectura de Webhooks y sus entregas |
inbox/system_events | Lectura de SystemEvents |
inbox/secrets | Lectura de Secrets — clave, notas y metadatos. El valor nunca se devuelve por ningún endpoint |
inbox/workflow_runs | Lectura de WorkflowRuns y de cada paso de cada ejecución |
inbox/all | Catch-all del eje de recurso — lee todos los recursos anteriores y no concede ningún verbo |
write | Verbo global: toda escritura (POST, PUT, PATCH, DELETE) sobre los recursos que lleva el token — crear, actualizar y eliminar, además de enviar .eml, reprocesar, probar webhook, rotar el secreto y reenviar una entrega |
Dos ejemplos: inbox/emails lee correos y no escribe; inbox/emails write también envía, reprocesa y elimina correos, y sigue sin alcanzar webhooks.
La forma granular inbox/emails:write es una excepción de la gramática, no el formato estándar: satisface ambos ejes de ese recurso por sí sola y existe para "lee un recurso, escribe otro". inbox/* es un comodín de registro y nunca se emite en un token — la comparación aquí es siempre literal.
read es un scope por defecto de Doorkeeper y no significa nada aquí: leer es el recurso sin verbo.
Los tokens sin el scope exigido por el endpoint reciben 403 insufficient_scope.
Pipeline de validación
El middleware de autenticación ejecuta los siguientes pasos en orden:
- Verifica
X-Origin-Shared-Secret—403si está ausente/diverge. - Verifica los headers mTLS (
cloudfront-viewer-mtls-client-cert-fingerprintpresente). - Extrae
Authorization: Bearer <jwt>. jwtVerifycon una allowlist de exactamente un algoritmo (RS256) contra la clave pública que publica el Authorization Server. Rechaza en caso de fallo de firma. Eltypno se verifica a propósito: el Authorization Server estampaat+jwt(RFC 9068), y exigir"JWT"rechazaría todo token real.- Valida
iss, que elaudcontenga el identificador de esta API, y queexpesté presente y en el futuro. - Resuelve la organización por
orgy luego el workspace dentro de ella (X-Workspace-Idelige uno cuando la organización tiene más de uno). - Verifica el scope contra el endpoint.
- Verificación opcional de revocación por
jti. - Adjunta
auth_context = { workspace, jti, sub, src, scopes }a la solicitud.
Aislamiento multi-tenant
Cada endpoint filtra su query por el Workspace derivado de org. Los clientes no pueden sobrescribir el workspace vía body o query — cualquier campo de ese tipo es ignorado. Los recursos fuera del workspace resuelto devuelven 404 not_found en lugar de 403 forbidden, para evitar filtrar su existencia.
Modos de fallo
| HTTP | code | Causa |
|---|---|---|
| 401 | mtls_required | Certificado de cliente ausente o inválido |
| 401 | invalid_token | JWT malformado o firma inválida |
| 401 | invalid_audience | aud no corresponde al entorno |
| 401 | token_expired | exp en el pasado |
| 401 | token_revoked | jti listado en RevokedTokens |
| 403 | insufficient_scope | El token carece del scope requerido |
| 403 | insufficient_permission | La persona detrás del token (src=user) no tiene el permiso que el endpoint exige en esta organización. message es el slug del permiso que falta, para que sepas qué pedirle al administrador |
| 403 | forbidden | message dice el motivo: unknown_organization (el claim org apunta a una cuenta que esta API no conoce), no_membership (la persona no tiene membresía viva en esa organización), unknown_workspace (X-Workspace-Id apunta a un workspace fuera de la organización — indistinguible de uno inexistente), o el X-Origin-Shared-Secret estaba ausente/incorrecto |
| 400 | workspace_required | La organización tiene más de un workspace y no se eligió ninguno. No es un fallo de autorización — el token está bien; envía X-Workspace-Id |
| 500 | as_unavailable | No se pudieron descargar las claves de firma del Authorization Server: su metadata o su endpoint JWKS dio timeout, respondió con error, redirigió o sirvió un documento que no coincide con el issuer. La verificación en sí es local, así que esto solo ocurre en los momentos listados en Cuándo se contacta al Authorization Server arriba — caché frío, conjunto de claves con más de una hora, o un kid que aún no hemos visto. No es un problema de tu token — reintenta en unos instantes. Es 5xx a propósito: un 401 aquí te mandaría a depurar una credencial perfecta durante nuestra caída. |
Consulta Errores para el envelope de error completo.