Verificación de firmas de webhooks

Cómo verificar el encabezado X-Tokeflow-Signature en los webhooks entrantes usando HMAC-SHA256, con un ejemplo completo en Node/Express.

Cada webhook que envía Tokeflow incluye un encabezado X-Tokeflow-Signature. Verificarlo demuestra que la solicitud provino de Tokeflow y que no fue manipulada en tránsito. Verifica siempre antes de actuar sobre un webhook: un endpoint sin verificar puede ser suplantado por cualquiera que conozca su URL.

El encabezado de firma

X-Tokeflow-Signature: t=1737030609,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
PartSignificado
tLa marca de tiempo Unix (en segundos) en la que se generó la firma.
v1La firma HMAC-SHA256, codificada en hexadecimal.

La firma se calcula sobre la cadena {t}.{raw_request_body}: la marca de tiempo, un . literal, y luego los bytes crudos exactos del cuerpo de la solicitud, con clave en el secreto de firma de tu endpoint.

Obtienes el secreto de firma (tiene el aspecto whsec_…) cuando creas el endpoint en el Dashboard de Tokeflow. Trátalo como una contraseña: guárdalo como un secreto, nunca lo incluyas en el control de versiones y úsalo tal cual —incluyendo el prefijo whsec_— como clave HMAC.

Pasos de verificación

  1. Lee el cuerpo crudo de la solicitud como bytes/cadena, antes de cualquier análisis de JSON.
  2. Lee el encabezado X-Tokeflow-Signature y divídelo en t y v1.
  3. Calcula HMAC_SHA256(secret, + "${t}.${rawBody}" + ) y codifícalo en hexadecimal.
  4. Compara tu valor con v1 usando una comparación de tiempo constante.
  5. Rechaza la solicitud si la marca de tiempo t está fuera de tu tolerancia (por ejemplo, más antigua que 5 minutos) para evitar ataques de repetición.

Captura el cuerpo crudo. La firma se calcula sobre los bytes exactos que envió Tokeflow. Si tu framework analiza el JSON y lo vuelve a serializar, el orden de las claves y los espacios en blanco pueden cambiar y la firma dejará de coincidir. Configura tu ruta para exponer el cuerpo crudo (consulta el ejemplo a continuación).

Ejemplo en Node / Express

import express from 'express';
import crypto from 'crypto';

const app = express();
const SIGNING_SECRET = process.env.TOKEFLOW_WEBHOOK_SECRET; // "whsec_..."
const TOLERANCE_SECONDS = 5 * 60;

// Use the raw body parser on the webhook route so the bytes are untouched.
app.post(
  '/webhooks/tokeflow',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.get('X-Tokeflow-Signature') || '';
    const rawBody = req.body; // a Buffer, thanks to express.raw

    if (!verify(rawBody, header, SIGNING_SECRET)) {
      return res.status(400).send('Invalid signature');
    }

    const event = JSON.parse(rawBody.toString('utf8'));

    // Acknowledge immediately, then process asynchronously.
    res.status(200).send('ok');
    handleEvent(event); // dedupe on event.id, reconcile via the API
  },
);

function verify(rawBody, signatureHeader, secret) {
  // Parse "t=...,v1=..."
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((kv) => kv.split('=')),
  );
  const timestamp = Number(parts.t);
  const provided = parts.v1;
  if (!timestamp || !provided) return false;

  // Reject stale timestamps (replay protection).
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) return false;

  // Recompute the HMAC over `${t}.${rawBody}`.
  const signedPayload = `${timestamp}.${rawBody.toString('utf8')}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Constant-time compare.
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(provided, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

El esquema de firma es compatible con el ampliamente usado patrón HMAC-SHA256 t=…,v1=…, por lo que las bibliotecas auxiliares existentes que verifican ese formato funcionarán: solo pasa tu secreto whsec_… como clave y verifica contra el cuerpo crudo.

Después de verificar

  • Responde con un estado 2xx rápidamente; realiza el trabajo pesado de forma asíncrona.
  • Una respuesta que no sea 2xx o un tiempo de espera agotado hace que Tokeflow reintente la entrega con retroceso.
  • Elimina duplicados según el id del evento y reconcilia el estado mediante la API. Consulta Tipos de eventos de webhook.

En esta página