Saltar al contenido principal

Seguridad

Buenas prácticas de seguridad para la integración del iframe de firma.

URLs de los ambientes

AmbienteIframe (integradores)API (sesiones)
PREPRODhttps://iframe-preprod.lakautac.com.arhttps://web-preprod.lakautac.com.ar
PRODhttps://iframe.lakautac.com.ar/embed/https://www.lakautac.com.ar

Protección de credenciales con Session Token

Crítico

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

Crítico

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 sessionToken es 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 signedDocId y sha256 para 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}`;
});
}
});
Estructura del error

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 sha256 para 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)