انتقل إلى المحتوى

توقيع webhook: التحقق من طلبات مُجيب بـ HMAC-SHA256

توقيع webhook في مُجيب هو الترويسة X-Mujib-Signature التي ترافق كل طلب، وفيها وقت الإرسال وبصمة HMAC-SHA256 محسوبة بسر الـ Webhook على الوقت والجسم الخام معًا. تحقق منها قبل أن تثق بأي حدث، وارفض الطلب إذا مضى على توقيعه أكثر من 5 دقائق.

بهذا تعرف أن الطلب جاء من مُجيب لا من أي جهة عرفت رابطك، وأن أحدًا لم يغيّر حرفًا فيه. السر يظهر لك مرة واحدة عند إنشاء الـ Webhook، كما في webhook واتساب.

كيف يُبنى توقيع webhook في مُجيب؟

رابط القسم «كيف يُبنى توقيع webhook في مُجيب؟»

يحسب مُجيب HMAC-SHA256 بسر الـ Webhook على نص يتكون من الوقت ثم نقطة ثم الجسم كما أُرسل، ويضع النتيجة في الترويسة بهذا الشكل:

X-Mujib-Signature: t=1767225600,v1=cde5fca83dacb883f5dbab42a1ba111d11c9b4778fdc91b404cc171beddd842e
الجزءالمعنى
tوقت الإرسال بالثواني منذ بداية 1970-01-01 بتوقيت UTC (وقت Unix)
v1البصمة: ناتج HMAC-SHA256 بصيغة hex وبأحرف صغيرة
المفتاحسر الـ Webhook كاملًا كما ظهر لك، ومعه البادئة whsec_، نصًّا كما هو بلا فك ترميز
النص الموقَّعقيمة t، ثم نقطة، ثم الجسم الخام كما وصل بايتًا بايتًا: <t>.<raw body>

كل محاولة إرسال توقَّع من جديد بوقتها، والجسم لا يتغير بين المحاولات.

كيف أتحقق من التوقيع خطوة بخطوة؟

رابط القسم «كيف أتحقق من التوقيع خطوة بخطوة؟»

تحقق على الجسم الخام قبل أن تحوّله إلى JSON، ثم قارن البصمتين مقارنة ثابتة الزمن.

  1. اقرأ الجسم الخام كما وصل، قبل أي معالجة.
  2. قسّم الترويسة عند الفاصلة، ثم كل جزء عند أول =، لتأخذ t وv1.
  3. ارفض الطلب إذا غاب أحدهما، أو لم يكن t عددًا صحيحًا، أو زاد الفرق بينه وبين ساعة خادمك على 300 ثانية في أي اتجاه.
  4. احسب HMAC-SHA256 بالسر على t ثم . ثم الجسم الخام.
  5. قارن الناتج بـ v1 بدالة مقارنة ثابتة الزمن، لا بعلامة المساواة العادية.
  6. بعد نجاح التحقق فقط: حوّل الجسم إلى JSON، وتجاهله إذا كنت عالجت قيمة X-Mujib-Delivery نفسها من قبل، ثم رد برمز من فئة 2xx.

كيف أتحقق من توقيع webhook في Node.js؟

رابط القسم «كيف أتحقق من توقيع webhook في Node.js؟»

هذه الدالة لا تحتاج إلى أي مكتبة، وتقبل الجسم Buffer أو نصًّا:

verify-mujib.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
/** true when `header` (X-Mujib-Signature) signs `rawBody` (a Buffer or string) with `secret`. */
export function verifyMujibSignature(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {
const parts = Object.fromEntries(
String(header ?? '').split(',').map((part) => {
const i = part.indexOf('=');
return [part.slice(0, i).trim(), part.slice(i + 1).trim()];
}),
);
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(nowSeconds - t) > TOLERANCE_SECONDS || !parts.v1) return false;
const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
const given = Buffer.from(parts.v1, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}

وهذا مستقبِل كامل بوحدة node:http المدمجة. يجمع الجسم خامًا، ويرد قبل أن يعالج الحدث، ويتجاهل الإرسال المكرر:

receiver.mjs
import { createServer } from 'node:http';
import { verifyMujibSignature } from './verify-mujib.mjs';
const secret = process.env.MUJIB_WEBHOOK_SECRET;
const handled = new Set(); // keep this in your database in production
createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const raw = Buffer.concat(chunks); // the body exactly as it arrived
if (!verifyMujibSignature(secret, req.headers['x-mujib-signature'], raw)) {
res.writeHead(401).end();
return;
}
res.writeHead(200).end('ok'); // answer first, then work
const deliveryId = req.headers['x-mujib-delivery'];
if (handled.has(deliveryId)) return; // a retry of a delivery already handled
handled.add(deliveryId);
const event = JSON.parse(raw.toString('utf8'));
console.log(event.type, event.id);
});
}).listen(3000);

إذا استخدمت إطار عمل فأعطِ الدالة الجسم الخام، لا الكائن الذي ينتجه محلّل JSON.

كيف أتحقق من التوقيع في Python؟

رابط القسم «كيف أتحقق من التوقيع في Python؟»

الدالة تستخدم المكتبة القياسية وحدها، وتأخذ الجسم bytes:

verify_mujib.py
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify_mujib_signature(secret, header, raw_body, now=None):
"""True when header (X-Mujib-Signature) signs raw_body (bytes) with secret."""
parts = {}
for part in (header or "").split(","):
key, sep, value = part.partition("=")
if sep:
parts[key.strip()] = value.strip()
try:
t = int(parts.get("t", ""))
except ValueError:
return False
v1 = parts.get("v1", "")
now = int(time.time()) if now is None else now
if abs(now - t) > TOLERANCE_SECONDS or not v1:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), v1.encode())

ومع FastAPI تعطيك await request.body() الجسم الخام:

app.py
import json
import os
from fastapi import FastAPI, Request, Response
from verify_mujib import verify_mujib_signature
app = FastAPI()
SECRET = os.environ["MUJIB_WEBHOOK_SECRET"]
@app.post("/hooks/mujib")
async def mujib_webhook(request: Request):
raw = await request.body() # the body exactly as it arrived
if not verify_mujib_signature(SECRET, request.headers.get("x-mujib-signature", ""), raw):
return Response(status_code=401)
event = json.loads(raw)
# Deduplicate on request.headers["x-mujib-delivery"], then queue the work.
print(event["type"], event["id"])
return Response(status_code=200)

كيف أتحقق من التوقيع في PHP؟

رابط القسم «كيف أتحقق من التوقيع في PHP؟»

hash_hmac وhash_equals من PHP نفسها، فلا تحتاج إلى أي مكتبة:

verify-mujib.php
<?php
const MUJIB_TOLERANCE_SECONDS = 300;
/** true when $header (X-Mujib-Signature) signs $rawBody with $secret. */
function verify_mujib_signature(string $secret, string $header, string $rawBody, ?int $now = null): bool
{
$parts = [];
foreach (explode(',', $header) as $part) {
$kv = explode('=', $part, 2);
if (count($kv) === 2) {
$parts[trim($kv[0])] = trim($kv[1]);
}
}
if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) {
return false;
}
$t = (int) $parts['t'];
if (abs(($now ?? time()) - $t) > MUJIB_TOLERANCE_SECONDS) {
return false;
}
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}

اقرأ الجسم من php://input، فمصفوفة $_POST تبقى فارغة مع JSON:

webhook.php
<?php
require __DIR__ . '/verify-mujib.php';
$raw = file_get_contents('php://input'); // the body exactly as it arrived
$header = $_SERVER['HTTP_X_MUJIB_SIGNATURE'] ?? '';
if (!verify_mujib_signature(getenv('MUJIB_WEBHOOK_SECRET') ?: '', $header, $raw)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// Deduplicate on $_SERVER['HTTP_X_MUJIB_DELIVERY'], then queue the work.
error_log($event['type'] . ' ' . $event['id']);
http_response_code(200);
echo 'ok';

كيف أختبر الكود قبل أن يصل حدث حقيقي؟

رابط القسم «كيف أختبر الكود قبل أن يصل حدث حقيقي؟»

جرّب دالتك على هذا المثال الثابت أولًا، ثم أرسل تجربة حقيقية من اللوحة. المثال موقَّع بمُوقِّع مُجيب نفسه بسر تجريبي، والجسم سطر واحد بلا سطر جديد في آخره:

check.mjs
import { verifyMujibSignature } from './verify-mujib.mjs';
const secret = 'whsec_example_secret_do_not_use';
const header = 't=1767225600,v1=cde5fca83dacb883f5dbab42a1ba111d11c9b4778fdc91b404cc171beddd842e';
const body = '{"id":"5d7c1e2a-3b4f-4c6d-9e8f-0a1b2c3d4e5f","type":"contact.created","createdAt":"2026-01-01T00:00:00.000Z","workspaceId":"7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d","data":{"contact":{"id":"c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8","name":"سارة","phone":"962790000000","channel":"whatsapp","stage":"new","createdAt":"2026-01-01T00:00:00.000Z"}}}';
console.log(verifyMujibSignature(secret, header, body, 1767225600)); // true
console.log(verifyMujibSignature(secret, header, body, 1767225901)); // false: more than 5 minutes later
check.py
from verify_mujib import verify_mujib_signature
secret = "whsec_example_secret_do_not_use"
header = "t=1767225600,v1=cde5fca83dacb883f5dbab42a1ba111d11c9b4778fdc91b404cc171beddd842e"
body = '{"id":"5d7c1e2a-3b4f-4c6d-9e8f-0a1b2c3d4e5f","type":"contact.created","createdAt":"2026-01-01T00:00:00.000Z","workspaceId":"7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d","data":{"contact":{"id":"c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8","name":"سارة","phone":"962790000000","channel":"whatsapp","stage":"new","createdAt":"2026-01-01T00:00:00.000Z"}}}'.encode()
print(verify_mujib_signature(secret, header, body, now=1767225600)) # True
print(verify_mujib_signature(secret, header, body, now=1767225901)) # False: more than 5 minutes later
check.php
<?php
require __DIR__ . '/verify-mujib.php';
$secret = 'whsec_example_secret_do_not_use';
$header = 't=1767225600,v1=cde5fca83dacb883f5dbab42a1ba111d11c9b4778fdc91b404cc171beddd842e';
$body = '{"id":"5d7c1e2a-3b4f-4c6d-9e8f-0a1b2c3d4e5f","type":"contact.created","createdAt":"2026-01-01T00:00:00.000Z","workspaceId":"7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d","data":{"contact":{"id":"c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8","name":"سارة","phone":"962790000000","channel":"whatsapp","stage":"new","createdAt":"2026-01-01T00:00:00.000Z"}}}';
var_dump(verify_mujib_signature($secret, $header, $body, 1767225600)); // bool(true)
var_dump(verify_mujib_signature($secret, $header, $body, 1767225901)); // bool(false): more than 5 minutes later

بعد ذلك افتح الـ Webhook في اللوحة واضغط «أرسل تجربة». يصل حدث webhook.test موقَّعًا بسرك الحقيقي، وتظهر نتيجته في «آخر الإرسالات».

لماذا يفشل التحقق مع أن السر صحيح؟

رابط القسم «لماذا يفشل التحقق مع أن السر صحيح؟»

السبب في الغالب أن البصمة حُسبت على نص غير الجسم الخام، أو بسر غير السر الحالي.

السببالحل
حوّلت الجسم إلى JSON ثم أعدت كتابته قبل التحققمكتبات JSON تغيّر المسافات، وبعضها يكتب الحروف العربية رموزًا مثل \u0633. تحقق على الجسم الخام
حذفت البادئة whsec_ من السرالمفتاح هو السر كاملًا كما ظهر لك
جدّدت السر ولم تحدّث نظامكالسر القديم يتوقف فورًا عند «جدّد السر»؛ ضع الجديد في نظامك
ساعة خادمك غير مضبوطةفعّل مزامنة الوقت التلقائية؛ الفرق المسموح 300 ثانية
تعيد إرسال طلب قديم محفوظ لديكالتوقيع القديم يُرفض بعد 5 دقائق، فاضغط «أرسل تجربة» لتجربة جديدة
أضافت أداة وسيطة مسافة أو سطرًا في آخر الجسمأي بايت زائد يغيّر البصمة؛ مرّر الجسم كما هو

كيف يحميني التوقيع من إعادة إرسال طلب قديم؟

رابط القسم «كيف يحميني التوقيع من إعادة إرسال طلب قديم؟»

الوقت t جزء من النص الموقَّع، فلا يستطيع من التقط طلبًا قديمًا أن يغيّر وقته من دون السر، والطلب الذي مضى على توقيعه أكثر من 5 دقائق ترفضه دالتك.

  • داخل هذه المهلة احفظ قيمة X-Mujib-Delivery وتجاهل كل طلب عالجت قيمته من قبل.
  • إعادة المحاولة من مُجيب توقَّع بوقت جديد، فتجتاز التحقق دائمًا، ثم يكشفها X-Mujib-Delivery نفسه.
  • لا تقبل طلبًا بلا توقيع حتى في بيئة الاختبار؛ حدث التجربة موقَّع مثل غيره.

الأسئلة الشائعة

هل مفتاح HMAC هو السر كله أم ما بعد whsec_؟

السر كله كما ظهر لك، ومعه البادئة whsec_. إذا حذفتها فلن تتطابق البصمة أبدًا.

هل يتغير التوقيع في كل إعادة محاولة؟

نعم. كل محاولة توقَّع من جديد بوقتها، أما الجسم والمعرّف X-Mujib-Delivery فيبقيان كما هما.

بماذا أرد إذا فشل التحقق؟

برمز خطأ مثل 401. يعدّه مُجيب فشلًا ويعيد المحاولة، وبعد 20 إرسالًا فاشلًا متتاليًا يتوقف الـ Webhook وحده.

ماذا أفعل إذا ضاع السر؟

لا يمكن عرضه مرة أخرى. افتح الـ Webhook واضغط «جدّد السر»، ثم ضع السر الجديد في نظامك فورًا.