Seguridad
Cómo AllSign protege cada sesión de firma embebida — desde la generación del token hasta la entrega del PDF firmado.
Modelo de seguridad
Inspirado en Plaid Link moderno (post-2020) y Veriff (session tokens). Cada sesión tiene múltiples capas:
┌─ Tu backend (secret key: allsign_live_sk_xxx) ───────────┐
│ POST /v2/signing-sessions │
│ → Crea documento + firmantes + config │
│ → Devuelve client_secret (token efímero, 15 min) │
│ → La secret key NUNCA sale de tu backend │
└───────────────────────────────────────────────────────────┘
│
│ client_secret (single-use, scoped a 1 session)
▼
┌─ Tu frontend (solo el client_secret) ────────────────────┐
│ JS SDK monta iFrame con el client_secret │
│ → client_secret es la UNICA credencial en el browser │
│ → postMessage con origin validation │
│ → CSP frame-ancestors restringe dominios │
└───────────────────────────────────────────────────────────┘
│
│ iFrame carga allsign.io/embed/sign/{session_id}
▼
┌─ AllSign ────────────────────────────────────────────────┐
│ Token hash (SHA-256) — nunca se almacena en texto plano │
│ Sesión atada a: documento + firmante + timestamp │
│ OTP opcional pre-firma (email o WhatsApp) │
│ Camera permissions para IDV embebido │
└───────────────────────────────────────────────────────────┘
Secret key vs Client secret
AllSign usa una sola credencial en el frontend: el client_secret. No hay publishable key. Este es el mismo modelo que Plaid adoptó en 2020 cuando deprecó su publicKey:
| Credencial | Prefijo | Dónde se usa | Alcance | Vida |
|---|---|---|---|---|
| Secret key | allsign_live_sk_ | Solo tu backend (server-side) | Crear documentos, signing sessions, leer datos, verificar firmas | Permanente (hasta que la rotes) |
| Client secret | as_sess_..._secret_... | Tu frontend (browser) | Montar UNA sola session de firma | 15 minutos, single-use |
// ✅ Backend — usa la secret key
const response = await fetch('https://api.allsign.io/v2/signing-sessions', {
method: 'POST',
headers: { 'Authorization': 'Bearer allsign_live_sk_xxx' },
body: JSON.stringify({ document_id, signer_email })
});
const session = await response.json();
// session.client_secret → pásalo a tu frontend
// ✅ Frontend — usa SOLO el client_secret
const signing = AllSign.init({ clientSecret: session.client_secret });
signing.modal();
// ❌ NUNCA pongas la secret key en el frontend
// El SDK detecta este error y lo rechaza con un mensaje claro
¿Por qué no hay una "publishable key" como Stripe? Porque no aporta nada en nuestro modelo. Stripe necesita una pk para permitir que el browser tokenice datos sensibles (tarjetas, cuentas bancarias) antes de que exista una PaymentIntent — la pk autentica ese momento "pre-session". En AllSign el browser nunca crea nada antes de una session: siempre es tu backend el que genera la session con la secret key, y el frontend recibe un client_secret ya scoped a esa session específica. El client_secret ya hace todo el trabajo que una pk haría: identifica al tenant, restringe las acciones permitidas, y es seguro para el browser porque expira rápido y es single-use. Agregar una pk sería ceremonia sin beneficio — y Plaid, que tenía exactamente ese modelo híbrido, lo eliminó en 2020 por la misma razón.
Signing Sessions (client_secret)
El client_secret generado por POST /v2/signing-sessions tiene estas propiedades:
| Propiedad | Valor |
|---|---|
| Tiempo de vida | 15 minutos desde la generación |
| Uso | Single-use — una vez que se abre, el token se consume |
| Almacenamiento | El token se hashea con SHA-256 antes de guardarse en DB |
| Contenido | Session ID + firmante + documento + timestamp + nonce encriptado |
| Revocación | Se invalida automáticamente si el documento cambia de estado |
| Alcance | Solo puede montar el widget — no puede leer ni modificar datos |
Buenas prácticas
- Crea la session justo antes de que el usuario necesite firmar — no la pre-generes
- Nunca expongas el
client_secreten logs — trátalo como credencial temporal - Si expira, crea una nueva session — el endpoint es idempotente para el mismo firmante
// ✅ Correcto: crea la session al momento de abrir
async function openSigning() {
const { client_secret } = await fetch('/api/create-session').then(r => r.json());
allsign.modal({ clientSecret: client_secret });
}
// ❌ Incorrecto: pre-crear la session al cargar la página
const session = await fetch('/api/create-session').then(r => r.json());
// ... 20 minutos después ...
allsign.modal({ clientSecret: session.client_secret }); // SESSION_EXPIRED
Content Security Policy (CSP)
Para que el iFrame de AllSign funcione en tu sitio, tu CSP debe permitir frames desde el dominio de AllSign.
Configuración requerida
Agrega estas directivas a tu Content Security Policy:
frame-src https://allsign.io;
Si usas el CDN del SDK:
script-src 'self' https://js.allsign.io;
frame-src https://allsign.io;
Ejemplo en meta tag
<meta http-equiv="Content-Security-Policy"
content="frame-src https://allsign.io; script-src 'self' https://js.allsign.io;">
Ejemplo en header HTTP (Next.js)
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: "frame-src https://allsign.io; script-src 'self' https://js.allsign.io;"
}
];
Frame-ancestors (del lado de AllSign)
AllSign configura frame-ancestors en las respuestas del iFrame para restringir qué dominios pueden embeber la firma, a partir del allowed_origins de la sesión. Como allowed_origins es obligatorio en producción (ver abajo), tu iframe live siempre queda restringido a tus dominios autorizados.
Restricción de origen (allowed_origins)
Cuando creas una signing session, puedes pasar allowed_origins para restringir desde qué dominios se puede usar el client_secret. Esto protege contra un escenario de phishing:
Sin allowed_origins:
1. Tu backend crea session → client_secret viaja al browser
2. Atacante intercepta el client_secret (XSS, proxy, devtools)
3. Atacante monta el iframe de AllSign en sitio-falso.com
4. Firmante ve iframe REAL de AllSign dentro de una página falsa
5. Firma creyendo que está en tu sitio → phishing exitoso
Con allowed_origins: ["https://tu-app.com"]:
1. Tu backend crea session con allowed_origins
2. Atacante intercepta el client_secret
3. Atacante monta el iframe en sitio-falso.com
4. AllSign detecta que parent_origin es sitio-falso.com
5. → 403 ORIGIN_NOT_ALLOWED — el iframe no carga
Uso
Pasa el campo allowed_origins al crear la session:
// Backend — al crear la signing session
const response = await fetch('https://api.allsign.io/v2/signing-sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer allsign_live_sk_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
document_id: 'doc_xxx',
signer_email: 'firmante@empresa.com',
allowed_origins: ['https://tu-app.com', 'https://staging.tu-app.com'],
}),
});
| Entorno de la key | allowed_origins | Resultado |
|---|---|---|
live (producción) | No enviado o [] | 400 ALLOWED_ORIGINS_REQUIRED — la sesión no se crea |
live (producción) | ["https://tu-app.com"] | Solo ese dominio puede usar el client_secret |
live (producción) | varios dominios | Cualquiera de la lista es aceptado |
test / dev (sandbox) | No enviado o [] | Permitido (DX de sandbox) — cualquier dominio puede embeber |
| Cualquier entorno | http://localhost:* | Siempre permitido — no necesitas agregarlo para desarrollo local |
allowed_origins es OBLIGATORIO en producción (keys live). Si creas una sesión live
sin él, recibes 400 ALLOWED_ORIGINS_REQUIRED — así te enteras al integrar (en tus logs
servidor-a-servidor), no cuando un firmante ya está esperando. Las keys de sandbox
(allsign_test_sk_* / allsign_dev_sk_*) pueden omitirlo, y http://localhost siempre se
permite, para que no tengas fricción en desarrollo.
Error: ALLOWED_ORIGINS_REQUIRED
Si tu backend crea una sesión live sin allowed_origins, la API responde:
{
"error": {
"code": "ALLOWED_ORIGINS_REQUIRED",
"type": "bad_request",
"message": "Casi listo: agrega 'allowed_origins' con el dominio donde montas el iframe...",
"field": "allowed_origins",
"example": { "allowed_origins": ["https://app.tuempresa.com"] },
"documentation_url": "https://developers.allsign.io/embedded/security"
}
}
La solución es agregar el arreglo allowed_origins con el dominio de tu app (no el de AllSign):
curl -X POST "https://api.allsign.io/v2/signing-sessions" \
-H "Authorization: Bearer allsign_live_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"document_id": "DOC_UUID",
"signer_email": "firmante@empresa.com",
"allowed_origins": ["https://app.tuempresa.com"]
}'
¿A quién protege?
A tu integración y a tus firmantes. Si un atacante obtiene un client_secret (por ejemplo, via XSS en tu sitio), no puede reutilizarlo en otro dominio para hacer phishing. El firmante solo puede interactuar con AllSign desde los dominios que tu backend autorizó.
Validación de origen (postMessage)
El SDK usa postMessage para comunicación entre tu página y el iFrame de AllSign. Internamente, el SDK valida que los mensajes provengan del origen correcto:
// Esto lo hace el SDK automáticamente — no necesitas implementarlo
window.addEventListener('message', (event) => {
if (event.origin !== 'https://allsign.io') return;
// Procesar evento...
});
El SDK maneja toda la validación de origen internamente. Tú solo configuras los callbacks onEvent/onSuccess/onExit al llamar AllSign.init(). Nunca necesitas escuchar postMessage directamente.
Verificación de identidad pre-firma
Para documentos sensibles, puedes requerir verificación de identidad antes de que el firmante acceda al documento. Esto se configura al crear el documento:
{
"name": "Contrato de arrendamiento",
"documents": [{ "base64Content": "...", "fileType": "pdf" }],
"participants": [{
"name": "Juan Pérez",
"email": "juan@empresa.com"
}],
"signatureValidation": {
"autografa": true,
"document_scan": true,
"selfie_capture": true,
"biometric_signature": true
}
}
Cuando el widget se monta, el firmante primero pasa por el pipeline de verificación (escaneo de INE, selfie, match biométrico) y solo después puede firmar.
Flujo con verificación
Widget se monta
→ Escaneo de INE (OCR + validación)
→ Selfie del firmante
→ Face match biométrico (INE vs selfie)
→ ✅ Identidad verificada
→ Mostrar documento para firma
→ Firma (autógrafa o simple)
→ ✅ Documento firmado con evidencia de identidad
Todos estos pasos ocurren dentro del widget embebido — el firmante nunca sale de tu aplicación.
OTP pre-firma (no disponible aún — fase 2)
El OTP pre-firma del SDK aún no está implementado. El backend acepta el campo otp_mode en el schema de POST /v2/signing-sessions, pero hoy solo el valor "none" está soportado. Cualquier otro valor ("required", "integrator_verified") devuelve 501 NOT IMPLEMENTED (fase 2 del roadmap, ALLSI-188). Por ahora, la autenticación del firmante es responsabilidad de tu app (ALLSI-195).
Cuando esté disponible, la configuración OTP se pasará al crear la Signing Session (no al crear el documento). El único valor funcional hoy es:
{
"document_id": "DOC_UUID",
"signer_email": "juan@empresa.com",
"otp_mode": "none"
}
Checklist de seguridad
Antes de ir a producción con firma embebida, verifica:
- Tu secret key (
allsign_live_sk_*) está en variables de entorno del backend — nunca en el frontend, nunca commiteada al repo - Las Signing Sessions se crean server-side con tu secret key — el frontend solo recibe el
client_secret - Envías
allowed_originscon los dominios de producción al crear la session — obligatorio para keys live (ver sección) - El
client_secretse crea justo antes de abrir el widget, no al cargar la página (TTL de 15 min) - El
client_secretno se escribe a logs ni se guarda en storage cliente (localStorage, cookies, etc.) - Tu CSP permite
frame-src https://allsign.io - Si embedes IDV (escaneo de INE), tu Permissions-Policy / CSP no bloquea la cámara — el SDK ya aplica
allow="camera; microphone; clipboard-write"al iframe automáticamente; solo evita bloquearlo (este item solo aplica si construyes tu propio iframe sin el SDK) - Tienes webhooks configurados como fuente de verdad (no solo callbacks del SDK)
- Si usas HMAC en webhooks, verificas la firma en cada request
- Las acciones críticas (pagos, activaciones) dependen del webhook o return URL, no del callback del SDK
- Para documentos sensibles, tienes verificación de identidad habilitada
- En producción,
test_modeestá desactivado
Siguiente paso
- Quickstart — Implementa tu primera firma embebida
- Webhooks — Configura y verifica webhooks con HMAC
- Errores —
ORIGIN_NOT_ALLOWED,CSP_BLOCKEDy demás códigos, con su recuperación

