التحقق من Webhooks

كيفية التحقق من أن webhooks أصلية ولم يتم التلاعب بها.

لماذا التحقق

أي شخص يعرف عنوان webhook الخاص بك قد يحاول إرسال أحداث مزيفة. لتجنب ذلك، يتضمن كل webhook توقيعاً تشفيرياً يجب التحقق منه.

تحقق دائماً من التوقيعات

لا تعالج أبداً webhook بدون التحقق من توقيعه، خاصة إذا كان يقوم بإجراءات مهمة مثل إنشاء الطلبات أو تعديل البيانات.

كيف يعمل

  1. عند إنشاء webhook، يتم إنشاء secret فريد
  2. عند إرسال webhook، نحسب hash HMAC-SHA256 للبيانات باستخدام سرك
  3. نضمّن هذا الهاش في الرأس X-SalonBookIt-Signature
  4. خادمك يحسب نفس الهاش ويقارنه

تنسيق التوقيع

X-SalonBookIt-Signature: sha256=abc123def456...

يتكون التوقيع من:

  • sha256= - بادئة تشير إلى الخوارزمية
  • يتبعه hash HMAC-SHA256 بالست عشري

الحصول على سرك

  1. اذهب إلى الإعدادات → التكاملات
  2. في قسم Webhooks، انقر على webhook الذي تريد التحقق منه
  3. انسخ قيمة الحقل Secret
احفظ السر بشكل آمن

استخدم متغيرات البيئة أو مدير الأسرار. لا تضمنه أبداً في الكود المصدري.

التحقق من التوقيع

Node.js / JavaScript

JavaScript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
    // استخراج الهاش من التوقيع
    const sigHash = signature.replace('sha256=', '');

    // حساب الهاش المتوقع
    const expectedHash = crypto
        .createHmac('sha256', secret)
        .update(payload, 'utf8')
        .digest('hex');

    // المقارنة بشكل آمن (تجنب هجمات التوقيت)
    return crypto.timingSafeEqual(
        Buffer.from(sigHash, 'hex'),
        Buffer.from(expectedHash, 'hex')
    );
}

// في نقطة النهاية الخاصة بك
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-salonbookit-signature'];
    const payload = req.body.toString();

    if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
        console.error('توقيع غير صالح');
        return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(payload);
    console.log('حدث صالح:', event.type);

    // معالجة الحدث...

    res.json({ received: true });
});

Python

Python
import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    """يتحقق من توقيع HMAC لـ webhook."""
    if not signature.startswith('sha256='):
        return False

    sig_hash = signature[7:]  # إزالة 'sha256='

    expected_hash = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    # مقارنة آمنة ضد هجمات التوقيت
    return hmac.compare_digest(sig_hash, expected_hash)


@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-SalonBookIt-Signature', '')
    payload = request.get_data()

    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        abort(401, 'Invalid signature')

    event = request.get_json()
    print(f"حدث صالح: {event['type']}")

    # معالجة الحدث...

    return {'received': True}

PHP

PHP
<?php

$webhookSecret = getenv('WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SALONBOOKIT_SIGNATURE'] ?? '';

function verifySignature(string $payload, string $signature, string $secret): bool
{
    if (strpos($signature, 'sha256=') !== 0) {
        return false;
    }

    $sigHash = substr($signature, 7);
    $expectedHash = hash_hmac('sha256', $payload, $secret);

    return hash_equals($expectedHash, $sigHash);
}

if (!verifySignature($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    die('Invalid signature');
}

$event = json_decode($payload, true);
error_log('حدث صالح: ' . $event['type']);

// معالجة الحدث...

http_response_code(200);
echo json_encode(['received' => true]);

Ruby

Ruby
require 'openssl'
require 'sinatra'

WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']

def verify_signature(payload, signature, secret)
  return false unless signature.start_with?('sha256=')

  sig_hash = signature[7..]
  expected_hash = OpenSSL::HMAC.hexdigest('sha256', secret, payload)

  Rack::Utils.secure_compare(expected_hash, sig_hash)
end

post '/webhook' do
  payload = request.body.read
  signature = request.env['HTTP_X_SALONBOOKIT_SIGNATURE'] || ''

  unless verify_signature(payload, signature, WEBHOOK_SECRET)
    halt 401, 'Invalid signature'
  end

  event = JSON.parse(payload)
  puts "حدث صالح: #{event['type']}"

  # معالجة الحدث...

  content_type :json
  { received: true }.to_json
end

التحقق من الطابع الزمني (اختياري)

لمزيد من الأمان، يمكنك التحقق من أن webhook ليس قديماً جداً باستخدام الرأس X-SalonBookIt-Timestamp:

JavaScript
const timestamp = parseInt(req.headers['x-salonbookit-timestamp']);
const now = Math.floor(Date.now() / 1000);
const tolerance = 300; // 5 دقائق

if (Math.abs(now - timestamp) > tolerance) {
    console.error('Webhook قديم جداً');
    return res.status(401).send('Timestamp expired');
}

هذا يمنع هجمات إعادة التشغيل حيث يحاول شخص ما إعادة إرسال webhook تم التقاطه سابقاً.

الأخطاء الشائعة

التوقيع لا يتطابق

  • تحقق من أنك تستخدم السر الصحيح
  • تأكد من استخدام البيانات raw (بدون تحليل)
  • لا تعدل البيانات قبل التحقق

تم تعديل البيانات

إذا كان إطار العمل الخاص بك يحلل JSON تلقائياً، احصل على الجسم الخام أولاً. في Express:

app.use(express.raw({ type: 'application/json' }))

مشاكل الترميز

تأكد من أن البيانات تُعامل كـ UTF-8 عند حساب الهاش.