Skip to content
Todos os artigos

Verificar corretamente as assinaturas do webhook e lidar com novas tentativas.

Charles Archibong

, Cofundador

Verificar a assinatura V2 nos bytes brutos da solicitação, rejeitar carimbos de data/hora fora da janela de reprodução, registrar o ID do evento sob uma restrição exclusiva antes de realizar qualquer operação e retornar 2xx somente quando o evento for armazenado de forma duradoura.

Título "Assinaturas e novas tentativas de webhook" ao lado de uma ilustração de uma janela de código com colchetes angulares, sobre um degradê azul-índigo profundo.

Pontos principais

  • Calcule o HMAC sobre os bytes brutos da solicitação, nunca sobre o JSON reserializado.
  • O carimbo de data/hora da assinatura, juntamente com a janela de reprodução, é o que impede que uma entrega antiga, devidamente assinada, seja reproduzida.
  • Elimine duplicados pelo ID do evento, não pelo ID da entrega: um reenvio manual é uma nova entrega do mesmo evento.
  • Retorne 2xx somente após o evento ser armazenado de forma duradoura e execute o trabalho lento em seguida.

Um receptor de webhook tem três tarefas, e elas acontecem em uma ordem fixa. Prove que a solicitação veio de Toden Verificando a assinatura nos bytes exatos recebidos. Prove que o evento é recente comparando o carimbo de data/hora assinado com a janela de reprodução. Em seguida, prove que você ainda não agiu sobre ele registrando o ID do evento sob uma restrição de unicidade. Somente após essas três etapas você retorna.2xxE só depois disso você começa o trabalho de verdade.

Se a ordem estiver errada, cada falha passa despercebida. Se a análise for feita antes da verificação, um corpo falsificado chegará à sua lógica de negócios. Se o carimbo de data/hora for ignorado, uma solicitação capturada poderá ser reproduzida indefinidamente. Se houver desduplicação com base na chave errada, um reenvio integrará um cliente duas vezes. Este artigo descreve cada etapa para Toden webhooks (abre num novo separador) , com um receptor que você pode adaptar.

O que vem em cada entrega?

Cada entrega é uma HTTP POSTcom um JSON corpo e um conjunto de cabeçalhos. Os que são importantes para a verificação, conforme documentado em autenticação por webhook (abre num novo separador) :

Cabeçalho

Exemplo

O que fazer com isso?

X-Myaza-Signature-V2

t=1786441200,v1=8df1…

Analise o carimbo de data/hora e cadav1valor. Verifique em relação a estes.

X-Myaza-Replay-Window

300

A janela de reprodução do endpoint em segundos. Não é assinada, portanto, nunca permita que ela amplie sua verificação.

X-Myaza-Event-Id

evt_…

Chave estável para desduplicação.

X-Myaza-Delivery

del_…

O registro de entrega. Útil para rastreamento, não para desduplicação.

X-Myaza-Event

verification.completed

Para fins de encaminhamento. O órgão signatário permanece com autoridade.

X-Myaza-Signature

sha256=…

Assinatura de compatibilidade legada apenas sobre o corpo.

Se você construiu contra a versão mais antigaX-Myaza-SignatureO cabeçalho ainda funciona. Ele assina o corpo da requisição sem um carimbo de data/hora, o que significa que não é possível determinar se uma requisição é recente. Migre para o cabeçalho V2.

Etapa 1: verificar a assinatura nos bytes brutos

A assinatura está em hexadecimal em minúsculas. HMAC - SHA256 , indexado com o segredo do seu endpoint, sobre a string<t>.<rawBody>: o carimbo de data/hora do cabeçalho, um ponto final e, em seguida, o corpo da solicitação exatamente como foi recebido.

"Exatamente como chegou" é a parte que quebra as integrações. A maioria dos frameworks web analisa JSON antes da execução do seu manipulador. Se você verificar em relação aJSON.stringify(req.body)Você está assinando uma reserialização: espaços em branco, ordem das chaves e formatação de números podem ser diferentes do que foi assinado, e a verificação falha intermitentemente de maneiras que parecem indicar uma chave secreta inválida. Configure a rota para fornecer o buffer bruto.

Compare usando uma função de tempo constante e compare com cadav1valor, não apenas o primeiro. Durante a rotação secreta Toden O cabeçalho contém tanto o segredo ativo quanto o segredo que está sendo desativado, portanto, inclui dois candidatos. Aceite a solicitação se qualquer um deles corresponder e remova o segredo antigo somente após o término do período de tolerância no painel de controle.

Etapa 2: rejeitar registros de data e hora obsoletos

Uma assinatura válida comprova quem enviou uma solicitação, não quando. Qualquer pessoa que capturasse uma entrega (por exemplo, de um registro de proxy) poderia reenviá-la, e ela ainda seria verificada. O carimbo de data/hora assinado elimina essa lacuna: porquetEstá dentro da string assinada; um atacante não pode alterá-la sem quebrar a assinatura.

Rejeite qualquer entrega cujo carimbo de data/hora esteja ausente, não possa ser analisado, seja anterior à janela de reprodução ou esteja em um futuro implausivelmente distante.X-Myaza-Replay-WindowO cabeçalho `HttpClient` indica a janela configurada para o endpoint, mas ele fica fora da string assinada, então qualquer pessoa que reproduzir uma requisição pode alterá-lo. Use-o para restringir sua verificação, se desejar, nunca para aumentá-la: limite-o à janela que você configurou para o endpoint. Certifique-se de que o relógio do seu servidor esteja sincronizado; um relógio desalinhado transforma essa verificação em rejeições aleatórias.

Retornar401para uma assinatura ausente, desatualizada ou inválida. Não devolva.200Para interromper as tentativas: isso informa ao remetente que o evento foi aceito.

Etapa 3: eliminar duplicados no evento, não na entrega.

Os webhooks são entregues pelo menos uma vez. Um receptor que expira, uma falha na rede após sua resposta ou um colega de equipe que clica em "Reenviar" no painel, tudo isso fará com que o mesmo evento seja enviado para você novamente.

A questão é qual identificador significa "a mesma coisa". Existem dois candidatos, e eles se comportam de maneira diferente:

  • O ID do evento (X-Myaza-Event-Id, eid(No envelope) identifica o evento comercial. Ele permanece o mesmo em novas tentativas automáticas e em um reenvio manual.

  • O ID de entrega (X-Myaza-Delivery) identifica um registro de entrega. Um reenvio manual cria outro registro de entrega, portanto, ele recebe um novo ID de entrega.

Defina a sua desduplicação com base no ID do evento. O guia de testes de webhook (abre num novo separador) afirma isso diretamente e sugere uma inserção que não faça nada em caso de conflito:

insert into received_myaza_events (event_id, event_type, payload)
values (:id, :type, :payload)
on conflict (event_id) do nothing;

Um evento precisa de atenção.verification.status_updatedé enviado plano, não embalado em{ id, event, createdAt, data }envolver os outros eventos usados, então o corpo não temidO campo. Obter o ID do evento do cabeçalho funciona para todos os tipos de evento, sendo essa a regra mais simples.

Passo 4: reconheça rapidamente e depois faça o trabalho.

Retornar2xxO evento só deve ser processado fora da requisição quando armazenado de forma permanente, e não antes: atualize o registro do cliente, notifique o solicitante, abra um chamado interno. O processamento lento dentro da requisição pode causar timeouts, e um timeout é tratado como uma falha e a requisição é tentada novamente.

Tipos de eventos desconhecidos devem ser armazenados ou ignorados e ainda assim reconhecidos. Novos eventos são adicionados ao catálogo ao longo do tempo; rejeitar um evento que você não reconhece gera um loop de repetição para algo que você nunca pretendeu processar.

Um receptor que faz todas as quatro coisas.

Adaptado do exemplo em Node.js da documentação, com a janela de reprodução limitada e a inserção para desduplicação adicionada.saveEventOnceequeueSeja responsável pelo seu próprio armazenamento e pela execução de tarefas.

import crypto from "node:crypto";
import express from "express";

const app = express();

app.post("/webhooks/myaza", express.raw({ type: "application/json" }), async (req, res) => {
  const parts = String(req.header("X-Myaza-Signature-V2") || "").split(",");
  const timestamp = parts.find((p) => p.startsWith("t="))?.slice(2);
  const candidates = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
  const MAX_WINDOW = 300; // the replay window configured for this endpoint
  const windowSeconds = Math.min(Number(req.header("X-Myaza-Replay-Window")) || MAX_WINDOW, MAX_WINDOW);

  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > windowSeconds) {
    return res.status(401).send("Expired signature");
  }

  const expected = crypto
    .createHmac("sha256", process.env.MYAZA_WEBHOOK_SECRET)
    .update(`${timestamp}.`)
    .update(req.body)
    .digest("hex");

  const valid = candidates.some((c) =>
    c.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(c), Buffer.from(expected))
  );
  if (!valid) return res.status(401).send("Invalid signature");

  const eventId = req.header("X-Myaza-Event-Id");
  const payload = JSON.parse(req.body.toString("utf8"));
  const isNew = await saveEventOnce(eventId, req.header("X-Myaza-Event"), payload);

  res.status(200).send("OK");
  if (isNew) queue.add("myaza-event", { eventId });
});

Como o evento é armazenado antes da resposta, ocorre uma falha entre a resposta e a resposta.queue.addNão perde nada: um pequeno processo de varredura recolhe eventos armazenados que nunca foram processados. Durante uma rotação, ambos os segredos são mantidos e um valor esperado é calculado para cada um; a verificação é aprovada se algum candidato corresponder a algum segredo.

Como as novas tentativas se comportam e o que isso significa para você.

Uma entrega que não recebe um2xx, ou se o tempo limite for atingido, a tentativa é repetida com um intervalo de espera (backoff):

Tentar

Atraso após tentativa anterior

1

30 segundos

2

5 minutos

3

30 minutos

4

2 horas

5

24 horas

Após cinco tentativas falhadas, a entrega é marcada.FAILEDe pode ser repetida manualmente a partir do painel de controle. Duas consequências se seguem.

Primeiramente, uma interrupção de um ou dois dias do seu lado é recuperável sem perda de dados, desde que você a corrija antes da última tentativa e lide com a sequência de novas tentativas de forma independente quando o serviço retornar. Gere alertas para falhas repetidas na entrega para que você tome conhecimento da interrupção por meio do seu monitoramento, em vez de apenas por um cliente.

Em segundo lugar, as entregas são repetidas independentemente, portanto, podem chegar fora de ordem. Suponha que seu fluxo de trabalho encaminhe uma verificação para revisão e um responsável pela conformidade a aprove uma hora depois: duasverification.status_updatedeventos, cada um com seu próprio cronograma de novas tentativas. Se a primeira entrega falhar e for bem-sucedida na segunda tentativa, você poderá receberapprovedantesin_reviewA regra documentada é tratar a mais recentechangedAtcomo atual, então armazene-a e ignore uma atualização mais antiga do que aquela que você possui. Quando os riscos são altos, leia o estado atual do API antes de agir.

Teste antes da produção.

Crie um endpoint de teste (sandbox) e use o simulador na página do endpoint para enviar eventos de amostra devidamente assinados pelo caminho de entrega real. Em seguida, analise os casos de falha propositalmente:

  1. Envie um evento simulado e confirme se o destinatário verifica a assinatura V2 e retorna.2xxrapidamente.

  2. Alterar um byte do corpo capturado e confirmar a401.

  3. Reproduza uma solicitação capturada após o período ter expirado e confirme uma401.

  4. Reenvie a entrega pelo painel de controle e confirme se o efeito colateral ocorre pelo menos uma vez.

  5. Retornar um não-2xxtemporariamente e confirme se uma nova tentativa está agendada.

  6. Gire o segredo e confirme se ambas as assinaturas foram aceitas durante o período de tolerância.

Um receptor que passa em todos os seis testes se comportará corretamente no dia em que algo der errado, que é o único dia em que isso importa.

Fontes

Charles Archibong

Sobre o autor

Charles Archibong

Cofundador

Charles Archibong é cofundador da Toden. Ele escreve sobre verificação de identidade, tecnologia financeira e o trabalho prático de construção de serviços digitais confiáveis.

Crie o seu produto.Nós tratamos do resto.

Identidade e conformidade de ponta a ponta, segundo padrões globais, com preços para fundadores.