enВойти в Senler

Подпись вебхуков

Проверяйте подпись входящих запросов с помощью секрета вебхука и обновляйте секрет при необходимости.

Основной раздел: Вебхуки.

Секрет и подпись

В блоке «Секрет вебхуков» хранится единый секрет подписи для всех вебхуков приложения, включая вызовы инструментов. Его можно показать или скрыть и скопировать в защищённое хранилище серверной части приложения. Ссылка «Подробнее» открывает окно с заголовками и формулой HMAC.

Если секрет потерян или скомпрометирован, нажмите «Заменить секрет». В окне подтверждения можно отменить действие или подтвердить замену. Старый секрет сразу перестанет работать для всех вебхуков и инструментов приложения, поэтому без паузы обновите его на бэкенде приложения.

Секрет и подпись. Отмеченные элементы: 7. окне подтверждения; 8. отменить; 9. подтвердить замену
5 / 5
7. окне подтверждения · 8. отменить · 9. подтвердить замену

Каждый запрос содержит заголовки:

  • Content-Type: application/json;
  • X-App-Id — Client ID приложения;
  • X-Webhook-Timestamp — время текущей попытки доставки, совпадающее с timestamp тела; при повторе оба значения меняются;
  • X-Webhook-Event-Id — уникальный ID события, совпадающий с event_id в теле;
  • X-Webhook-Signature — hex-строка HMAC-SHA256.

Подпись защищает всё тело запроса. Она рассчитывается как HMAC-SHA256 от строки v1.timestamp.event_id.canonicalJson(payload) с секретом приложения. canonicalJson рекурсивно сортирует ключи объектов, сохраняет порядок массивов и не добавляет пробелы. В примере request.body содержит разобранный JSON полученного запроса:

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

function canonicalize(value) {
  if (Array.isArray(value)) return value.map(canonicalize);
  if (value !== null && typeof value === "object") {
    return Object.fromEntries(
      Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
    );
  }
  return value;
}

const payload = request.body;
const deliveryTimestamp = request.headers["x-webhook-timestamp"] ?? "";
const eventId = request.headers["x-webhook-event-id"] ?? "";
const signature = request.headers["x-webhook-signature"] ?? "";
const signedContent = [
  "v1",
  payload.timestamp,
  payload.event_id,
  JSON.stringify(canonicalize(payload)),
].join(".");
const expected = createHmac("sha256", webhookSecret)
  .update(signedContent, "utf8")
  .digest();
const received = Buffer.from(signature, "hex");
const signatureIsValid =
  /^[a-f\d]{64}$/i.test(signature) &&
  payload.timestamp === deliveryTimestamp &&
  payload.event_id === eventId &&
  received.length === expected.length &&
  timingSafeEqual(received, expected);

Перед обработкой проверьте свежесть X-Webhook-Timestamp, подпись, соответствие X-Webhook-Event-Id полю event_id, X-App-Id, разрешённый project_id и тип события. Не записывайте секрет в логи и не передавайте его в чат. Повторный event_id не обрабатывайте заново.

Управление и замена секрета

В списке webhook показываются заданное название, URL, выбранные события, состояние, время последнего вызова и последний HTTP-статус. Через переключатель состояния webhook можно отключить и снова включить; отключённый webhook не получает новые боевые события.

Откройте страницу webhook, чтобы изменить название, URL или отмеченный галочками набор публичных событий, затем нажмите «Сохранить». Секрет не относится к отдельному URL: он доступен в общем блоке на странице вебхуков приложения.

Управление и замена секрета. Отмеченные элементы: 1. страницу webhook; 2. URL; 3. набор публичных событий; 4. «Сохранить»
1. страницу webhook · 2. URL · 3. набор публичных событий · 4. «Сохранить»

Кнопка удаления webhook открывает подтверждение. После окончательного удаления отправка на этот URL прекращается; единый секрет приложения не меняется.