إذا رأيتَ رسالة خطأ تبدأ بحروف eyJ، فأنت تتعامل مع JWT. هو الصيغة الأكثر انتشاراً لجلسات الويب وواجهات الـ API، وهو أيضاً أكثر الصيغ التي يساء فهمها. فك الترميز سهل جداً؛ المشكلة أن سهولة القراءة تجعل الناس يظنون أن قراءته تعني التحقق منه، وهذا بالتحديد هو الخطأ الذي ينتج عنه تطبيقات غير آمنة.
بنية التوكن: ثلاث نقاط فقط
أي JWT يتكوّن من ثلاثة أجزاء مفصولة بنقاط، وكل جزء بترميز base64url. لا يوجد جزء رابع ولا خامس:
HEADER . PAYLOAD . SIGNATURE
↓ ↓ ↓
الخوارزمية المطالبات التوقيع
(alg) (claims) (تحقق)الـ header يخبر الخادم بخوارزمية التوقيع المستخدمة، والـ payload يحمل البيانات المعروفة باسم المطالبات، والـ signature هو ما يحمي المحتوى من التعديل بعد إصداره.
| الجزء | ماذا يحتوي | ماذا لا يحتوي |
|---|---|---|
| Header | الخوارزمية ونوع التوكن | أي سرّ — أبداً |
| Payload | المطالبات: المعرّف والصلاحيات والانتهاء | كلمات مرور أو بيانات حسّاسة |
| Signature | دليل أن المحتوى لم يُعدَّل | التشفير — هو تحقّق لا سرّية |
أكبر سوء فهم: base64 ليس تشفيراً
ما تجريه في أداة أو في الطرفية هو ترميز عكسي، لا كسر لأي تشفير. لا حاجة إلى مفتاح، ولا يوجد ما يمنع أي شخص من فعل الشيء نفسه بتوكنك:
echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -dالفك في متصفحك دون إرسال التوكن
هذه هي الخطوة العملية في هذا الدليل. لاحظ أن الكود التالي لا يفعل أي طلب شبكة: التوكن لا يغادر جهازك ولا يُسجَّل في أي سجل خادم. هذا هو الفرق الجوهري بين أداة تعمل في المتصفح وأداة ترسل ما أدخلته إلى خادم.
function decodeJwt(token) {
const part = token.split(".")[1];
const base64 = part.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
return JSON.parse(atob(padded));
}
function inspectJwt(token) {
const claims = decodeJwt(token);
if (claims.exp && Date.now() / 1000 > claims.exp) {
return { valid: false, reason: "منتهي منذ " + new Date(claims.exp * 1000).toISOString(), claims };
}
return { valid: true, claims };
}الخطأ الأكثر شيوعاً: token has expired
مطالبات exp تُقاس بالثواني منذ 1970، لا بالمللي ثانية، وهذا مصدر الخلط الأول:
| القيمة | الوحدة | النتيجة |
|---|---|---|
| exp = 1700000000 | ثانية Unix | طبيعي |
| exp = 1700000000000 | مللي ثانية | خطأ — يبدو دائماً منتهياً |
| exp = "2026-01-01" | نص | غير صالح |
إذا رأيت خطأ انتهاء والـ payload يبدو صحيحاً، فقارن وحدة القياس أولاً. الثانية تُقارن بـ Date.now() مقسوماً على 1000، لا بـ Date.now() مباشرة:
const remaining = claims.exp - Date.now() / 1000;
console.log(remaining > 0 ? `صالح لـ ${Math.floor(remaining / 60)} دقيقة` : "منتهي");الفك لا يتحقق — التوقيع هو ما يتحقق
هذه هي النقطة التي يجب أن تخرج بها. الأداة التي تفك التوكن تعطيك محتوى الـ payload فقط، ولا تختبر صحته، لأن ذلك يتطلب المفتاح السري الموجود عند الخادم، وهو لا يصل إلى التطبيق الأمامي أبداً. الترتيب الصحيح في تطبيق حقيقي:
- الخادم يتحقق من التوقيع بالمفتاح السري عند إصدار التوكن وعند كل طلب.
- التطبيق الأمامي يكتفي بقراءة المطالبات لعرض الحالة، دون الوثوق بها.
- كل قرار صلاحيات حقيقي يُتخذ على الخادم، لا بناءً على ما في التوكن.
alg none: خلل يمرّ تحت الانتباه
كانت هذه ثغرة في مواصفة JWT الأولى: الخوارزمية none تعني حرفياً لا توقيع. مهاجم يغيّر الخوارزمية إلى none ويحذف التوقيع، فيصبح التوكن بلا حماية. الخطر أن بعض المكتبات قبلت ذلك افتراضياً:
{
"alg": "none",
"sub": "1",
"role": "admin"
}القاعدة العملية: افرض قائمة الخوارزميات المسموح بها صراحةً في إعدادات التحقق، ولا تثق أبداً بالخوارزمية المعلنة في الـ header، لأنها جزء من البيانات التي يملكها المهاجم.
متى تستخدم أداة فك JWT
- تشخيص طلبات 401: هل انتهت الصلاحية أم أن التوقيع مرفوض؟
- قراءة المطالبات لمعرفة الدور دون طلب إضافي للخادم.
- مقارنة ما يرسله الخادم مع ما يفترضه التطبيق.
- فحص محتوى توكن في مرحلة التطوير فقط، لا على بيانات إنتاج.