ДескусДескус · Документация
Все разделыВебхуки

Проверка подписи: Node.js, PHP, Python

Подпись доказывает, что запрос пришёл от Дескуса и тело не меняли по дороге. Это HMAC-SHA256 от сырого тела запроса на секрете подписки, в шестнадцатеричном виде, с приставкой sha256=.

X-Deskus-Signature: sha256=<hex HMAC-SHA256(секрет, сырое тело)>

Правила

  • Считайте по байтам тела до разбора JSON. Разобрали и собрали обратно — пробелы и порядок полей поменяются, подпись не сойдётся.

  • Сравнивайте за постоянное время (timingSafeEqual, hash_equals, compare_digest), а не обычным ==.

  • Секрет whsec_… — у подписки: показать или заменить — в меню подписки в настройках. Через API он приходит один раз, при создании.

  • Подпись та же, что у канала «Приём по API» в обратную сторону: одна проверка на оба направления.

Node.js (Express)

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

const app = express();
const secret = process.env.DESKUS_WEBHOOK_SECRET;

// express.raw — чтобы получить тело байтами, а не разобранный JSON
app.post("/deskus/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(req.body).digest("hex");
  const got = Buffer.from(req.get("X-Deskus-Signature") || "");
  const want = Buffer.from(expected);
  if (got.length !== want.length || !crypto.timingSafeEqual(got, want)) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200); // ответить сразу, работа — после
  queue.add(event);
});

PHP

<?php
$secret = getenv('DESKUS_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // сырое тело
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
$got = $_SERVER['HTTP_X_DESKUS_SIGNATURE'] ?? '';

if (!hash_equals($expected, $got)) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
http_response_code(200);
// …обработка $event['type'] и $event['data']

Python (Flask)

import hashlib
import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["DESKUS_WEBHOOK_SECRET"].encode()


@app.post("/deskus/webhook")
def deskus_webhook():
    raw = request.get_data()  # сырое тело, до разбора JSON
    expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Deskus-Signature", "")):
        abort(401)
    event = request.get_json()
    # …обработка event["type"] и event["data"]
    return "", 200

Если подпись не сходится

  • Тело уже разобрано фреймворком — включите получение сырых байтов, как в примерах.

  • Не тот секрет: после «Заменить секрет» старый перестаёт действовать сразу.

  • Прокси перед приложением меняет тело (сжатие, перекодировка) — проверяйте подпись там, где тело исходное.

Не нашли ответ? Напишите нам
Поддержка ответит в рабочее время.
Вы сотрудник «Дескус»?Изменить статью в Дескусе