Seguridad
Buenas prácticas de seguridad para la integración del iframe de firma.
URLs de los ambientes
| Ambiente | Iframe (integradores) | API (sesiones) |
|---|---|---|
| PREPROD | https://iframe-preprod.lakautac.com.ar | https://web-preprod.lakautac.com.ar |
| PROD | https://iframe.lakautac.com.ar/embed/ | https://www.lakautac.com.ar |
Protección de credenciales con Session Token
Nunca expongas tu API Key en el frontend. La API Key debe vivir exclusivamente en tu backend. El frontend solo maneja tokens de sesión efímeros.
El flujo seguro de integración utiliza un Session Token de corta duración (30 minutos) en lugar de exponer las credenciales del integrador:
Tu Backend Tu Frontend Iframe Lakaut
│ │ │
│ (API Key segura aquí) │ │
│ │ │
│<── POST /api/firma/session ──────│ │
│ │ │
│── { sessionToken } ────────────>│ │
│ │ │
│ │── <iframe src="...?sessionToken=...">
│ │ │
│ │── postMessage({ sessionToken })│
│ │ │
│ │ [iframe resuelve token │
│ │ internamente contra API] │
│ │ │
Ejemplo de backend (Node.js)
// El API Key NUNCA sale del servidor
const LAKAUT_API_KEY = process.env.LAKAUT_API_KEY;
const INTEGRATOR_ID = process.env.LAKAUT_INTEGRATOR_ID;
// PREPROD: https://web-preprod.lakautac.com.ar
// PROD: https://www.lakautac.com.ar
const LAKAUT_WEB_URL = process.env.LAKAUT_WEB_URL;
app.post('/api/firma/session', authenticateUser, async (req, res) => {
const response = await fetch(
`${LAKAUT_WEB_URL}/api/integration/session/new?id=${INTEGRATOR_ID}`,
{
method: 'POST',
headers: { 'X-Api-Key': LAKAUT_API_KEY }
}
);
const data = await response.json();
res.json({ sessionToken: data.tokenSession });
});
¿Qué pasa si alguien intercepta el sessionToken?
- El token expira en 30 minutos
- Solo sirve para una sesión de firma específica
- No permite acceder a la API Key ni a las credenciales del integrador
- El iframe valida el token contra el backend antes de usarlo
Validación de origen
Siempre validá el origen de los mensajes postMessage. Sin esta validación, tu aplicación es vulnerable a ataques.
// PREPROD: https://iframe-preprod.lakautac.com.ar
// PROD: https://iframe.lakautac.com.ar/embed/
const IFRAME_ORIGIN = 'https://iframe-preprod.lakautac.com.ar';
// ✅ CORRECTO - Validar origen
window.addEventListener('message', (event) => {
if (event.origin !== IFRAME_ORIGIN) {
console.warn('Mensaje de origen no autorizado ignorado');
return;
}
// Procesar mensaje...
});
// ❌ INCORRECTO - No validar origen
window.addEventListener('message', (event) => {
// PELIGROSO: cualquier sitio puede enviar mensajes
procesarMensaje(event.data);
});
HTTPS obligatorio
Tu aplicación debe servirse sobre HTTPS para integrar el iframe. Esto es obligatorio por:
- Políticas de cookies modernas (SameSite)
- Content Security Policy
- Requisitos de seguridad del servicio de firma
✅ https://miapp.com/firmar
❌ http://miapp.com/firmar
Content Security Policy (CSP)
Si tu aplicación usa CSP, agregá las directivas necesarias:
// PREPROD:
Content-Security-Policy:
frame-src https://iframe-preprod.lakautac.com.ar;
connect-src https://iframe-preprod.lakautac.com.ar;
// PROD:
Content-Security-Policy:
frame-src https://iframe.lakautac.com.ar/embed/;
connect-src https://iframe.lakautac.com.ar/embed/;
En HTML:
<!-- PREPROD -->
<meta http-equiv="Content-Security-Policy"
content="frame-src https://iframe-preprod.lakautac.com.ar; connect-src https://iframe-preprod.lakautac.com.ar;">
<!-- PROD -->
<meta http-equiv="Content-Security-Policy"
content="frame-src https://iframe.lakautac.com.ar/embed/; connect-src https://iframe.lakautac.com.ar/embed/;">
Idempotencia
El campo idemKey previene firmas duplicadas si el usuario reintenta o hay problemas de red:
// Generar una idemKey única por operación de firma
const idemKey = crypto.randomUUID();
// Guardar la idemKey asociada al documento en tu sistema
await guardarOperacion({
documentoId: 'DOC-123',
idemKey: idemKey,
estado: 'iniciada'
});
// Enviar al iframe
iframe.contentWindow.postMessage({
type: 'lakaut.init',
payload: {
idemKey: idemKey,
sessionToken: tuSessionToken,
// ...
}
}, IFRAME_ORIGIN);
Si la misma idemKey se envía dos veces, el sistema detecta el duplicado y evita cobrar/firmar dos veces.
Verificación de integridad
El documento firmado incluye un hash SHA256. Usalo para verificar que el documento no fue alterado:
window.addEventListener('message', (event) => {
if (event.data.type === 'lakaut.signature.completed') {
const { signedDocId, document, delivery } = event.data.payload;
// Guardar el hash junto con el documento
guardarDocumentoFirmado({
id: signedDocId,
sha256: document.sha256,
size: document.size,
fechaFirma: new Date()
});
// Opcional: verificar el hash del archivo recibido
if (delivery.mode === 'binary') {
verificarHash(delivery.fileBase64, document.sha256);
}
}
});
async function verificarHash(base64, hashEsperado) {
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (hashHex !== hashEsperado) {
console.error('¡Hash no coincide! El documento puede haber sido alterado.');
return false;
}
return true;
}
Almacenamiento seguro
Credenciales
- No expongas tu API Key en código frontend, repositorios, ni en variables
VITE_* - El
sessionTokenes efímero y seguro para el frontend - Tu API Key debe estar en variables de entorno del backend (ej:
process.env.LAKAUT_API_KEY)
Documentos firmados
- Almacená los documentos firmados en tu servidor, no solo en el navegador del usuario
- Guardá siempre el
signedDocIdysha256para auditoría - Considerá cifrar los documentos en reposo
// Enviar documento firmado a tu backend
async function guardarEnBackend(payload) {
await fetch('/api/documentos/firmados', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${tuToken}`
},
body: JSON.stringify({
signedDocId: payload.signedDocId,
sha256: payload.document.sha256,
size: payload.document.size,
pdfBase64: payload.delivery.fileBase64
})
});
}
Manejo de sesiones
Timeout de inactividad
Implementá un timeout si el usuario no completa la firma:
const TIMEOUT_MINUTOS = 15;
let timeoutId;
function resetearTimeout() {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
cerrarIframe();
mostrarMensaje('La sesión de firma expiró por inactividad.');
}, TIMEOUT_MINUTOS * 60 * 1000);
}
// Resetear en cada interacción
window.addEventListener('message', (event) => {
if (event.origin === IFRAME_ORIGIN) {
resetearTimeout();
}
});
Session Token expirado
El sessionToken tiene una validez de 30 minutos. Si expira, debés solicitar uno nuevo desde tu backend y recargar el iframe:
window.addEventListener('message', (event) => {
if (event.origin !== IFRAME_ORIGIN) return;
if (event.data.type === 'lakaut.error' &&
(event.data.code === 'SESSION_EXPIRED' || event.data.code === 'SESSION_TOKEN_EXPIRED')) {
// Obtener nuevo token y recargar
obtenerSessionToken().then(nuevoToken => {
iframe.src = `${IFRAME_ORIGIN}/embed/?sessionToken=${nuevoToken}`;
});
}
});
Recordá que en lakaut.error, los campos code y message están en el nivel raíz del mensaje, no dentro de un payload.
Protección contra clickjacking
Si tu sitio puede ser embebido en otros sitios, asegurate de que el iframe de firma no sea afectado:
# En tu servidor
X-Frame-Options: SAMEORIGIN
O con CSP:
Content-Security-Policy: frame-ancestors 'self';
Checklist de seguridad
Antes de ir a producción, verificá:
- API Key guardada exclusivamente en el backend (nunca en frontend)
- Session Token obtenido desde tu backend, no directamente desde el frontend
- Validación de origen en todos los handlers de
message - HTTPS en toda tu aplicación
- CSP configurado correctamente
-
idemKeyúnico por operación - Almacenamiento de
sha256para auditoría - Timeout de sesión implementado
- Manejo de error
SESSION_TOKEN_EXPIRED(token expirado) - Documentos firmados guardados en backend (no solo browser)
- Logs de errores para debugging
- URLs actualizadas al ambiente de producción (cuando estén disponibles)