API de FlavosoWebhooks y eventosComprobar las firmas

Comprobar las firmas

Cada entrega lleva Flavoso-Signature, un HMAC-SHA256 sobre la marca de tiempo y el cuerpo sin procesar. Código de comprobación para Node.js, Python, PHP, Go y C#.

El método

Cada entrega lleva la cabecera Flavoso-Signature: t=1759579200,v1=5257a8…. En ella, t es la hora en segundos desde 1970 y v1 un HMAC-SHA256, en hexadecimal y en minúsculas:

  • Clave: tu secreto de firma, la cadena completa, incluido whsec_, en UTF-8.
  • Mensaje: t, un punto y el cuerpo sin procesar. Es decir, 1759579200.{"id":"evt_…",…}.

Comprueba la firma antes de fiarte del contenido. Así sabes que la entrega viene de Flavoso y que no se ha modificado por el camino. Es el mismo método que usa Stripe: si ya lo has implementado allí, ya lo conoces.

Paso a paso

  1. 1

    Tomar el cuerpo sin procesar

    Exactamente los bytes que han llegado, antes de que nada los interprete como JSON.

  2. 2

    Descomponer la cabecera

    Sepárala por las comas y después por el primer =. t aparece una vez; v1, una o dos.

  3. 3

    Comprobar la antigüedad

    Si t se desvía más de 300 segundos de tu reloj, rechaza la entrega. Si no, una entrega interceptada podría volver a enviarse más tarde.

  4. 4

    Calcular el valor esperado

    HMAC-SHA256 con el secreto sobre t + . + cuerpo, en hexadecimal.

  5. 5

    Comparar

    Compara el resultado con cada v1 en tiempo constante. Si coincide uno, la entrega es auténtica.

Código de comprobación

// flavoso-signature.js
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the body exactly as received. Never JSON.stringify(req.body) again.
export function verifyFlavosoSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = header.split(",").map((part) => part.trim().split("="));
  const t = Number(parts.find(([key]) => key === "t")?.[1]);
  const signatures = parts.filter(([key]) => key === "v1").map(([, value]) => value);
  if (!Number.isInteger(t) || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest("hex");
  return signatures.some(
    (s) => s.length === expected.length && timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
  );
}

Obtener el cuerpo sin procesar

La mayoría de los frameworks interpretan el JSON automáticamente. Si después vuelves a convertir el cuerpo en texto, obtienes otros caracteres (espacios, orden, caracteres especiales) y una firma incorrecta. Así obtienes los bytes:

EntornoCuerpo sin procesar
Expressexpress.raw({ type: "application/json" }) en esta ruta
Next.js (App Router)await request.text()
Flaskrequest.get_data()
FastAPIawait request.body()
Djangorequest.body
PHPfile_get_contents('php://input')
Laravel$request->getContent()
Goio.ReadAll(r.Body)
ASP.NET CoreStreamReader sobre Request.Body, antes de cualquier model binding
// app/api/flavoso/webhook/route.js (App Router)
import { verifyFlavosoSignature } from "@/lib/flavoso-signature";

export async function POST(request) {
  const raw = await request.text(); // exactly as received, before any JSON parsing
  const header = request.headers.get("flavoso-signature") ?? "";
  if (!verifyFlavosoSignature(raw, header, process.env.FLAVOSO_WEBHOOK_SECRET)) {
    return new Response("invalid signature", { status: 400 });
  }
  await enqueue(JSON.parse(raw)); // answer fast, work in the background
  return new Response(null, { status: 200 });
}

Valor de prueba

Con él puedes probar tu comprobación sin Flavoso: con este secreto, este cuerpo y t como «ahora», debe aceptar la firma, y con un carácter cambiado en el cuerpo, rechazarla.

# Your verification must accept this example with t = 1759579200 as "now".
# openssl prints the same v1 (after "SHA2-256(stdin)="):
printf '%s' '1759579200.{"id":"evt_test","type":"ping"}' \
  | openssl dgst -sha256 -hmac 'whsec_test_secret'

# secret:  whsec_test_secret
# body:    {"id":"evt_test","type":"ping"}
# header:  Flavoso-Signature: t=1759579200,v1=d5a5a8b08d9a25070a9e22c9fa06ad59b4082eb039b25cee3d83319e00d8201a

Cambiar el secreto

Tras «Crear un nuevo secreto de firma», Flavoso firma durante 24 horas con el secreto nuevo y el anterior a la vez, el nuevo primero: la cabecera lleva entonces dos v1. Basta con que coincida uno. Configura el secreto nuevo durante ese tiempo; después solo es válido el nuevo.

Si un secreto se ha filtrado, renuévalo de inmediato. Durante 24 horas, y no más, seguirá siendo posible una entrega firmada con el anterior.

Errores frecuentes

  • El cuerpo se leyó como JSON y se volvió a convertir en texto.
  • El secreto equivocado: el webhook de tu app y los webhooks propios de un negocio tienen secretos distintos.
  • El reloj del servidor va mal. Ajústalo con NTP.
  • Se comparó el hexadecimal en mayúsculas, o no se quitaron los espacios alrededor de las partes de la cabecera.
  • Se tomó Flavoso-Event-Id como prueba. El identificador no es un secreto; solo la firma demuestra algo.

¿Algo no ha quedado claro? Escribe a support@flavoso.com e indica el identificador de X-Request-Id. Así podremos encontrar tu solicitud en el registro.