Webhooks explicados: cargas firmadas y reintentos
Por qué los webhooks necesitan cargas firmadas y reintentos
Acabas de integrar un servicio de terceros que envía webhooks a tu endpoint. Todo funciona en las pruebas, pero en producción notas eventos faltantes, entregas duplicadas y, peor aún, alguien falsificando solicitudes a tu URL de webhook. Sin las medidas adecuadas de seguridad y confiabilidad, los webhooks pueden convertirse en una fuente de errores y vulnerabilidades.
Los webhooks son callbacks HTTP que notifican a tu aplicación cuando ocurre un evento. Son simples en concepto pero complicados en la práctica porque cruzan límites de confianza y problemas de confiabilidad de red. En esta guía, cubriremos dos aspectos críticos: cargas firmadas para verificar la autenticidad y reintentos para asegurar la entrega. Recorreremos implementaciones prácticas que puedes aplicar de inmediato.
Entendiendo las cargas firmadas
Una carga firmada es una forma en que el emisor demuestra que el webhook provino de él y no fue alterado. El emisor calcula una firma criptográfica sobre el cuerpo de la solicitud (y a veces los encabezados) usando un secreto compartido, luego incluye esa firma en un encabezado. Tu endpoint recalcula la firma y la compara.
El algoritmo más común es HMAC-SHA256. Es rápido, seguro y ampliamente soportado. Así es como funciona:
- El emisor y el receptor comparten una clave secreta (configurada en los ajustes del webhook).
- Para cada webhook, el emisor crea una firma hasheando la carga con el secreto usando HMAC-SHA256.
- La firma se envía en un encabezado como
X-Signature. - El receptor calcula el mismo HMAC y lo compara usando una comparación de tiempo constante para prevenir ataques de temporización.
Implementando verificación HMAC en Node.js
Veamos un ejemplo concreto. Supongamos que el emisor incluye la firma en X-Signature como una cadena hexadecimal. Así es como la verificas en una ruta de Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Usar el cuerpo sin procesar para la verificación de firma
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const expected = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Firma inválida');
}
// Procesar el evento
const event = JSON.parse(req.body);
console.log('Evento recibido:', event.type);
res.status(200).send('OK');
});
app.listen(3000);
Importante: Siempre usa el cuerpo de la solicitud sin procesar para el cálculo de la firma, no el JSON parseado. Parsear y volver a stringificar puede cambiar los espacios en blanco y el orden, rompiendo la firma.
Protección contra marcas de tiempo y replay
Las firmas por sí solas no previenen ataques de replay. Un atacante podría capturar una solicitud firmada válida y reenviarla más tarde. Para mitigar esto, incluye una marca de tiempo en la carga firmada o en un encabezado. Por ejemplo, agrega un encabezado X-Timestamp con la hora Unix. El receptor verifica que la marca de tiempo esté dentro de una ventana de tolerancia (por ejemplo, 5 minutos) y rechaza solicitudes más antiguas.
Además, incluye la marca de tiempo en el cálculo de la firma para prevenir alteraciones. El emisor calcula HMAC sobre timestamp + '.' + body. El receptor hace lo mismo y compara.
Implementando reintentos para una entrega confiable
Los webhooks viajan por la internet pública, así que los fallos son inevitables: tiempos de espera de red, errores del servidor (5xx) o tu endpoint temporalmente caído. Un sistema de webhooks robusto debe reintentar entregas fallidas.
Estrategias de reintento
Las estrategias de reintento comunes incluyen:
- Retardo fijo: Reintentar después de un intervalo fijo (por ejemplo, 1 minuto). Simple pero puede saturar si ocurren muchos fallos.
- Backoff exponencial: Esperar más después de cada intento (por ejemplo, 1s, 2s, 4s, 8s...). Este es el enfoque más común y recomendado.
- Jitter: Agregar aleatoriedad al retardo para evitar problemas de estampida cuando muchos webhooks fallan simultáneamente.
Una política típica podría ser: reintentar hasta 5 veces con backoff exponencial y jitter, comenzando a los 10 segundos. Después de eso, marcar la entrega como fallida y posiblemente alertar.
Idempotencia: manejando duplicados
Los reintentos pueden causar entregas duplicadas. Tu endpoint debe ser idempotente: procesar el mismo evento dos veces no debería causar efectos secundarios como cobrar dos veces a un cliente. Incluye un ID de evento único en la carga (por ejemplo, event.id) y almacena los IDs procesados durante un período razonable. Antes de procesar, verifica si el ID ya ha sido visto.
Alternativamente, diseña tus operaciones para que sean naturalmente idempotentes (por ejemplo, usando upserts en lugar de inserts).
Respondiendo rápidamente
Tu endpoint de webhook debe acusar recibo rápidamente (en unos pocos segundos) y procesar el evento de forma asíncrona. Si el procesamiento tarda demasiado, el emisor puede agotar el tiempo de espera y reintentar, causando duplicados. Usa una cola: responde con 200 OK inmediatamente después de verificar la firma, luego encola el evento para procesamiento en segundo plano.
Comparación de medidas de seguridad para webhooks
| Medida | Propósito | Complejidad de implementación |
|---|---|---|
| Firma HMAC | Verificar autenticidad e integridad | Baja |
| Validación de marca de tiempo | Prevenir ataques de replay | Baja |
| Claves de idempotencia | Evitar procesamiento duplicado | Media |
| Lista blanca de IPs | Restringir a IPs de emisores conocidos | Baja (pero frágil) |
| HTTPS | Cifrar datos en tránsito | Baja |
Si bien la lista blanca de IPs puede agregar una capa, no es confiable porque las IPs del emisor pueden cambiar. Confía principalmente en las firmas.
Probando tu endpoint de webhook
Usa herramientas como ngrok para exponer tu servidor local y disparar webhooks de prueba desde el panel del proveedor. Verifica que las firmas se validen correctamente y que los reintentos funcionen simulando fallos (por ejemplo, devolver 500 en los primeros intentos).
Al depurar, es posible que necesites inspeccionar cargas JSON. El Formateador JSON puede ayudarte a formatear y validar cargas de webhook rápidamente.
Preguntas frecuentes
¿Qué es una carga de webhook firmada?
Una carga firmada incluye una firma criptográfica (generalmente HMAC) que permite al receptor verificar que la solicitud provino del emisor esperado y no fue alterada. Se calcula usando un secreto compartido.
¿Cuántas veces debo reintentar un webhook fallido?
No hay un número universal, pero 3–5 reintentos con backoff exponencial son comunes. Después de eso, registra el fallo y considera alertar. El objetivo es manejar problemas transitorios sin reintentos infinitos.
¿Puedo usar JWT para firmas de webhooks?
Sí, algunos proveedores usan JWT (JSON Web Tokens) para firmar webhooks. El token contiene claims y está firmado con un secreto o clave privada. La verificación es similar: valida la firma y verifica claims como la expiración.
¿Listo para implementar webhooks seguros? Comienza generando un secreto fuerte, agregando verificación HMAC y configurando reintentos con idempotencia. Para una inspección rápida de cargas durante el desarrollo, prueba nuestro Formateador JSON para formatear y validar el JSON de webhooks.