إنتقل إلى المحتوى الرئيسي

توقيعات الـ webhooks

تحقق من أن طلبات الـ webhook صادرة فعلًا من OrcaRail عبر التحقق من توقيعاتها.

لماذا تتحقق من التوقيعات؟​

تضمن توقيعات الـ webhooks ما يلي:

  • أن الطلب صادر فعلًا من OrcaRail
  • أن الحمولة لم يُتلاعب بها
  • أنك لا تعالج طلبات خبيثة

كيف يعمل​

توقّع OrcaRail كل طلب webhook باستخدام HMAC SHA-256 وسر الـ webhook الخاص بك. يجب أن يتحقق خادمك من هذا التوقيع قبل معالجة الحدث.

ترويسة التوقيع​

تضمّن OrcaRail التوقيع في ترويسة x-webhook-signature:

x-webhook-signature: abc123def456...

سر الـ webhook​

سر توقيع الـ webhook هو سر مفتاح API الخاص بك (sk_live_...)، وهو القيمة نفسها التي حصلت عليها عند إنشاء مفتاح API. اضبطه عبر PAYMENTS_WEBHOOK_SECRET (أو متغير البيئة في تطبيقك). احفظه بأمان ولا تكشفه أبدًا في كود جهة العميل.

عملية التحقق​

1. احصل على التوقيع​

استخرج التوقيع من ترويسة x-webhook-signature:

const signature = req.headers['x-webhook-signature']

2. احسب التوقيع المتوقع​

احسب التوقيع المتوقع باستخدام HMAC SHA-256:

const crypto = require('crypto')
const webhookSecret = process.env.PAYMENTS_WEBHOOK_SECRET

// Get the raw request body
const payload = JSON.stringify(req.body)

// Compute HMAC SHA-256
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex')

3. قارن التوقيعات​

قارن التوقيع المستلم بالتوقيع المتوقع باستخدام مقارنة آمنة زمنيًا:

const crypto = require('crypto')

function verifySignature(receivedSignature, expectedSignature) {
if (!receivedSignature || !expectedSignature) {
return false
}

const receivedBuffer = Buffer.from(receivedSignature)
const expectedBuffer = Buffer.from(expectedSignature)

// Use timing-safe comparison to prevent timing attacks
if (receivedBuffer.length !== expectedBuffer.length) {
return false
}

return crypto.timingSafeEqual(receivedBuffer, expectedBuffer)
}

مثال كامل​

Node.js (Express)​

const express = require('express')
const crypto = require('crypto')

const app = express()

// Middleware to capture raw body for signature verification
app.use('/webhooks/orcarail', express.raw({ type: 'application/json' }))

app.post('/webhooks/orcarail', (req, res) => {
const webhookSecret = process.env.PAYMENTS_WEBHOOK_SECRET

if (!webhookSecret) {
console.warn(
'PAYMENTS_WEBHOOK_SECRET not set, skipping signature verification'
)
// In production, you should always verify signatures
// return res.status(500).json({ error: 'Webhook secret not configured' });
}

// Get signature from header
const receivedSignature = req.headers['x-webhook-signature']

if (!receivedSignature && webhookSecret) {
return res.status(400).json({ error: 'Missing webhook signature' })
}

// Compute expected signature
const payload = req.body.toString()
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex')

// Verify signature
if (webhookSecret && !verifySignature(receivedSignature, expectedSignature)) {
return res.status(400).json({ error: 'Invalid webhook signature' })
}

// Parse JSON payload
const event = JSON.parse(payload)

// Process event
handleWebhookEvent(event)

res.status(200).json({ received: true })
})

function verifySignature(receivedSignature, expectedSignature) {
if (!receivedSignature || !expectedSignature) {
return false
}

const receivedBuffer = Buffer.from(receivedSignature)
const expectedBuffer = Buffer.from(expectedSignature)

if (receivedBuffer.length !== expectedBuffer.length) {
return false
}

return crypto.timingSafeEqual(receivedBuffer, expectedBuffer)
}

Python (Flask)​

import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/orcarail', methods=['POST'])
def webhook():
webhook_secret = os.environ.get('PAYMENTS_WEBHOOK_SECRET')

if not webhook_secret:
return jsonify({'error': 'Webhook secret not configured'}), 500

# Get signature from header
received_signature = request.headers.get('x-webhook-signature')

if not received_signature:
return jsonify({'error': 'Missing webhook signature'}), 400

# Get raw request body
payload = request.get_data()

# Compute expected signature
expected_signature = hmac.new(
webhook_secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()

# Verify signature using timing-safe comparison
if not hmac.compare_digest(received_signature, expected_signature):
return jsonify({'error': 'Invalid webhook signature'}), 400

# Parse JSON payload
event = request.get_json()

# Process event
handle_webhook_event(event)

return jsonify({'received': True}), 200

Ruby (Rails)​

class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token

def orcarail
webhook_secret = ENV['PAYMENTS_WEBHOOK_SECRET']

unless webhook_secret
return render json: { error: 'Webhook secret not configured' }, status: 500
end

received_signature = request.headers['x-webhook-signature']

unless received_signature
return render json: { error: 'Missing webhook signature' }, status: 400
end

payload = request.body.read

expected_signature = OpenSSL::HMAC.hexdigest(
OpenSSL::Digest.new('sha256'),
webhook_secret,
payload
)

unless ActiveSupport::SecurityUtils.secure_compare(
received_signature,
expected_signature
)
return render json: { error: 'Invalid webhook signature' }, status: 400
end

event = JSON.parse(payload)

handle_webhook_event(event)

render json: { received: true }, status: 200
end
end

PHP​

تتحقق PHP SDK وتحلل في استدعاء واحد:

use OrcaRail\Exception\SignatureVerificationException;
use OrcaRail\Webhook;

$payload = file_get_contents('php://input'); // raw body, not a parsed array

try {
$event = Webhook::constructEvent(
$payload,
$_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '',
getenv('PAYMENTS_WEBHOOK_SECRET'),
);
} catch (SignatureVerificationException $exception) {
http_response_code(400);
echo json_encode(['error' => 'Invalid webhook signature']);
exit;
}

handle_webhook_event($event);

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

دون الـ SDK، احسب الـ HMAC بنفسك وقارنه باستخدام hash_equals():

$expected = hash_hmac('sha256', $payload, getenv('PAYMENTS_WEBHOOK_SECRET'));

if (!hash_equals($expected, strtolower($_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? ''))) {
http_response_code(400);
exit;
}

أفضل ممارسات الأمان​

1. تحقق دائمًا من التوقيعات في الإنتاج​

لا تتخطَّ التحقق من التوقيع أبدًا في الإنتاج، حتى لو كان اختياريًا:

if (process.env.NODE_ENV === 'production' && !webhookSecret) {
throw new Error('PAYMENTS_WEBHOOK_SECRET must be set in production')
}

2. استخدم مقارنة آمنة زمنيًا​

استخدم دائمًا دوال مقارنة آمنة زمنيًا لمنع هجمات التوقيت:

  • Node.js: crypto.timingSafeEqual()
  • Python: hmac.compare_digest()
  • Ruby: ActiveSupport::SecurityUtils.secure_compare()
  • PHP: hash_equals()

3. احفظ السر بأمان​

  • خزّن سر الـ webhook في متغيرات البيئة
  • لا تضفه أبدًا إلى نظام التحكم في الإصدارات
  • دوّره دوريًا
  • استخدم أسرارًا مختلفة لكل بيئة

4. تعامل مع غياب التوقيع بشكل سليم​

إذا كان التوقيع مفقودًا، فارفض الطلب:

if (!receivedSignature) {
return res.status(400).json({ error: 'Missing webhook signature' })
}

الاختبار دون توقيعات​

للتطوير المحلي، يمكنك تعطيل التحقق من التوقيع مؤقتًا:

const webhookSecret = process.env.PAYMENTS_WEBHOOK_SECRET

// Skip verification in development if secret not set
if (!webhookSecret && process.env.NODE_ENV === 'development') {
console.warn('Skipping webhook signature verification in development')
// Process event without verification
} else {
// Verify signature
if (!verifySignature(receivedSignature, expectedSignature)) {
return res.status(400).json({ error: 'Invalid webhook signature' })
}
}

:::warning مهم لا تعطّل التحقق من التوقيع أبدًا في الإنتاج، ولا حتى للاختبار. :::

الخطوات التالية​