Appearance
Autenticación
English version: Authentication.
La API de core autentica las integraciones con claves de API por tenant, enviadas como bearer token: sin JWT, sin roles en una clave: la clave es la identidad del tenant. Los usuarios del dashboard inician sesión por separado con correo y contraseña (POST /v1/auth/login, una sesión ddu_).
Authorization: Bearer dd_...Todo endpoint salvo GET /healthz exige la cabecera. En el servidor solo se guarda el sha256 del token (tabla api_keys, protegida por RLS): una base de datos filtrada nunca filtra claves utilizables.
Obtener tu primera clave
La primera clave de un tenant la emite el equipo de Dialer Digital en el aprovisionamiento (en local: priv/repo/seeds.exs, que imprime el bearer token una sola vez). A partir de ahí rotas las claves tú mismo por la API.
El dashboard inicia sesión con cuentas de usuario
El dashboard del propietario inicia sesión con cuentas de usuario del dashboard (correo y contraseña en POST /v1/auth/login, que emite una sesión ddu_; ver la Referencia de la API), y esas cuentas pueden añadir un segundo factor. El inicio de sesión único con tu propio proveedor de identidad SAML está publicado en core v0.86.0 (publicada como v0.86.0-prod-0.1) (abajo); su botón «Sign in with SSO» sale en dashboard v0.41.0 (dashboard#532).
Gestionar claves: /v1/api_keys
| Método | Ruta | Notas |
|---|---|---|
GET | /v1/api_keys | Lista las claves: id, label, revoked_at, created_at. Los hashes nunca se exponen. |
POST | /v1/api_keys | Cuerpo {"label": "..."}. Devuelve 201 con la clave nueva y el token en claro: se muestra una vez, nunca más. |
DELETE | /v1/api_keys/{id} | Revoca la clave (fija revoked_at). Las claves nunca se borran: la pista de auditoría las conserva. |
Emitir y revocar exigen un inicio de sesión account_admin del dashboard. Un inicio de sesión member recibe 403 role_forbidden, y también una clave dd_ en cuanto la cuenta tiene un account_admin activo (core#2359, core#2449); una cuenta sin account_admin activo todavía puede emitir su primera clave con una clave dd_. El ajuste de la plataforma api_key_manages_people (apagado por defecto) devuelve a la clave su alcance anterior. Listar no cambia. Ver el aviso en Usuarios del dashboard.
Emitir una clave nueva
bash
curl -s -X POST $API/v1/api_keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"label": "warehouse-etl"}'json
{
"api_key": {
"id": "7c0e6a1e-3f2b-4d5c-9e8f-1a2b3c4d5e6f",
"label": "warehouse-etl",
"revoked_at": null,
"created_at": "2026-06-11T14:30:00Z"
},
"token": "dd_kQ3v…plaintext-shown-ONCE…"
}Guarda token de inmediato: no hay forma de recuperarlo después.
Receta de rotación
POST /v1/api_keyscon una etiqueta nueva → obtienes el nuevo token en claro.- Despliega el token nuevo en tu integración.
DELETE /v1/api_keys/{old-id}→ la clave antigua deja de funcionar al instante.
La revocación también corta las conexiones WebSocket vivas: cada latido vuelve a autenticar el token, así que una clave revocada se descarta en un latido (~30 s) con el código de cierre 1008.
Segundo factor (MFA)
Los usuarios del dashboard (sesiones ddu_, no claves de API) pueden proteger su cuenta con una app autenticadora TOTP (RFC 6238) más códigos de recuperación de un solo uso. Es opcional por usuario. Las claves de API (dd_) nunca se ven afectadas. Se publicó en core v0.86.0 (publicada como v0.86.0-prod-0.1); las rutas están en la Referencia de la API.
Inicio de sesión con un factor activo. POST /v1/auth/login responde 200 con mfa_required: true y un challenge de un solo uso en lugar de una sesión. El desafío no abre ninguna otra ruta. Se canjea en POST /v1/auth/mfa/verify con {"challenge": "...", "code": "123456"} o, una sola vez, {"challenge": "...", "recovery_code": "..."} (exactamente uno de los dos); la respuesta es la misma que la de un inicio de sesión en un paso y lleva el token ddu_. El desafío caduca a los 5 minutos, tras 5 códigos erróneos o si la contraseña cambia entre medias, y un desafío desconocido, caducado o gastado da un único 401 idéntico. Un código TOTP se acepta como máximo una vez. Un usuario sin factor inicia sesión en un paso, como antes.
Gestionar tu propio factor (todo exige una sesión ddu_; una clave dd_ recibe 403):
| Método | Ruta | Notas |
|---|---|---|
GET | /v1/me/mfa | El estado de tu segundo factor. |
POST | /v1/me/mfa/enroll | Cuerpo {"password": "..."}. Devuelve el secreto y su URI otpauth:// (el contenido del QR), mostrados una vez; queda pendiente hasta confirmarse. 409 si ya hay un factor activo. |
POST | /v1/me/mfa/confirm | Cuerpo {"code": "..."}. Activa el factor y devuelve los diez códigos de recuperación, mostrados una vez (solo se guarda su hash). |
POST | /v1/me/mfa/disable | Reautenticación: la contraseña actual más un code TOTP o un recovery_code. |
POST | /v1/me/mfa/recovery-codes | La misma reautenticación. Revoca los códigos sin usar y emite un juego nuevo, mostrado una vez. |
Los códigos y contraseñas erróneos tienen límite de ritmo por usuario (429 con Retry-After), compartido por todas las rutas anteriores que comparan un código o una contraseña.
Lista de bloqueo de contraseñas. Allí donde se fija una contraseña de consola (aceptar una invitación y confirmar un restablecimiento en POST /v1/auth/password-reset/confirm, POST /v1/me/password y el alta de demo), se comprueba tras la política de longitud (de 12 a 128 caracteres) contra una lista local de contraseñas de uso común o comprometidas, plegando mayúsculas y jerga leet, de modo que Password123456 y P4ssw0rd99 se detectan. También se rechaza la que tiene menos de cuatro caracteres distintos o contiene la parte local del correo de la persona o el nombre del espacio de trabajo (fragmentos de 4 o más caracteres). La respuesta es 422 con el código password_blocklisted y el motivo en el mensaje (common, repetitive, contains_email o contains_tenant); no cambia nada y el enlace de invitación o restablecimiento sigue canjeable. Las contraseñas existentes nunca se comprueban. El ajuste de plataforma password_blocklist_enforced sale encendido.
Freno progresivo por cuenta. Además del límite de ritmo por usuario, POST /v1/auth/login cuenta las filas bad_password de la pista de auditoría de inicios de sesión de la persona desde su último acceso correcto, dentro de 24 horas. Con 20 (login_progressive_threshold_failures) o más, el inicio de sesión responde 429 con Retry-After sin comparar nada, ni siquiera la contraseña correcta, hasta 15 minutos después del último fallo; la pausa se duplica por cada 20 fallos más y tiene un tope de 6 horas. Un acceso correcto reinicia la cuenta. Es una pausa y no un bloqueo, para que quien solo conoce el correo de un administrador no pueda dejarlo fuera para siempre. Los intentos retenidos no se registran, así que no alargan la espera.
Exigido por el tenant. Un ajuste por tenant, mfa_required_enforced, sale apagado. Cuando se enciende para un tenant (un ajuste del lado de la plataforma, que no se puede escribir con una clave ni una sesión del tenant), un usuario sin factor recibe en el inicio de sesión mfa_required: true con purpose: enroll y debe inscribirse (POST /v1/auth/mfa/enroll, luego POST /v1/auth/mfa/enroll/confirm) antes de que se emita una sesión.
Recuperar a una persona que perdió el autenticador y los códigos de recuperación
Esta vía se publicó en core v0.88.0 (core#1580) y está en la Referencia de la API generada. Un account_admin con una sesión ddu_ puede quitar el factor de otra persona activa del mismo tenant:
| Método | Ruta | Notas |
|---|---|---|
POST | /v1/users/{id}/mfa/remove | Cuerpo {"reason": "..."}. El motivo es texto libre obligatorio, se recorta y se conserva en la pista de auditoría. |
La operación invalida el factor activo y sus códigos de recuperación sin usar, revoca todas las sesiones ddu_ vivas de la persona y añade un evento factor_removed_by_admin con el administrador, el destino y el motivo. Estos efectos comparten una transacción: si no se puede escribir el evento o la revocación de sesiones, el factor sigue activo. Los eventos de auditoría existentes no se borran.
Una clave de API del tenant (dd_) y un member reciben 403; una persona fuera del tenant es indistinguible de una que no existe (404). Un administrador no puede usar esta ruta sobre sí mismo (409): la vía de autoservicio sigue siendo POST /v1/me/mfa/disable, con reautenticación. Si el tenant exige MFA, el siguiente inicio de sesión con contraseña de la persona recuperada arranca el flujo de inscripción forzada antes de poder emitir una sesión nueva. Si no, puede iniciar sesión sin factor e inscribirse de nuevo más tarde.
Inicio de sesión único (SAML)
Cada tenant puede iniciar sesión a su gente a través de su propio proveedor de identidad (IdP) SAML 2.0, iniciado por el SP. Se publicó en core v0.86.0 (publicada como v0.86.0-prod-0.1); las rutas están en la Referencia de la API. El botón «Sign in with SSO» del dashboard sale en dashboard v0.41.0 (dashboard#532); el flujo de abajo es el contrato de la API. Las claves de API (dd_) nunca se ven afectadas.
Está apagado hasta que se configura la celda. La celda necesita SSO_SP_BASE_URL, la URL base pública por la que un navegador llega a core. Sin ella, las rutas de metadatos, inicio y ACS responden 404, y GET /v1/sso/idp-config devuelve service_provider: null.
Configura tu IdP (exige un usuario account_admin con sesión ddu_). Una clave dd_ no puede leer ni escribir esta configuración, de modo que una clave de integración filtrada no pueda apuntar el inicio de sesión a un IdP del atacante: recibe 403 role_forbidden.
| Método | Ruta | Notas |
|---|---|---|
GET | /v1/auth/sso/{tenant_id}/metadata | Pública. El XML de metadatos del proveedor de servicio que se pega en tu IdP: entity ID, URL del ACS (binding HTTP-POST) y formato de NameID emailAddress, ambos derivados del id de tu tenant. |
GET | /v1/sso/idp-config | La configuración de tu IdP (o null) más el bloque service_provider con las mismas URL. |
PUT | /v1/sso/idp-config | Cuerpo idp_entity_id, sso_url (debe ser https://), certificates (uno o dos certificados PEM: las únicas claves de las que se acepta una firma), require_signed_assertion, require_signed_response, email_attribute (opcional) y enabled. |
PATCH | /v1/sso/idp-config | Cuerpo {"enabled": true | false}. |
DELETE | /v1/sso/idp-config | Elimina la configuración. |
GET | /v1/sso/idp-config/events | El historial de auditoría de los cambios de la configuración. |
El inicio de sesión. Tres pasos, para que un formulario falsificado enviado al navegador de una víctima no pueda iniciar sesión como el atacante:
POST /v1/auth/sso/startcon{"tenant_id": "..."}devuelve la URL del IdP a la que enviar al navegador (redirect_url) y unbrowser_secretque pertenece solo a ese navegador. Un tenant sin IdP habilitado recibe un único404, indistinguible de un tenant desconocido.- El IdP envía su respuesta al ACS del tenant,
POST /v1/auth/sso/{tenant_id}/acs. Core la valida y responde con un303de vuelta al/sso/callbackdel dashboard, con uncodede un solo uso en el fragmento de la URL (oerror=sso_failed, una respuesta genérica para todo rechazo). El ACS nunca emite una sesión. POST /v1/auth/sso/exchangecon{"code": "...", "browser_secret": "..."}emite la sesiónddu_habitual. El código vive 120 segundos, se gasta en su primera presentación coincida o no el secreto, y un código desconocido, caducado o gastado da un único401idéntico.
Qué se comprueba. La firma debe verificarse contra un certificado que hayas fijado (la clave dentro del documento se ignora; RSA con SHA-256 o más fuerte, SHA-1 se rechaza), y debe cubrir el elemento del que se leen los datos, de modo que una aserción envuelta o duplicada se rechaza. Audience, recipient y destination deben ser las URL propias de este tenant, la respuesta debe contestar a la petición que inició el inicio de sesión (InResponseTo), la ventana de validez debe cumplirse y un ID de aserción se acepta una sola vez. Una respuesta emitida para el SP de un tenant no vale nada en el de otro.
Sin aprovisionamiento just-in-time. La persona ya debe ser usuaria del tenant: el correo de la aserción se compara con ella, y un correo desconocido se rechaza sin crear nada. Los usuarios deshabilitados siguen deshabilitados. Un inicio de sesión SSO no pide el segundo factor propio de la app: el IdP es la autoridad.
Exigir SSO. Un ajuste por tenant, sso_required_enforced, sale apagado. Cuando un operador de la plataforma lo enciende para un tenant que además tiene un IdP habilitado, POST /v1/auth/login responde 403 con el código sso_required después de que la autenticación por contraseña tenga éxito, y SSO es la única entrada. Con el ajuste encendido pero sin IdP habilitado no hace nada, así que una configuración rota del IdP no puede dejar fuera a un tenant por sí sola. Las claves de API, los tokens de widget y el restablecimiento de contraseña no se tocan.
Pista de auditoría de inicios de sesión
Cada inicio de sesión de una persona del dashboard (sesiones ddu_, nunca claves de API) deja una fila en una pista de solo-anexar: por contraseña, por segundo factor y por SSO, exitoso o rechazado. Un trigger de base de datos rechaza todo UPDATE o DELETE, de modo que una fila no se puede editar ni eliminar por la API ni por el propio rol de base de datos de la aplicación. La ruta llega a la Referencia de la API cuando su OpenAPI vendorizado se refresca a la versión de core que lleva core#1520; el comportamiento de abajo es el de esa versión.
Leerla (exige un usuario account_admin con sesión ddu_). Un member y una clave dd_ reciben ambos 403 role_forbidden: la pista nombra personas y direcciones de red, y una clave de integración no es una persona.
| Método | Ruta | Notas |
|---|---|---|
GET | /v1/auth/sign-in-events | Lo más reciente primero. Filtros user_id, outcome (success o failure), since (inclusivo) y until (exclusivo), ambos RFC 3339. limit vale 50 por defecto y tiene tope 200; next_cursor en la respuesta es el cursor de la página siguiente, null en la última. Un filtro que no se puede interpretar es un 400 que lo nombra, nunca una página vacía. |
Qué dice una fila. method (password, mfa o sso), outcome, un reason de una lista cerrada (bad_password, unknown_user, disabled, suspended, sso_required, mfa_failed, sso_rejected, mfa_pending), ip, user_agent (truncado a 256 caracteres), el session_id que emitió el inicio de sesión y occurred_at. Una contraseña aceptada de una persona que debe un segundo factor (o su inscripción) se registra como method: password, outcome: success, reason: mfa_pending (un paso concedido, aún sin sesión); la sesión en sí aparece con la fila mfa que canjea el desafío.
Una dirección que no es de nadie nunca se guarda en claro. Cuando la dirección no coincide con ninguna persona, la fila tiene user_id: null y email_attempted guarda solo un resumen SHA-256 de la dirección normalizada tomado con un prefijo de propósito fijo (no el SHA-256 desnudo de la dirección); esas filas no se pueden leer por esta ruta (no pertenecen a ningún tenant).
Cierra por seguridad. Un inicio de sesión exitoso se escribe en la misma transacción que emite su sesión: si la pista no se puede escribir, el inicio de sesión se rechaza. Un rechazo se escribe con el mejor esfuerzo después del rechazo, así que la respuesta a quien llama es la misma en ambos casos.
Aún sin retención. La pista solo crece; una política de purga no forma parte de esta funcionalidad.
Modos de fallo
| Estado | Código | Cuándo |
|---|---|---|
401 | unauthorized | Clave ausente, mal formada, desconocida o revocada. |
403 | forbidden | Clave válida, pero el tenant está suspendido. |
Autenticación del WebSocket: un ticket efímero
Los navegadores no pueden fijar cabeceras en el handshake de un WebSocket, así que algo tiene que viajar en la query string. /v1/ws acepta exactamente una cosa ahí, y no es tu clave de API: un ticket de un solo uso que caduca en segundos. Emitirlo es una llamada extra, y esa llamada es todo el punto: la clave de larga vida se queda en una cabecera Authorization, donde debe estar.
Paso 1: emite el ticket, presentando la clave de API de la forma normal:
POST /v1/ws/ticket
Authorization: Bearer dd_...json
{
"ticket": "ddtt_8Zr3kQ0m2sVx1yPd7La9WfHnB6TcEjRu4KgMoZvQiXs",
"expires_at": "2026-01-01T00:00:30Z",
"expires_in": 30
}expires_in es la vida del ticket en segundos, y es corta a propósito: léela de la respuesta en lugar de suponer un valor.
Si ya está lleno un tope de conexiones del piso en vivo, esta llamada no emite nada y responde 429 too_many_connections con un error.scope de widget_token o tenant: ver Rate limits & caps (en inglés).
Paso 2: abre el socket con el ticket:
GET /v1/ws?ticket=ddtt_...El ticket hereda el principal exacto del bearer que lo emitió, y su carril va codificado en el prefijo: ddtt_ para una clave de tenant, ddta_ para una de agente. Un ticket emitido por una credencial de agente nunca puede abrir un socket de tenant.
403 (tenant suspendido) y 426 (la petición no es un upgrade) se comportan como en el resto. 401 cubre tres casos con una única respuesta idéntica: ticket desconocido, ya canjeado y caducado, para que un cliente no aprenda nada de la diferencia. El tema del socket sale del ticket canjeado: un socket solo recibe los eventos de su propio tenant, y no hay nada a lo que suscribirse por nombre. Detalles en Eventos WebSocket.
Por qué un ticket y no la clave
Todo lo que va en una URL acaba en los logs de acceso, el historial del navegador y las cabeceras Referer. En vez de pedirte que lo mitigues, la API hace que lo que viaja ahí no valga nada: el ticket se quema en el primer canje y caduca en segundos aunque no se use. Emite uno por conexión y nada reutilizable llegará nunca a un log.
Modelo de tenancy
- Una clave ↔ un tenant.
GET /v1/medevuelve el tenant (no un usuario). GET /v1/tenantsdevuelve una lista de un elemento (tu propio tenant), conservada por paridad con el dashboard.- La API nunca acepta un id de tenant por ruta, query ni cuerpo. El id de un recurso de otro tenant se lee como
404 not_found: la existencia nunca se filtra (lo aplica RLS de Postgres, no una comparación de ids en el handler).
Lista de seguridad para integradores
- Guarda los tokens solo en el servidor; nunca los incrustes en código que se entrega a consumidores.
- Emite una clave por integración (etiquétala) para que la revocación sea quirúrgica.
- Rota con la receta de arriba según tu calendario normal de credenciales.
- Registra el
X-Request-IDde las llamadas fallidas: hace que los casos de soporte sean instantáneos.