Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Webhook Signatures: valide HMAC no Node.js

Atualizado em: 24 de outubro de 2026

Terminal Linux usado em desenvolvimento e administração de aplicações Node.js

Webhook Signatures no Node.js verificam se uma requisição foi criada pelo provedor esperado e se o corpo não foi alterado durante o transporte. O padrão mais comum usa HMAC com um segredo compartilhado, timestamp e comparação em tempo constante.

HTTPS protege o canal, mas não prova por si só que o emissor conhece o segredo do webhook. Um endpoint público pode receber chamadas de qualquer origem. A assinatura adiciona autenticação da mensagem e ajuda a bloquear payloads forjados.

Formato típico

Webhook-Signature: t=1792780000,v1=3c68...

O provedor calcula o HMAC sobre uma mensagem canônica, por exemplo:

${timestamp}.${rawBody}

A aplicação precisa reproduzir exatamente a mesma sequência de bytes.

Preserve o corpo bruto

O erro mais comum é verificar depois que o framework transformou JSON em objeto. Espaços, ordem de propriedades e codificação podem mudar.

app.post('/webhooks/provider',
  express.raw({ type: 'application/json', limit: '1mb' }),
  webhookHandler
);

Depois da verificação, faça o parsing.

const rawBody = req.body;
const payload = JSON.parse(rawBody.toString('utf8'));

Calculando HMAC

import { createHmac, timingSafeEqual } from 'node:crypto';

function signWebhook({ secret, timestamp, rawBody }) {
  return createHmac('sha256', secret)
    .update(String(timestamp))
    .update('.')
    .update(rawBody)
    .digest('hex');
}

Comparação em tempo constante

function safeEqualHex(expectedHex, receivedHex) {
  if (!/^[a-f0-9]+$/i.test(receivedHex)) return false;

  const expected = Buffer.from(expectedHex, 'hex');
  const received = Buffer.from(receivedHex, 'hex');

  if (expected.length !== received.length) return false;
  return timingSafeEqual(expected, received);
}

timingSafeEqual exige buffers do mesmo tamanho. Valide formato antes da comparação.

Parsing do header

function parseSignatureHeader(value) {
  const parts = String(value ?? '').split(',');
  const parsed = new Map();

  for (const part of parts) {
    const [key, rawValue] = part.split('=', 2);
    if (key && rawValue) parsed.set(key.trim(), rawValue.trim());
  }

  return {
    timestamp: Number(parsed.get('t')),
    signatures: parts
      .filter(part => part.trim().startsWith('v1='))
      .map(part => part.trim().slice(3))
  };
}

Aceitar múltiplas assinaturas facilita rotação de segredos e evolução de versão.

Proteção contra replay

Uma assinatura válida pode ser capturada e reenviada. Exija timestamp recente.

function validateTimestamp(timestamp, toleranceSeconds = 300) {
  if (!Number.isSafeInteger(timestamp)) return false;

  const now = Math.floor(Date.now() / 1000);
  return Math.abs(now - timestamp) <= toleranceSeconds;
}

Sincronize relógios e monitore desvios. Para operações críticas, persista o ID do evento e rejeite duplicações.

Verificação completa

function verifyWebhook({ header, rawBody, secrets }) {
  const parsed = parseSignatureHeader(header);

  if (!validateTimestamp(parsed.timestamp)) {
    throw new Error('Timestamp inválido');
  }

  const valid = secrets.some(secret => {
    const expected = signWebhook({
      secret,
      timestamp: parsed.timestamp,
      rawBody
    });

    return parsed.signatures.some(received =>
      safeEqualHex(expected, received)
    );
  });

  if (!valid) throw new Error('Assinatura inválida');
}

Rotação de segredo

Durante a rotação, aceite segredo atual e anterior por uma janela curta. O emissor pode assinar com ambos ou a aplicação pode testar os dois.

const webhookSecrets = [
  process.env.WEBHOOK_SECRET_CURRENT,
  process.env.WEBHOOK_SECRET_PREVIOUS
].filter(Boolean);

Remova o segredo antigo depois de confirmar que todos os emissores usam o novo. Consulte Secret Management no Node.js.

Segredos por endpoint

Use segredo diferente por provedor, ambiente e, quando necessário, por tenant. Um segredo comprometido não deve permitir forjar todos os webhooks.

Assinaturas assimétricas

Alguns provedores usam RSA ou Ed25519. Nesse modelo, o emissor guarda a chave privada e consumidores validam com chave pública.

import { verify } from 'node:crypto';

const valid = verify(
  null,
  signedMessage,
  publicKey,
  signatureBuffer
);

Siga exatamente o algoritmo, encoding e formato de chave documentados pelo provedor. Não escolha o algoritmo com base em header não confiável sem allowlist.

Resposta rápida

Depois de validar assinatura e persistir o evento, responda rapidamente. Processamento longo aumenta retries do provedor.

await inbox.insert({
  eventId: payload.id,
  type: payload.type,
  body: payload
});

res.status(202).end();

Um worker processa a Inbox de forma assíncrona.

Idempotência

Assinatura válida não impede entregas duplicadas. Crie constraint única por provedor e ID do evento.

CREATE UNIQUE INDEX webhook_inbox_unique
ON webhook_inbox (provider, event_id);

Veja Consumer Idempotente no Node.js.

Ordem dos passos

  1. limite tamanho do corpo;
  2. capture bytes brutos;
  3. extraia timestamp e assinatura;
  4. valide tolerância temporal;
  5. calcule e compare assinatura;
  6. faça parsing do JSON;
  7. valide schema;
  8. persista com idempotência;
  9. responda;
  10. processe assíncronamente.

Erros HTTP

Use respostas consistentes sem revelar detalhes:

  • 400: header ou corpo malformado;
  • 401 ou 400: assinatura inválida, conforme contrato;
  • 413: corpo grande;
  • 202 ou 200: evento aceito;
  • 500: falha transitória de persistência.

Não informe qual segredo ou parte da assinatura falhou.

Proxies e body parsers

Garanta que nenhum middleware consuma ou modifique o corpo antes do verificador. Compressão de request, conversão de charset e normalização também podem alterar os bytes assinados.

Logs

Registre provedor, ID do evento, tipo, resultado e latência. Nunca registre segredo, assinatura completa ou payload sensível. Para diagnóstico, um prefixo curto ou hash pode ser suficiente.

Rate limiting

Endpoints de webhook podem sofrer abuso. Limite tamanho, conexões e frequência, mas considere IPs variáveis e retries legítimos. A assinatura continua sendo a defesa principal de autenticidade.

Testes

  • assinatura válida;
  • corpo alterado;
  • timestamp expirado;
  • timestamp futuro;
  • header ausente;
  • hex inválido;
  • segredo atual e anterior;
  • payload duplicado;
  • JSON inválido após assinatura válida;
  • corpo acima do limite.

Use fixtures oficiais do provedor quando disponíveis.

Erros comuns

  • assinar JSON reserializado;
  • comparar strings com ===;
  • não verificar timestamp;
  • não persistir IDs processados;
  • usar o mesmo segredo em produção e teste;
  • registrar assinatura completa;
  • processar antes de responder;
  • não planejar rotação;
  • aceitar algoritmo arbitrário.

Checklist

  • raw body preservado;
  • limite de bytes;
  • mensagem canônica documentada;
  • timestamp validado;
  • comparação constante;
  • segredos rotacionáveis;
  • schema validado;
  • Inbox idempotente;
  • resposta rápida;
  • logs sem segredos.

Conclusão

Webhook Signatures no Node.js protegem endpoints públicos contra payloads forjados e alterações. A verificação correta depende dos bytes originais, do algoritmo exato e de uma janela temporal contra replay.

Combine assinatura com idempotência, validação de schema e processamento assíncrono. Com rotação de segredos e testes de fixtures reais, webhooks se tornam uma integração confiável em vez de uma rota pública difícil de auditar.

Consulte a documentação oficial do módulo crypto do Node.js e o RFC do HMAC.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita