As Assinaturas HMAC no Node.js permitem verificar se uma mensagem foi criada por quem possui um segredo compartilhado e se o conteúdo não foi alterado. O mecanismo é comum em webhooks, callbacks de pagamento, integração entre serviços, links assinados e APIs que precisam autenticar o corpo da requisição.
Uma implementação segura precisa assinar os bytes exatos, incluir timestamp e identificador, usar algoritmo moderno, comparar em tempo constante, bloquear replay e rotacionar segredos. Recriar JSON antes de verificar, aceitar timestamps antigos ou registrar a chave pode tornar a assinatura inútil.
Neste guia, você aprenderá a criar e validar HMAC com node:crypto, definir um formato canônico, assinar webhooks, proteger contra replay, rotacionar chaves, lidar com proxies e testar o fluxo.
O que é HMAC?
HMAC significa Hash-based Message Authentication Code. A RFC 2104 define o mecanismo. A documentação do módulo node:crypto descreve createHmac(), chaves e comparações criptográficas.
O que HMAC garante?
- integridade da mensagem;
- autenticidade entre partes que compartilham o segredo;
- detecção de alterações no conteúdo.
HMAC não cifra a mensagem. Quem intercepta o tráfego ainda pode ler o conteúdo se TLS não estiver sendo usado.
HMAC versus hash
Um hash simples não usa segredo:
SHA256(message)Qualquer pessoa pode recalcular. HMAC inclui uma chave:
HMAC-SHA256(secret, message)Somente quem possui o segredo pode gerar uma assinatura válida.
Criando uma assinatura
import { createHmac } from 'node:crypto';
function signHmac(secret, payload) {
return createHmac('sha256', secret)
.update(payload)
.digest('hex');
}O payload pode ser uma string ou Buffer. Para webhooks, prefira os bytes brutos recebidos.
Gerando um segredo
import { randomBytes } from 'node:crypto';
const secret = randomBytes(32).toString('base64url');Use CSPRNG e entropia suficiente. Não derive o segredo de nome da empresa, senha curta ou ID previsível.
Armazenamento do segredo
Mantenha a chave em gerenciador de segredos, não no código ou banco comum. Consulte Gestão de Segredos no Node.js.
Formato da assinatura
Um header pode carregar versão e digest:
X-Signature: v1=8a35...A versão permite mudar o formato ou algoritmo sem quebrar todas as integrações.
Timestamp
X-Signature-Timestamp: 1788170400Inclua o timestamp no material assinado. Assim, um atacante não pode alterá-lo para ampliar a janela.
Mensagem assinada
const signedPayload = `${timestamp}.${rawBody}`;
const signature = signHmac(secret, signedPayload);O emissor e o receptor precisam usar exatamente o mesmo formato.
Identificador do evento
Inclua event ID ou message ID:
const signedPayload = [
timestamp,
eventId,
rawBody
].join('.');O ID ajuda na idempotência e detecção de replay.
Corpo bruto
Assine e valide os bytes originais. Parsear JSON e serializar novamente pode mudar espaços, ordem de campos ou representação de números.
Express com raw body
app.post(
'/webhooks/provider',
express.raw({ type: 'application/json' }),
handleWebhook
);Depois da validação, converta:
const body = JSON.parse(req.body.toString('utf8'));Fastify
Configure plugin ou parser que preserve o raw body. Faça isso apenas nas rotas que precisam, para evitar duplicar buffers em toda a aplicação.
Limite do corpo
Defina tamanho máximo antes de ler. Uma assinatura válida não deve permitir um payload ilimitado.
Comparação em tempo constante
import { timingSafeEqual } from 'node:crypto';
function secureEqualHex(expectedHex, receivedHex) {
const expected = Buffer.from(expectedHex, 'hex');
const received = Buffer.from(receivedHex, 'hex');
return expected.length === received.length
&& timingSafeEqual(expected, received);
}Primeiro verifique comprimento para evitar erro da função.
Validando o header
function parseSignature(header) {
if (typeof header !== 'string') return null;
const match = /^v1=([a-f0-9]{64})$/.exec(header);
return match ? match[1] : null;
}Rejeite formatos desconhecidos, duplicados ou excessivamente longos.
Validação completa
function verifyWebhook({
secret,
timestamp,
eventId,
rawBody,
signature
}) {
const payload = `${timestamp}.${eventId}.${rawBody}`;
const expected = signHmac(secret, payload);
return secureEqualHex(expected, signature);
}Proteção contra replay
Uma assinatura válida pode ser capturada e reenviada. Use timestamp, event ID e registro de processamento.
Janela de tempo
const toleranceSeconds = 300;
const age = Math.abs(nowSeconds - Number(timestamp));
if (age > toleranceSeconds) {
throw new InvalidSignatureError();
}Use o relógio do servidor e mantenha hosts sincronizados.
Event ID único
INSERT INTO processed_webhooks (provider, event_id)
VALUES ($1, $2)
ON CONFLICT DO NOTHING;Se nenhuma linha for inserida, o evento já foi recebido.
Consulte Idempotência em APIs Node.js.
Ordem das verificações
- Limitar corpo e headers.
- Validar formato.
- Validar timestamp.
- Calcular assinatura.
- Comparar.
- Registrar event ID.
- Processar.
Resposta rápida
Depois de validar e persistir o evento, responda rapidamente e processe de forma assíncrona. Consulte Webhooks no Node.js.
Transação
O registro de idempotência e a mensagem de fila ou outbox devem confirmar juntos quando necessário.
Veja Outbox Pattern no Node.js.
Várias assinaturas
Durante rotação, o emissor pode enviar assinaturas com duas chaves:
X-Signature: v1=antiga,v1=novaDefina um parser limitado e aceite se uma assinatura válida corresponder a uma chave ativa.
Rotação do segredo
- Criar chave nova.
- Distribuir ao receptor.
- Assinar com nova e antiga.
- Confirmar validação.
- Parar assinatura antiga.
- Revogar a chave.
Key ID
X-Signature-Key-Id: webhook-2026-08O ID localiza a chave sem expor o segredo. Valide contra uma lista conhecida e limite tentativas.
Segredo por cliente
Não compartilhe uma única chave com todos os parceiros. Uma chave por integração permite revogar e auditar individualmente.
Segredo por ambiente
Teste e produção precisam de chaves diferentes. Eventos de sandbox não devem ser aceitos na rota de produção.
Algoritmo
HMAC-SHA-256 é uma escolha comum. Não permita que o cliente escolha arbitrariamente o algoritmo por header.
Algorithm confusion
A versão do protocolo deve definir o algoritmo. Rejeite versões desconhecidas em vez de fazer fallback inseguro.
Encoding
Escolha hex ou Base64 e use o mesmo padrão nos dois lados. Base64URL é útil em headers, mas não misture variantes.
Unicode
Assinar os bytes brutos evita diferenças de normalização. Quando assinar texto criado pela aplicação, defina UTF-8 explicitamente.
Canonicalização
Para estruturas criadas nos dois lados, use um formato canônico documentado. JSON comum não garante ordem de chaves.
Assinatura de query string
Ordene parâmetros, normalize encoding e inclua método, caminho e expiração:
GET
/downloads/report.pdf
expires=1788170400&user=42Evite assinar uma URL e validar outra representação equivalente sem normalização.
Links assinados
Inclua prazo curto e escopo do recurso. Não use o mesmo segredo de webhooks para links de download.
Assinatura de requisições de API
Um protocolo pode assinar método, caminho, body hash, timestamp e nonce. Documente exatamente quais headers participam.
HMAC e API keys
Uma API key identifica o cliente; HMAC prova posse do segredo e integridade da requisição. Consulte API Keys no Node.js.
TLS continua obrigatório
HMAC não protege confidencialidade. Use HTTPS e valide certificados. Em integrações de alto risco, considere mTLS no Node.js.
Proxy e body
Proxies podem descomprimir, transformar encoding ou alterar caminho. Defina o ponto em que a assinatura é calculada e preserve os bytes.
Compressão
Assine o corpo antes ou depois da compressão conforme o protocolo. O receptor precisa validar exatamente a representação recebida.
Headers duplicados
Defina comportamento para múltiplos headers de assinatura. Rejeitar ambiguidade costuma ser mais seguro.
Logs
Registre provider, key ID, event ID, idade, resultado e request ID. Nunca registre segredo ou assinatura completa sem necessidade.
Veja Logs com Pino no Node.js.
Auditoria
Criação, rotação, revogação e falhas repetidas devem ser auditadas. Consulte Logs de Auditoria no Node.js.
Métricas
Monitore assinaturas inválidas, timestamps expirados, replay, versões desconhecidas, atraso de entrega e eventos duplicados.
Rate limiting
Limite falhas por origem e integração. Um endpoint de webhook também pode sofrer DoS.
Erros para o cliente
{
"code": "INVALID_SIGNATURE",
"message": "Assinatura inválida"
}Não revele se falhou timestamp, chave, formato ou digest.
Teste de sucesso
test('aceita assinatura válida', () => {
const signature = signHmac(secret, payload);
assert.equal(verify(secret, payload, signature), true);
});Teste de alteração
Mude um byte do corpo e confirme rejeição.
Teste de timestamp
Assine corretamente com timestamp fora da janela. A requisição deve falhar.
Teste de replay
Envie o mesmo event ID duas vezes. O segundo processamento não pode repetir efeitos.
Teste de raw body
Use JSON com espaços e ordem diferente. A validação deve operar sobre os bytes recebidos, não sobre objeto serializado.
Teste de rotação
Durante a janela, assinatura antiga e nova funcionam. Depois da revogação, apenas a nova.
Teste de timing
Confirme que comprimentos inválidos são rejeitados sem chamar timingSafeEqual com buffers diferentes.
Erros comuns
- Assinar JSON reserializado: bytes mudam.
- Sem timestamp: replay permanece válido.
- Timestamp fora da assinatura: atacante altera a janela.
- Comparação com ===: diferença de tempo pode vazar informação.
- Segredo compartilhado por todos: revogação afeta todas as integrações.
- Registrar chave: observabilidade vira fonte de vazamento.
- Confiar no HMAC sem TLS: conteúdo fica visível.
Boas práticas
- Use segredo aleatório.
- Assine bytes brutos.
- Inclua timestamp e event ID.
- Use HMAC-SHA-256.
- Compare em tempo constante.
- Bloqueie replay.
- Versione o protocolo.
- Rotacione com sobreposição.
- Use chave por integração.
- Audite sem registrar segredos.
Conclusão
As Assinaturas HMAC no Node.js fornecem autenticidade e integridade para webhooks, APIs e mensagens entre sistemas. A assinatura só é confiável quando cobre a representação exata recebida.
Timestamp, event ID, comparação segura e rotação completam a proteção. Com TLS, idempotência e segredos individuais, HMAC cria um protocolo simples e robusto sem depender de assinaturas que permanecem válidas indefinidamente.



