Saltar al contenido principal

Autenticación

La API requiere dos capas independientes de autenticación, validadas en este orden:

  1. mTLS (nivel de transporte) — verificado por AWS CloudFront frente a Vercel.
  2. 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 el account_public_id incluido 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:

HeaderSignificado
cloudfront-viewer-mtls-client-cert-subjectSubject DN del certificado de cliente
cloudfront-viewer-mtls-client-cert-issuerIssuer DN
cloudfront-viewer-mtls-client-cert-serial-numberNúmero de serie
cloudfront-viewer-mtls-client-cert-fingerprintFingerprint SHA-256
cloudfront-viewer-mtls-client-cert-validity-not-afterExpiración del certificado
cloudfront-viewer-mtls-client-certPEM 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 solo RS256.
  • 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 kid que 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​

ClaimOrigenDescripción
issAuthorization ServerSu identificador, como URL: https://account[.{env}].kobana.com.br.
iatTimestamp UnixMomento de emisión.
audEsta API, como URLhttps://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.
jtipublic_id del AccessTokenID único de token, usado para revocación.
subusers.public_idLa persona, nunca el tenant. Resuelve al operador y sus permisos cuando src=user; en un token de máquina queda para auditoría.
orgaccounts.public_idEl tenant, y es obligatoria. Es lo que acota toda respuesta. Un token sin ella se rechaza, en lugar de caer en sub.
srcuser / system / apiOrigen del token.
expobligatoriaExpiració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.
scopestringLista 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.

ScopeConcede
inbox/workspacesLectura de Workspaces
inbox/inboxesLectura de Inboxes y destinatarios permitidos
inbox/emailsLectura de Emails (y descarga del .eml)
inbox/agentsLectura de Agents, versiones de prompt y catálogo de modelos (/models)
inbox/agent_runsLectura de AgentRuns
inbox/usageLectura de uso y costo agregados (/usage/*)
inbox/webhooksLectura de Webhooks y sus entregas
inbox/system_eventsLectura de SystemEvents
inbox/secretsLectura de Secrets — clave, notas y metadatos. El valor nunca se devuelve por ningún endpoint
inbox/workflow_runsLectura de WorkflowRuns y de cada paso de cada ejecución
inbox/allCatch-all del eje de recurso — lee todos los recursos anteriores y no concede ningún verbo
writeVerbo 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:

  1. Verifica X-Origin-Shared-Secret — 403 si está ausente/diverge.
  2. Verifica los headers mTLS (cloudfront-viewer-mtls-client-cert-fingerprint presente).
  3. Extrae Authorization: Bearer <jwt>.
  4. jwtVerify con una allowlist de exactamente un algoritmo (RS256) contra la clave pública que publica el Authorization Server. Rechaza en caso de fallo de firma. El typ no se verifica a propósito: el Authorization Server estampa at+jwt (RFC 9068), y exigir "JWT" rechazaría todo token real.
  5. Valida iss, que el aud contenga el identificador de esta API, y que exp esté presente y en el futuro.
  6. Resuelve la organización por org y luego el workspace dentro de ella (X-Workspace-Id elige uno cuando la organización tiene más de uno).
  7. Verifica el scope contra el endpoint.
  8. Verificación opcional de revocación por jti.
  9. 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​

HTTPcodeCausa
401mtls_requiredCertificado de cliente ausente o inválido
401invalid_tokenJWT malformado o firma inválida
401invalid_audienceaud no corresponde al entorno
401token_expiredexp en el pasado
401token_revokedjti listado en RevokedTokens
403insufficient_scopeEl token carece del scope requerido
403insufficient_permissionLa 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
403forbiddenmessage 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
400workspace_requiredLa 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
500as_unavailableNo 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.