Verificando assinaturas de webhook

Como verificar o cabeçalho X-Tokeflow-Signature em webhooks recebidos usando HMAC-SHA256, com um exemplo completo em Node/Express.

Todo webhook que a Tokeflow envia carrega um cabeçalho X-Tokeflow-Signature. Verificá-lo comprova que a requisição veio da Tokeflow e não foi adulterada em trânsito. Sempre verifique antes de agir sobre um webhook — um endpoint não verificado pode ser falsificado por qualquer pessoa que descubra sua URL.

O cabeçalho de assinatura

X-Tokeflow-Signature: t=1737030609,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
ParteSignificado
tO timestamp Unix (em segundos) em que a assinatura foi gerada.
v1A assinatura HMAC-SHA256, codificada em hexadecimal.

A assinatura é calculada sobre a string {t}.{raw_request_body} — o timestamp, um . literal, e então os bytes brutos exatos do corpo da requisição — usando o signing secret do seu endpoint como chave.

Você obtém o signing secret (que se parece com whsec_…) ao criar o endpoint no Dashboard da Tokeflow. Trate-o como uma senha: armazene-o como um segredo, nunca faça commit dele, e use-o literalmente — incluindo o prefixo whsec_ — como a chave HMAC.

Passos de verificação

  1. Leia o corpo bruto da requisição como bytes/string, antes de qualquer parsing de JSON.
  2. Leia o cabeçalho X-Tokeflow-Signature e divida-o em t e v1.
  3. Calcule HMAC_SHA256(secret, + "${t}.${rawBody}" + ) e codifique-o em hexadecimal.
  4. Compare o seu valor com v1 usando uma comparação de tempo constante.
  5. Rejeite a requisição se o timestamp t estiver fora da sua tolerância (por exemplo, mais antigo que 5 minutos) para prevenir ataques de replay.

Capture o corpo bruto. A assinatura é calculada sobre os bytes exatos que a Tokeflow enviou. Se o seu framework faz parsing do JSON e o reserializa, a ordem das chaves e os espaços em branco podem mudar e a assinatura deixará de corresponder. Configure sua rota para expor o corpo bruto (veja o exemplo abaixo).

Exemplo em 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);
}

O esquema de assinatura é compatível com o padrão HMAC-SHA256 t=…,v1=… amplamente utilizado, então bibliotecas auxiliares existentes que verificam esse formato funcionarão — basta passar o seu segredo whsec_… como a chave e verificar contra o corpo bruto.

Após verificar

  • Responda com um status 2xx rapidamente; faça o trabalho pesado de forma assíncrona.
  • Uma resposta não-2xx ou um timeout faz com que a Tokeflow repita a entrega com backoff.
  • Deduplique pelo id do evento e reconcilie o estado via a API. Veja Tipos de evento de webhook.

Nesta página