Webhooks: 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 perdidos, entregas duplicadas y ocasionales preocupaciones de seguridad. Los webhooks son simples en concepto pero complicados en la práctica, especialmente cuando se trata de verificar la autenticidad y manejar fallos.
Esta guía explica cómo proteger los webhooks con cargas firmadas e implementar mecanismos de reintento robustos, para que puedas construir integraciones confiables.
¿Qué son los webhooks?
Los webhooks son callbacks HTTP definidos por el usuario. Cuando ocurre un evento en un sistema de origen (por ejemplo, se completa un pago), se envía una solicitud HTTP POST a una URL que especifiques, conteniendo datos sobre el evento. Esto permite comunicación en tiempo real entre servicios sin polling.
Pero los webhooks introducen desafíos: ¿cómo sabes que la solicitud provino genuinamente del remitente esperado? ¿Qué pasa si tu endpoint está temporalmente caído? Ahí es donde entran las cargas firmadas y los reintentos.
Proteger webhooks con cargas firmadas
Una carga firmada usa una firma criptográfica para demostrar que la solicitud se originó en el remitente y no fue manipulada. El enfoque común es HMAC (Hash-based Message Authentication Code) con un secreto compartido.
Cómo funcionan las firmas HMAC
El remitente calcula un HMAC sobre el cuerpo de la solicitud (y a veces encabezados como la marca de tiempo) usando una clave secreta. La firma se envía en un encabezado (por ejemplo, X-Signature). Tu servidor recalcula el HMAC y lo compara con la firma recibida. Si coinciden, la solicitud es auténtica.
Aquí tienes un ejemplo mínimo en Python usando Flask:
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b'your_shared_secret'
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
if not signature:
abort(400, 'Missing signature')
body = request.get_data()
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401, 'Invalid signature')
# Process the event
return '', 200
Puntos clave:
- Usa siempre una función de comparación de tiempo constante (como
hmac.compare_digest) para prevenir ataques de temporización. - Incluye una marca de tiempo en los datos firmados para prevenir ataques de repetición; rechaza solicitudes con más de unos minutos de antigüedad.
- Mantén el secreto seguro; nunca lo expongas en el lado del cliente.
Medidas de seguridad adicionales
- HTTPS: Usa siempre HTTPS para proteger la carga en tránsito.
- Lista de IPs permitidas: Si el proveedor publica rangos de IP, restringe las solicitudes a esas IPs.
- Idempotencia: Usa IDs de evento para deduplicar, ya que los reintentos pueden causar duplicados.
Implementar reintentos confiables
Los consumidores de webhooks pueden fallar: tu servidor podría estar caído o el procesamiento podría agotar el tiempo de espera. Los remitentes deberían reintentar las entregas fallidas con retroceso exponencial. Como consumidor, debes manejar los reintentos con elegancia.
Del lado del remitente (si estás construyendo un sistema de webhooks)
- Devuelve un 2xx rápidamente: Confirma la recepción antes de procesar para evitar tiempos de espera. Usa una cola para trabajo asíncrono.
- Reintenta en caso de fallo: Si no se recibe un 2xx dentro de un tiempo de espera (por ejemplo, 5 segundos), reintenta con demoras crecientes (por ejemplo, 1s, 5s, 30s, 5m, 30m).
- Limita los reintentos: Tras un número máximo de intentos (por ejemplo, 5), marca la entrega como fallida y alerta.
- Incluye un ID de evento único: Para que los consumidores puedan deduplicar.
Del lado del consumidor (tu endpoint)
- Responde rápido: Devuelve 2xx inmediatamente y procesa de forma asíncrona si es necesario.
- Sé idempotente: Usa el ID de evento para evitar procesar el mismo evento dos veces.
- Maneja duplicados: Almacena los IDs de eventos procesados durante un período razonable.
- Registra fallos: Si el procesamiento falla, devuelve 5xx para activar un reintento, pero asegúrate de no quedar atrapado en un bucle.
Comparación: cargas firmadas vs. autenticación básica
| Enfoque | Seguridad | Complejidad | Protección contra repetición |
|---|---|---|---|
| Firma HMAC | Alta (integridad + autenticidad) | Moderada | Sí (con marca de tiempo) |
| Autenticación básica | Baja (credenciales en el encabezado) | Baja | No |
| Lista de IPs permitidas | Media (a nivel de red) | Baja | No |
Probar webhooks localmente
Durante el desarrollo, necesitas una URL pública para recibir webhooks. Herramientas como ngrok o localtunnel pueden exponer tu servidor local. Prueba siempre la verificación de firmas y el comportamiento de reintentos con fallos simulados.
Al inspeccionar cargas de webhooks, puede que necesites formatear el JSON para facilitar la lectura. El Formateador JSON puede ayudarte a imprimir con formato y validar el cuerpo JSON.
Preguntas frecuentes
¿Qué es una carga de webhook firmada?
Una carga firmada incluye una firma criptográfica (a menudo HMAC) que permite al receptor verificar la autenticidad e integridad de la solicitud.
¿Cuántas veces debería reintentarse un webhook?
No hay un número fijo; las prácticas comunes van de 3 a 10 reintentos con retroceso exponencial. La clave es equilibrar la confiabilidad con el uso de recursos y alertar sobre fallos persistentes.
¿Puedo usar webhooks sin firmas?
No es recomendable. Sin firmas, no puedes verificar al remitente, lo que hace que tu endpoint sea vulnerable a suplantación. Usa siempre firmas en producción.
¿Listo para depurar tus cargas de webhooks? Prueba nuestro Formateador JSON para validar y formatear datos JSON rápidamente.