Auvral

Verificar firmas

La verificación HMAC-SHA256 obligatoria de los webhooks de Auvral, con código que funciona en Node, Python, Go, PHP y Ruby.

Verificar la firma es obligatorio

La URL de tu webhook es alcanzable por cualquiera que la descubra. Sin verificación, un POST falsificado que dice verification.succeeded es indistinguible de uno real, y es el evento que desbloquea cuentas.

Un manejador de webhooks sin verificación es un endpoint abierto para otorgarle estado de verificado a cualquiera.

Cómo se construye la firma

Cada entrega lleva:

x-auvral-signature: v1=6b2f0c...9ae4
x-auvral-timestamp: 1787761964

La firma es un HMAC-SHA256, codificado en hexadecimal, sobre la cadena:

{x-auvral-timestamp}.{raw request body}

con la llave del secreto de firma de tu webhook (desde la página del webhook en el dashboard).

Fíjate en los dos detalles con los que la gente tropieza: un . literal une la marca de tiempo y el cuerpo, y el valor del header lleva el prefijo v1=, que no es parte del hash. El prefijo es una versión del esquema, para poder agregar esquemas futuros sin romper los manejadores existentes. Compáralo en vez de recortar los primeros tres caracteres a ciegas.

Las tres reglas

1. Lee el cuerpo crudo antes de parsearlo.

La firma cubre bytes exactos. Si tu framework parsea el JSON antes de que tú lo veas, esos bytes se perdieron: re-serializar el objeto produce distintos espacios y otro orden de llaves, y la firma no va a coincidir. Todos los ejemplos de abajo leen los bytes crudos primero y parsean solo después de verificar.

2. Rechaza marcas de tiempo viejas.

Sin una comprobación de frescura, una petición capturada una vez se puede reproducir contra ti para siempre. Una ventana de cinco minutos es un buen valor por defecto; compara en ambas direcciones para que un reloj adelantado no acepte una marca de tiempo del futuro lejano.

3. Compara en tiempo constante.

Un == ingenuo sobre cadenas filtra información de tiempos que se puede usar para reconstruir una firma válida byte por byte. Usa la comparación en tiempo constante de tu lenguaje.

Y después de verificar, deduplica por x-auvral-event-id: los reintentos lo reutilizan, y un manejador que no sea idempotente va a procesar la misma verificación hasta seis veces.

Código que funciona

webhook.js
import express from "express";
import crypto from "crypto";

const app = express();
const SECRET = process.env.AUVRAL_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;

function verify(rawBody, signatureHeader, timestampHeader) {
  if (!signatureHeader?.startsWith("v1=") || !timestampHeader) return false;

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestampHeader));
  if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${timestampHeader}.${rawBody}`)
    .digest("hex");

  const received = signatureHeader.slice("v1=".length);
  if (received.length !== expected.length) return false;

  return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

// express.raw, no express.json: el cuerpo ya parseado no se puede verificar
app.post("/webhooks/auvral", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");

  if (!verify(rawBody, req.get("x-auvral-signature"), req.get("x-auvral-timestamp"))) {
    return res.sendStatus(401);
  }

  const eventId = req.get("x-auvral-event-id");
  if (alreadyProcessed(eventId)) return res.sendStatus(200);
  markProcessed(eventId);

  const event = JSON.parse(rawBody);

  // confirma primero, trabaja después: el emisor expira a los 10s
  res.sendStatus(200);
  enqueue(event);
});

If you use express.json() globally, mount express.raw() on this route specifically, or configure a verify callback that stashes the raw buffer.

Probar tu implementación

Usa Enviar evento de prueba en el dashboard. Entrega un evento webhook.test firmado exactamente como uno real, así que un manejador que lo acepte va a aceptar eventos reales.

Después revisa los caminos de fallo, porque esos son los que importan:

  • Cambia un carácter del secreto en tu configuración. Deberías recibir 401, no 200.
  • Reproduce una entrega con un x-auvral-timestamp viejo. Debería rechazarse.
  • Entrega el mismo x-auvral-event-id dos veces. La segunda no debe hacer nada.

Cada intento, con su código de estado y el cuerpo de tu respuesta, queda registrado en la página del webhook en el dashboard.

Si la verificación falla

SíntomaCausa probable
La firma nunca coincideEl cuerpo se parseó y se re-serializó antes de que le aplicaras el hash
La firma nunca coincideLe aplicaste el hash solo al cuerpo, sin {timestamp}. adelante
La firma nunca coincideIncluiste el prefijo v1= en el valor comparado
Funciona con algunos eventos y falla con otrosUn proxy o un middleware está reescribiendo el cuerpo
El evento de prueba pasa y los reales fallanOtro webhook, otro secreto: cada webhook tiene el suyo

Relacionado

En esta página