En esta página
Conserve los bytes antes de interpretar JSON
La firma utiliza exactamente timestamp + '.' + raw_body, con HMAC SHA-256 y el secreto de webhook de su entorno. Lea el timestamp de X-Face-Auth-Timestamp y la firma de X-Face-Auth-Signature, con formato sha256=<hex>.
No reconstruya el mensaje mediante JSON.stringify ni otra serialización del objeto interpretado. Espacios, saltos de línea u orden de propiedades modifican los bytes. Configure su framework para disponer del cuerpo bruto antes del parser JSON.
Verificación en el backend
El ejemplo recibe una tolerancia temporal definida por su política; no representa una promesa de plazo de entrega. Sincronice el reloj del servidor y evalúe las reentregas junto con su política contra replay.
import { createHmac, timingSafeEqual } from "node:crypto";
// Read X-Face-Auth-Timestamp / X-Face-Auth-Signature. rawBody is the exact received Buffer.
export function verifyWebhook(rawBody, timestamp, signature, secret, maxAgeSeconds, nowSeconds = Date.now() / 1000) {
if (!Buffer.isBuffer(rawBody) || !secret || !Number.isFinite(maxAgeSeconds) || maxAgeSeconds <= 0 || !Number.isFinite(nowSeconds)) return false;
if (typeof timestamp !== "string" || !/^[0-9]{1,16}$/.test(timestamp) || typeof signature !== "string" || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
const sentAt = Number(timestamp);
if (!Number.isSafeInteger(sentAt) || Math.abs(nowSeconds - sentAt) > maxAgeSeconds) return false;
const expected = createHmac("sha256", secret).update(Buffer.from(timestamp + ".", "utf8")).update(rawBody).digest();
const received = Buffer.from(signature.slice(7), "hex");
return expected.length === received.length && timingSafeEqual(expected, received);
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
Después de la validación
Solo después de validar HMAC interprete JSON, valide el envelope y procese el evento. Compruebe la coherencia del identificador entre header y cuerpo. Elimine duplicados mediante event_id en almacenamiento duradero; la verificación criptográfica por sí sola no impide repetir efectos de negocio.
Los ejemplos aíslan la validación y no implementan un receptor completo. Añada límites de cuerpo, tratamiento de excepciones, persistencia, colas, logs protegidos y conciliación. Nunca registre el secreto, cuerpos con datos personales ni enlaces de recorrido en herramientas públicas.