يتضمن كل طلب webhook من NueForm توقيعاً تشفيرياً يتيح لك التحقق من أن الطلب أصلي ولم يُعبث به. يجب عليك دائماً التحقق من هذا التوقيع قبل معالجة بيانات webhook.
كيف يعمل التوقيع
عندما يُرسل NueForm طلب webhook، فإنه:
- يُسلسل الحمولة كسلسلة JSON.
- يحسب تجزئة HMAC-SHA256 لسلسلة JSON باستخدام مفتاح webhook السري كمفتاح.
- يُشفر التجزئة كسلسلة سداسية عشرية بأحرف صغيرة.
- يُرسل الملخص السداسي العشري في ترويسة HTTP باسم
X-NueForm-Signature.
من جانبك، تقوم بنفس العملية على جسم الطلب الخام وتقارن نتيجتك بقيمة الترويسة. إذا تطابقتا، فالطلب أصلي.
ترويسة التوقيع
X-NueForm-Signature: 5d41402abc4b2a76b9719d911017c592a3f6e7d4b9c1d5e8f2a7b3c6d9e0f1a2
قيمة الترويسة هي سلسلة سداسية عشرية من ٦٤ حرفاً (مخرج HMAC-SHA256).
مفتاح Webhook السري
مفتاح webhook السري هو سلسلة سداسية عشرية من ٦٤ حرفاً مُولّدة من ٣٢ بايت عشوائي. وهو فريد لحسابك ومشترك عبر جميع نقاط نهاية webhook (لكل نموذج والعامة).
استرجاع المفتاح السري
اجلب مفتاحك الحالي عبر واجهة API:
curl https://app.nueform.com/api/v1/webhooks/secret \
-H "Authorization: Bearer nf_your_api_key"
الاستجابة:
{
"secret": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}
إذا لم يكن لديك مفتاح سري بعد، يُنشئ NueForm واحداً تلقائياً عند أول طلب.
إعادة إنشاء المفتاح السري
إذا تم اختراق مفتاحك السري، أعد إنشاءه فوراً:
curl -X POST https://app.nueform.com/api/v1/webhooks/secret/regenerate \
-H "Authorization: Bearer nf_your_api_key"
إعادة إنشاء مفتاحك السري تُبطل المفتاح القديم فوراً. ستستخدم جميع تسليمات webhook من تلك النقطة فصاعداً المفتاح الجديد. حدّث كود التحقق قبل أو فوراً بعد إعادة الإنشاء لتجنب رفض webhooks صالحة.
نماذج أكواد التحقق
Node.js
import crypto from 'crypto';
function verifyWebhookSignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// استخدم مقارنة آمنة زمنياً لمنع هجمات التوقيت
const a = Buffer.from(signature, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(a, b);
}
// مثال Express.js
app.post('/webhooks/nueform', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-nueform-signature'];
const rawBody = req.body.toString();
if (!verifyWebhookSignature(rawBody, signature, process.env.NUEFORM_WEBHOOK_SECRET)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(rawBody);
console.log('Verified webhook:', payload.event, payload.responseId);
// معالجة webhook بشكل غير متزامن
processWebhookAsync(payload);
res.status(200).send('OK');
});
عند استخدام Express.js، يجب استخدام express.raw() أو express.text() للوصول إلى جسم الطلب الخام. إذا استخدمت express.json()، سيتم تحليل الجسم وإعادة تسلسله، مما قد يُنتج سلسلة مختلفة عن التي تم توقيعها --- مما يتسبب في فشل التحقق.
Python
import hashlib
import hmac
def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
# استخدم مقارنة آمنة زمنياً
return hmac.compare_digest(expected, signature)
# مثال Flask
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/nueform', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-NueForm-Signature', '')
raw_body = request.get_data()
if not verify_webhook_signature(raw_body, signature, WEBHOOK_SECRET):
abort(401, 'Invalid signature')
payload = request.get_json()
print(f"Verified webhook: {payload['event']} {payload['responseId']}")
# معالجة غير متزامنة (مثلاً، إضافة إلى قائمة Celery)
process_webhook.delay(payload)
return 'OK', 200
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifyWebhookSignature(body []byte, signature string, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
// hmac.Equal تقوم بمقارنة ثابتة الوقت
return hmac.Equal([]byte(expected), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
signature := r.Header.Get("X-NueForm-Signature")
if !verifyWebhookSignature(body, signature, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// معالجة حمولة webhook
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
PHP
<?php
function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
// استخدم مقارنة آمنة زمنياً
return hash_equals($expected, $signature);
}
// الاستخدام
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_NUEFORM_SIGNATURE'] ?? '';
$secret = getenv('NUEFORM_WEBHOOK_SECRET');
if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
http_response_code(401);
echo 'Invalid signature';
exit;
}
$payload = json_decode($rawBody, true);
error_log("Verified webhook: {$payload['event']} {$payload['responseId']}");
// معالجة webhook
processWebhook($payload);
http_response_code(200);
echo 'OK';
Ruby
require 'openssl'
require 'json'
def verify_webhook_signature(raw_body, signature, secret)
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body)
# استخدم مقارنة آمنة زمنياً
Rack::Utils.secure_compare(expected, signature)
end
# مثال Sinatra
post '/webhooks/nueform' do
raw_body = request.body.read
signature = request.env['HTTP_X_NUEFORM_SIGNATURE'] || ''
unless verify_webhook_signature(raw_body, signature, ENV['NUEFORM_WEBHOOK_SECRET'])
halt 401, 'Invalid signature'
end
payload = JSON.parse(raw_body)
logger.info "Verified webhook: #{payload['event']} #{payload['responseId']}"
# معالجة غير متزامنة
WebhookProcessorJob.perform_async(payload)
status 200
body 'OK'
end
المقارنة الآمنة زمنياً
تستخدم جميع نماذج الأكواد أعلاه مقارنة آمنة زمنياً (تُسمى أيضاً مقارنة ثابتة الوقت) عند فحص التوقيعات. هذه ممارسة أمنية مهمة تمنع هجمات التوقيت.
يعمل هجوم التوقيت عن طريق قياس المدة التي تستغرقها مقارنة السلاسل. المقارنة العادية بـ == تعيد false فور العثور على أول حرف غير متطابق، لذا يمكن للمهاجم تعلم حرف واحد من التوقيع في كل مرة عن طريق قياس زمن استجابة الرد.
تستغرق دوال المقارنة الآمنة زمنياً نفس المدة بغض النظر عن عدد الأحرف المتطابقة، مما يجعل هذا الهجوم غير عملي.
| اللغة | الدالة |
|---|---|
| Node.js | crypto.timingSafeEqual() |
| Python | hmac.compare_digest() |
| Go | hmac.Equal() |
| PHP | hash_equals() |
| Ruby | Rack::Utils.secure_compare() |
لا تستخدم أبداً === أو == أو .equals() لمقارنة توقيعات HMAC. استخدم دائماً دالة المقارنة الآمنة زمنياً المدمجة في لغتك.
ماذا تفعل إذا فشل التحقق
إذا فشل التحقق من التوقيع، يجب على نقطة النهاية:
- إعادة رمز حالة
401 Unauthorized. لا تعالج الحمولة. - تسجيل الفشل لأغراض تصحيح الأخطاء. تضمين IP الطلب والطابع الزمني و(اختيارياً) التوقيع المستلم.
- عدم كشف مفتاحك السري في رسائل الخطأ أو السجلات.
- تحقيق في الأسباب الشائعة:
- مفتاح سري خاطئ. تأكد من استخدام مفتاح webhook السري الحالي. إذا أعدت إنشاءه مؤخراً، حدّث كود التحقق.
- تحويل الجسم. تأكد من التحقق مقابل جسم الطلب الخام، وليس نسخة مُحللة ومُعاد تسلسلها. الوسائط التي تحلل JSON قبل تشغيل كود التحقق هي السبب الأكثر شيوعاً للفشل.
- مشاكل الترميز. يجب التعامل مع الجسم الخام كبايتات UTF-8. تأكد من أن إطار العمل لا يُطبق تحويلات ترميز غير متوقعة.
- تعديل الوكيل. إذا كان وكيل عكسي (مثل Cloudflare أو nginx) يُعدل جسم الطلب، فلن يتطابق التوقيع. أعدّ الوكيل لتمرير الجسم دون تغيير.
قائمة تحقق استكشاف الأخطاء
| المشكلة | الحل |
|---|---|
| التوقيع لا يتطابق أبداً | تحقق من أنك تقرأ بايتات الجسم الخام، وليس كائن JSON مُحلل |
| التوقيع توقف عن التطابق فجأة | تحقق مما إذا تم إعادة إنشاء مفتاح webhook السري |
| التوقيع يتطابق محلياً لكن ليس في الإنتاج | تحقق من تحويل الجسم بواسطة الوكيل أو CDN |
ترويسة X-NueForm-Signature مفقودة | تأكد من أن إطار العمل يحافظ على الترويسات المخصصة (بعضها يحذف الترويسات بادئة X-) |