Webhooks Explicados: Payloads Assinados e Retentativas
Por que Webhooks São Traiçoeiros
Webhooks são uma ideia simples: em vez de consultar uma API repetidamente para detectar mudanças, o provedor envia eventos para o seu endpoint via HTTP POST. Mas, na prática, webhooks introduzem uma série de desafios que complicam a vida de muitos desenvolvedores: verificar se a requisição realmente veio do provedor, lidar com entregas duplicadas e enfrentar falhas de rede. Se você ignorar isso, corre o risco de brechas de segurança, corrupção de dados e usuários insatisfeitos.
Este artigo aborda os dois aspectos mais críticos de webhooks prontos para produção: payloads assinados e retentativas. Vamos cobrir como verificar assinaturas, como implementar retentativas com backoff exponencial e como tornar seu processamento idempotente.
Payloads Assinados: Provando Autenticidade
Qualquer um pode enviar um HTTP POST para o seu endpoint. Para garantir que a requisição veio do seu provedor, o provedor assina o payload com uma chave secreta e envia a assinatura em um cabeçalho. Sua tarefa é recalcular a assinatura e compará-la.
A maioria dos provedores usa HMAC-SHA256. A assinatura normalmente é calculada sobre o corpo bruto da requisição (às vezes com um timestamp) e codificada em hexadecimal. Por exemplo, o Stripe usa um cabeçalho como Stripe-Signature contendo um timestamp e a assinatura.
Como o HMAC Funciona
HMAC (Hash-based Message Authentication Code) combina uma chave secreta com uma função de hash. Dados a mesma chave e mensagem, a saída é determinística. Sem a chave, um atacante não consegue forjar uma assinatura válida.
Veja um exemplo mínimo em Node.js:
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Em Python, é igualmente direto:
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Boas Práticas para Verificação de Assinatura
- Use comparação de tempo constante para prevenir ataques de temporização. Tanto
crypto.timingSafeEqualquantohmac.compare_digestsão seguros. - Verifique o corpo bruto, não um objeto JSON parseado. Parsear e re-serializar pode alterar espaços em branco e quebrar a assinatura.
- Inclua um timestamp nos dados assinados para prevenir ataques de replay. Rejeite requisições mais antigas que alguns minutos.
- Armazene segredos com segurança—use variáveis de ambiente ou um gerenciador de segredos, nunca os codifique diretamente.
Lidando com Timestamps e Ataques de Replay
Muitos provedores incluem um timestamp no cabeçalho da assinatura. Por exemplo, o cabeçalho pode ser t=1614556800,v1=abc123.... Você deve:
- Extrair o timestamp e a assinatura.
- Verificar se o timestamp está dentro de uma janela aceitável (ex.: 5 minutos).
- Recalcular a assinatura usando o timestamp e o payload.
- Comparar as assinaturas de forma segura.
Isso impede que um atacante reenvie uma requisição válida antiga.
Retentativas: Lidando com Falhas com Elegância
Webhooks são entregues pela internet pública, então falhas são inevitáveis. Seu endpoint pode estar fora do ar, lento ou retornar um erro 500. Um provedor robusto tentará reenviar entregas falhas, mas você também precisa lidar com retentativas do seu lado se for o provedor.
Backoff Exponencial com Jitter
Ao tentar novamente, evite bombardear o endpoint com retentativas imediatas. Use backoff exponencial: espere mais tempo após cada falha. Adicione jitter (aleatoriedade) para evitar problemas de manada trovejante quando muitos webhooks falham simultaneamente.
Um cronograma típico de retentativas pode ser:
| Tentativa | Atraso (aprox.) |
|---|---|
| 1 | Imediato |
| 2 | 30 segundos |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
Após um número máximo de tentativas, marque a entrega como falha e alerte.
Idempotência: A Chave para Retentativas Seguras
Como retentativas podem causar entregas duplicadas, seu manipulador de webhook deve ser idempotente. Isso significa que processar o mesmo evento várias vezes deve ter o mesmo efeito que processá-lo uma vez.
Como alcançar idempotência:
- Use um ID de evento único fornecido no payload (ex.: campo
id). Armazene os IDs processados em um banco de dados com uma restrição de unicidade. - Projete operações para serem idempotentes—por exemplo, use upserts em vez de inserts, ou verifique se um recurso já existe antes de criá-lo.
- Retorne 2xx rapidamente e processe de forma assíncrona se o trabalho for pesado. Isso reduz a chance de timeouts que disparam retentativas.
Implementando Retentativas no Lado do Provedor
Se você está construindo um sistema de webhooks, precisa de um mecanismo de entrega confiável. Aqui está uma abordagem simplificada:
- Quando um evento ocorre, enfileire um job de entrega com o payload e a URL de destino.
- Um worker pega o job e envia o HTTP POST com um timeout (ex.: 10 segundos).
- Se o status da resposta não for 2xx, agende uma retentativa com backoff exponencial.
- Acompanhe as tentativas e desista após um máximo (ex.: 5 tentativas).
- Registre falhas e forneça uma forma de os usuários reenviarem manualmente.
Use uma fila persistente (ex.: Redis, RabbitMQ ou uma tabela de banco de dados) para que as retentativas sobrevivam a reinicializações.
Juntando Tudo: Um Checklist para Receptor de Webhook
Ao construir um receptor de webhook, siga este checklist:
- Verifique a assinatura usando o segredo do provedor e comparação de tempo constante.
- Verifique o timestamp para prevenir ataques de replay.
- Responda com 2xx rapidamente—confirme o recebimento antes de fazer processamento pesado.
- Processe de forma assíncrona se necessário, usando uma fila.
- Garanta idempotência rastreando IDs de eventos.
- Registre tudo para depuração e auditoria.
- Monitore falhas e configure alertas para retentativas repetidas.
FAQ
O que é um payload de webhook assinado?
Um payload assinado inclui uma assinatura criptográfica (geralmente HMAC-SHA256) gerada com um segredo compartilhado. O receptor recalcula a assinatura para verificar que a requisição veio do remetente esperado e não foi adulterada.
Por que webhooks precisam de retentativas?
Problemas de rede, indisponibilidade do servidor ou erros temporários podem fazer com que uma entrega de webhook falhe. Retentativas com backoff exponencial garantem que o evento seja eventualmente entregue sem sobrecarregar o receptor.
Como evito processamento duplicado de webhooks?
Use idempotência: inclua um ID de evento único no payload, armazene os IDs processados e projete suas operações para serem idempotentes (ex.: upserts). Assim, mesmo que um webhook seja entregue várias vezes, o efeito é o mesmo.
Se você precisa inspecionar ou depurar payloads de webhook, o JSON Formatter pode ajudar a formatar e validar dados JSON rapidamente.