Проверка подписи

Перед обработкой проверяйте подлинность каждого webhook.

Заголовки

ЗаголовокОписание
X-Webhook-IdУникальный идентификатор события. Используйте для защиты от повторной обработки.
X-Webhook-TimestampUnix timestamp в секундах — время формирования webhook.
X-Webhook-Signature-VersionВерсия алгоритма подписи. Сейчас v1.
X-Webhook-SignatureПодпись HMAC-SHA256 в формате v1=<signature>.

Порядок проверки

  1. Получите raw body запроса.
  2. Прочитайте X-Webhook-Timestamp, X-Webhook-Signature-Version и X-Webhook-Signature.
  3. Проверьте, что версия подписи — v1.
  4. Убедитесь, что timestamp отличается от текущего времени не больше чем на 5 минут в любую сторону.
  5. Сформируйте строку timestamp.raw_body.
  6. Рассчитайте HMAC-SHA256 с webhook secret.
  7. Сравните результат с X-Webhook-Signature через constant-time сравнение.
  8. Обработайте JSON и сохраните X-Webhook-Id. Если такой ID уже обработан, не выполняйте бизнес-логику повторно.
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(rawBody, timestamp, signature, version, secret) {
  if (
    version !== "v1" ||
    !/^\d{10,11}$/.test(timestamp) ||
    !/^v1=[a-fA-F0-9]{64}$/.test(signature) ||
    Math.abs(Date.now() / 1000 - Number(timestamp)) > 300
  ) {
    return false;
  }

  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();

  return timingSafeEqual(expected, Buffer.from(signature.slice(3), "hex"));
}

Если подпись не совпала

Webhook с неверной подписью нужно отклонить. Пример записи в логе:

Webhook received
ID (untrusted header): 4ad561fd-6ecd-445d-a65f-989ea5995f18
Signature: INVALID
Timestamp: fresh
Check: HMAC mismatch
Body: 1159 bytes