Як перевірити підпис webhook-запитів у HelpCrunch

Перевіряйте webhook-запити за допомогою заголовка X-HelpCrunch-Signature
Написано Konstantine
Оновлено 1 день тому

Коли HelpCrunch надсилає webhook-запит, він містить заголовок X-HelpCrunch-Signature. Цей заголовок використовується для перевірки того, що запит справді надійшов від HelpCrunch і що його payload не був змінений під час передавання.

Якщо ваш застосунок відхиляє вхідні webhook-запити через невалідний підпис, найпоширеніша причина полягає в тому, що підпис перевіряється не за оригінальним сирим(raw) payload, а за вже розпарсеним або зміненим тілом запиту.


Як працює перевірка підпису webhook

Щоб перевірити webhook-запит від HelpCrunch на вашому боці:

  1. Отримайте сире тіло запиту (raw payload) саме в тому вигляді, в якому воно було отримане.

  2. Згенеруйте HMAC-SHA1 хеш, використовуючи ваш ключ підпису webhook.

  3. Порівняйте згенерований хеш зі значенням із заголовка X-HelpCrunch-Signature.

Підпис завжди передається в webhook-запитах HelpCrunch, тому якщо перевірка не проходить, проблема зазвичай полягає в тому, як обробляється payload до моменту валідації.


Важливі вимоги

Використовуйте сире тіло запиту

Підпис потрібно перевіряти саме за сирим тілом запиту (raw payload), до будь-якого JSON-парсингу або повторної серіалізації.

Чому це важливо: якщо ваш сервер спочатку розпарсить JSON, а потім перетворить його назад у рядок, навіть незначні зміни - наприклад, порядок ключів, пробіли або кодування - можуть призвести до того, що підпис більше не збігатиметься.

Використовуйте порівняння за константний час

Не порівнюйте підписи за допомогою звичайного порівняння рядків, наприклад == або ===.

Натомість використовуйте функцію порівняння за константний час, наприклад:

  • hash_equals у PHP

  • timingSafeEqual у Node.js

  • compare_digest у Python

Це допомагає захистити ваш endpoint від "атак по часу".

Формат підпису

HelpCrunch використовує:

  • Алгоритм: HMAC-SHA1

  • Формат результату: шістнадцятковий рядок

  • Довжина: 40 символів

Приклад для PHP

function verifyWebhookSignature(string $rawBody, string $signingKey, string $receivedSignature): bool
{
    $expectedSignature = hash_hmac('sha1', $rawBody, $signingKey);

    return hash_equals($expectedSignature, $receivedSignature);
}

// Usage
$rawBody = file_get_contents('php://input');
$receivedSignature = $_SERVER['HTTP_X_HELPCRUNCH_SIGNATURE'] ?? '';
$signingKey = 'your-webhook-signing-key';

if (!verifyWebhookSignature($rawBody, $signingKey, $receivedSignature)) {
    http_response_code(401);
    exit('Invalid signature');
}

Приклад для Node.js

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signingKey, receivedSignature) {
    const expectedSignature = crypto
        .createHmac('sha1', signingKey)
        .update(rawBody)
        .digest('hex');

    return crypto.timingSafeEqual(
        Buffer.from(expectedSignature),
        Buffer.from(receivedSignature)
    );
}

// Usage (Express)
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const receivedSignature = req.headers['x-helpcrunch-signature'];

    if (!verifyWebhookSignature(req.body, process.env.SIGNING_KEY, receivedSignature)) {
        return res.status(401).send('Invalid signature');
    }
    // process webhook...
});

Приклад для Python

import hmac
import hashlib

def verify_webhook_signature(raw_body: bytes, signing_key: str, received_signature: str) -> bool:
    expected_signature = hmac.new(
        signing_key.encode(),
        raw_body,
        hashlib.sha1
    ).hexdigest()

    return hmac.compare_digest(expected_signature, received_signature)

# Usage (Flask)
from flask import Flask, request

@app.route('/webhook', methods=['POST'])
def webhook():
    received_signature = request.headers.get('X-HelpCrunch-Signature', '')

    if not verify_webhook_signature(request.get_data(), SIGNING_KEY, received_signature):
        return 'Invalid signature', 401

Поширені причини, чому перевірка підпису не проходить

Якщо підпис не збігається, перевірте:

1. Чи тіло запиту не є сирим

Це найпоширеніша проблема. Переконайтеся, що ви хешуєте оригінальне тіло запиту, а не розпарсений JSON-об’єкт і не його повторно серіалізовану версію.

2. Чи використовується неправильний ключ підпису

Ще раз перевірте, що ваш застосунок використовує саме той webhook signing key, який налаштований для цього webhook.

3. Чи підпис порівнюється неправильно

Використовуйте функцію порівняння за константний час, а не звичайну перевірку на рівність.

4. Чи тіло запиту змінюється middleware

Деякі фреймворки автоматично парсять або трансформують вхідний JSON ще до того, як виконується ваш код перевірки. У такому разі потрібно налаштувати endpoint так, щоб він мав доступ до сирого тіла (raw payload) запиту в першу чергу.


Порада для діагностики проблеми:

Якщо webhook працює в Postman, але не проходить перевірку з реальними запитами від HelpCrunch, проблема часто не в самому заголовку, а в тому, як ваш сервер обробляє payload до моменту обчислення хешу.

Іншими словами, якщо ви вручну надсилаєте запит із таким самим значенням заголовка в Postman, це ще не означає, що перевірка підпису у вашому production-середовищі реалізована правильно.

Чи була наша стаття корисною?