Проверка подписи: 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Если подпись не сходится
Тело уже разобрано фреймворком — включите получение сырых байтов, как в примерах.
Не тот секрет: после «Заменить секрет» старый перестаёт действовать сразу.
Прокси перед приложением меняет тело (сжатие, перекодировка) — проверяйте подпись там, где тело исходное.
