Webhooks explicados: cargas firmadas y estrategias de reintento
Has creado una API que envía webhooks a los endpoints de tus usuarios. Pero entonces te enfrentas a los problemas clásicos: ¿cómo aseguras que la carga no ha sido manipulada? ¿Qué sucede cuando el endpoint está caído? ¿Cómo evitas el procesamiento duplicado? En este artículo, abordaremos estos desafíos de frente con cargas firmadas y estrategias de reintento robustas.
Por qué los webhooks necesitan firmas
Los webhooks son callbacks HTTP que notifican a otros sistemas sobre eventos. Debido a que se envían por la internet pública, son vulnerables a la interceptación y suplantación. Sin una firma, un actor malicioso podría enviar webhooks falsos a tus usuarios, desencadenando acciones no deseadas. Firmar las cargas con un secreto compartido garantiza autenticidad e integridad.
Cómo funcionan las cargas firmadas
El enfoque más común es HMAC (Hash-based Message Authentication Code) con SHA-256. El emisor y el receptor comparten una clave secreta. El emisor calcula una firma sobre la carga (y a veces una marca de tiempo) y la incluye en un encabezado. El receptor recalcula la firma y la compara con la recibida.
Paso a paso: Firmar una carga
- Crear la carga: Serializa los datos de tu evento a JSON. Asegura un formato consistente (por ejemplo, sin espacios en blanco adicionales) porque la firma se calcula sobre los bytes exactos.
- Agregar una marca de tiempo: Incluye una marca de tiempo Unix en la carga o en el encabezado para prevenir ataques de repetición. El receptor puede rechazar solicitudes con más de unos minutos de antigüedad.
- Calcular el HMAC: Usa tu clave secreta para generar el HMAC-SHA256 de la cadena de carga (o marca de tiempo + carga).
- Adjuntar la firma: Envíala en un encabezado como
X-Signature.
Aquí tienes un ejemplo en Python:
import hmac
import hashlib
import json
import time
secret = b'your_shared_secret'
payload = {"event": "user.created", "data": {"id": 123}}
body = json.dumps(payload, separators=(',', ':'))
timestamp = str(int(time.time()))
signed_content = timestamp + '.' + body
signature = hmac.new(secret, signed_content.encode(), hashlib.sha256).hexdigest()
# Enviar encabezados: X-Signature: signature, X-Timestamp: timestamp
Verificar la firma
En el extremo receptor, debes:
- Extraer la marca de tiempo y la firma de los encabezados.
- Rechazar si la marca de tiempo es demasiado antigua (por ejemplo, >5 minutos) para prevenir ataques de repetición.
- Recalcular la firma usando el mismo método.
- Comparar usando una función de tiempo constante para evitar ataques de temporización.
Ejemplo de verificación en Node.js:
const crypto = require('crypto');
function verifySignature(req, secret) {
const signature = req.headers['x-signature'];
const timestamp = req.headers['x-timestamp'];
if (!signature || !timestamp) return false;
if (Date.now() / 1000 - timestamp > 300) return false; // 5 min
const body = JSON.stringify(req.body);
const signed = timestamp + '.' + body;
const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Diseñar estrategias de reintento
Incluso con firmas, los webhooks pueden fallar debido a problemas de red, errores del servidor o tiempos de espera. Una estrategia de reintento robusta garantiza la entrega sin abrumar al receptor.
Consideraciones clave
- Idempotencia: Los receptores deben manejar eventos duplicados con elegancia. Usa una clave de idempotencia (por ejemplo, ID de evento) para deduplicar.
- Retroceso exponencial: Reintenta después de retrasos crecientes (por ejemplo, 1s, 5s, 30s, 5m, 30m) para dar tiempo al receptor a recuperarse.
- Reintentos máximos: Establece un límite (por ejemplo, 5 intentos) y luego marca el webhook como fallido, quizás alertando al usuario.
- Tiempos de espera: Establece un tiempo de espera razonable (por ejemplo, 10 segundos) para cada intento.
Implementar reintentos
La mayoría de los emisores de webhooks usan un sistema de colas (por ejemplo, Redis, RabbitMQ o una base de datos) para programar reintentos. Aquí tienes un flujo simplificado:
- Al activarse el evento, encola un trabajo de webhook con la carga y la URL de destino.
- El worker toma el trabajo e intenta la entrega.
- Si el estado de la respuesta es 2xx, marca como exitoso.
- Si no, programa un reintento con retroceso exponencial, hasta el máximo de intentos.
- Después del máximo de intentos, marca como fallido y notifica.
Ejemplo de programación de reintentos:
| Intento | Retraso |
|---|---|
| 1 | Inmediato |
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
Mejores prácticas para la seguridad de webhooks
- Usa HTTPS: Siempre entrega webhooks sobre TLS para prevenir escuchas.
- Rota secretos: Rota periódicamente los secretos compartidos y admite múltiples secretos activos durante la rotación.
- Valida URLs: Asegúrate de que la URL del webhook sea HTTPS y no apunte a IPs internas (para prevenir SSRF).
- Registra y monitorea: Mantén registros de intentos de entrega y fallos para depuración y auditoría.
Preguntas frecuentes
¿Qué es una carga de webhook firmada?
Una carga firmada incluye una firma criptográfica (generalmente HMAC) que permite al receptor verificar la autenticidad del emisor y que la carga no ha sido manipulada.
¿Cuántas veces debo reintentar un webhook fallido?
No hay una respuesta única, pero un enfoque común es de 3 a 5 reintentos con retroceso exponencial. Considera la criticidad del evento y la capacidad del receptor.
¿Puedo usar JWT para firmas de webhooks?
Sí, se puede usar JWT, pero es más complejo. HMAC es más simple y suficiente para la mayoría de los casos de uso. JWT es útil si necesitas incluir reclamos o expiración dentro de la firma misma.
¿Listo para probar tus cargas de webhook? Usa nuestro Formateador JSON para validar y embellecer tus cargas JSON antes de enviarlas.