التحقق من تواقيع الـ Webhooks
تحقّق من ترويسة Teyra-Signature باستخدام HMAC-SHA256 لتثبت أن الإشعار صادر من تيرا باي ولم يُعدَّل.
يستطيع أي شخص إرسال طلب POST إلى رابط الـ Webhook الخاص بك. قبل أن تثق بأي حدث، تحقّق من ترويسة Teyra-Signature باستخدام سر التوقيع الخاص بنقطة النهاية (whsec_…).
ترويسة التوقيع
Teyra-Signature: t=1730124420,v1=6c0f2b8e3c…| الجزء | المعنى |
|---|---|
t | طابع زمني Unix (بالثواني) لحظة توقيع تيرا باي لهذه المحاولة. |
v1 | قيمة HMAC-SHA256 بترميز hex للبيانات الموقّعة، ومفتاحها سر نقطة النهاية. |
البيانات الموقّعة هي الطابع الزمني، ثم نقطة، ثم جسم الطلب الخام كما وصل تمامًا:
signed_payload = "<t>.<raw body>"
v1 = hex( HMAC_SHA256( key = endpoint secret, message = signed_payload ) )استخدم السر كاملًا مفتاحًا، بما فيه البادئة whsec_.
خطوات التحقق
- اقرأ الجسم الخام قبل أي تحليل لـ JSON، فإعادة تسلسل JSON لن تطابق البايتات الأصلية.
- قسّم الترويسة عند
,ثم كل جزء عند أول=. خذ قيمةtوكل قيمv1. - ارفض الطلب إذا ابتعد
tأكثر من 5 دقائق عن ساعة خادمك، فهذا يمنع إعادة إرسال الطلبات القديمة. - احسب قيمة
v1المتوقعة وقارنها بمقارنة ثابتة الزمن. - بعد ذلك فقط حلّل JSON وعالج الحدث.
الكود
import crypto from 'node:crypto';
import express from 'express';
const WEBHOOK_SECRET = process.env.TEYRA_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 300;
export function verifyTeyraSignature(rawBody, header, secret, now = Date.now()) {
if (!header) return false;
let timestamp = null;
const signatures = [];
for (const part of header.split(',')) {
const i = part.indexOf('=');
if (i === -1) continue;
const key = part.slice(0, i).trim();
const value = part.slice(i + 1).trim();
if (key === 't') timestamp = value;
else if (key === 'v1') signatures.push(value);
}
if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false;
if (Math.abs(Math.floor(now / 1000) - Number(timestamp)) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody) // Buffer with the exact bytes received
.digest();
return signatures.some((sig) => {
const given = Buffer.from(sig, 'hex');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
const app = express();
// express.raw keeps the body as a Buffer. Don't use express.json() on this route.
app.post('/hooks/teyra', express.raw({ type: 'application/json' }), async (req, res) => {
if (!verifyTeyraSignature(req.body, req.get('Teyra-Signature'), WEBHOOK_SECRET)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
if (await alreadyProcessed(event.id)) return res.sendStatus(200);
await queueEventForProcessing(event); // do slow work in the background
res.sendStatus(200);
});أخطاء شائعة
- التحليل قبل التحقق. الأُطر التي تحلّل JSON تلقائيًا (
express.json()أو$request->all()في Laravel أو Django REST Framework) تغيّر المسافات وترتيب المفاتيح. تحقّق دائمًا على البايتات الخام. - استخدام سر خاطئ. لكل نقطة نهاية سرها الخاص، ونقاط وضع الاختبار منفصلة عن نقاط الوضع الحي.
- المقارنة بـ
==. استخدمcrypto.timingSafeEqualأوhash_equalsأوhmac.compare_digest. - انحراف ساعة الخادم. أبقِ ساعة خادمك متزامنة (NTP)، وإلا رفض فحص المهلة (5 دقائق) أحداثًا صحيحة.
اختبر التحقق
استخدم إرسال حدث تجريبي في المطورون ← Webhooks لإرسال حدث payment.succeeded موقّع إلى نقطة النهاية، ثم راجع رمز الاستجابة في قائمة عمليات التسليم. راجع الاختبار من لوحة التحكم.