Application Security

دوران وإلغاء صلاحية رموز JWT للأنظمة عديمة الحالة

يوفر تنفيذ رموز JSON Web Tokens (JWTs) في البنى عديمة الحالة فوائد كبيرة في قابلية التوسع، لكنه يُدخل ثغرة أمنية حرجة: سرقة الرموز. على عكس الأنظمة القائمة على الجلسات، حيث يمكن للخادم إلغاء الجلسة فوراً من جانب العميل، فإن رموز JWT ذاتية المحتوى. بمجرد إصدارها، تظل صالحة حتى تنتهي مدتها، حتى إذا قرر الخادم تسجيل خروج المستخدم. يستكشف هذا المنشور استراتيجيات قوية للتخفيف من هذا الخطر من خلال دوران الرموز والإلغاء الانتقائي.

معضلة الأنظمة عديمة الحالة

في بيئة عديمة الحالة التقليدية، لا يخزن الخادم حالة المستخدم. يعمل رمز JWT كمصدر للحقيقة، ويحتوي على جميع المطالبات الضرورية. ومع ذلك، إذا سرق المهاجم رمز وصول عبر XSS أو اعتراض الشبكة، فيمكنه استخدامه حتى وقت انتهاء صلاحيته القصير. بينما يساعد تحديد عمر رمز الوصول (على سبيل المثال، 15 دقيقة)، فإنه يخلق احتكاكاً في تجربة المستخدم. هنا تصبح نمط رمز الوصول/رمز التجديد ضرورياً.

من خلال إصدار رموز وصول قصيرة العمر ورموز تجديد طويلة العمر، نحد من نافذة الفرص المتاحة للمهاجمين. يتعامل رمز الوصول مع طلبات واجهة برمجة التطبيقات، بينما يُستخدم رمز التجديد حصراً للحصول على رموز وصول جديدة. والأهم من ذلك، يجب تخزين رمز التجديد بأمان، ويفضل ذلك في ملفات تعريف الارتباط HTTP-only، لمنع الوصول من خلال JavaScript.

تنفيذ دوران الرموز

دوران الرموز هو ممارسة إصدار رمز تجديد جديد في كل مرة يتم فيها استخدام رمز تجديد. تقلل هذه الاستراتيجية من قيمة رمز التجديد المسروق. إذا سرق المهاجم رمز تجديد، فيمكنه إجراء طلب واحد فقط قبل أن يؤدي محاولة التجديد التالية للمستخدم الشرعي إلى إلغاء صلاحية الرمز المسروق.

إليك مثال مبسط بلغة Node.js يوضح منطق الدوران:

async function handleRefreshToken(req, res) {
  const { refreshToken } = req.body;
  
  // 1. التحقق من توقيع الرمز وانتهاء صلاحيته
  const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
  
  // 2. التحقق مما إذا كان الرمز موجوداً في مخزننا (Redis)
  const storedToken = await redis.get(`refresh:${decoded.jti}`);
  if (!storedToken) {
    return res.status(401).send('Refresh token revoked or invalid');
  }
  
  // 3. إصدار رموز وصول وتجديد جديدة
  const newAccessToken = generateAccessToken(decoded.userId);
  const newRefreshToken = generateRefreshToken(decoded.userId);
  
  // 4. تخزين رمز التجديد الجديد وحذف القديم
  await redis.setEx(`refresh:${newJti}`, EXPIRY, newRefreshToken);
  await redis.del(`refresh:${decoded.jti}`);
  
  res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
}

استراتيجيات الإلغاء

على الرغم من الدوران، هناك سيناريوهات يكون فيها الإلغاء الفوري ضرورياً، مثل تغيير كلمة المرور أو الإبلاغ عن اختراق الحساب. بما أن رموز JWT عديمة الحالة، لا يمكننا ببساطة "حذفها". نحتاج إلى طريقة للتحقق من صلاحيته قبل منح الوصول.

1. رموز وصول قصيرة العمر

أكثر وسائل التخفيف فعالية هي الحفاظ على رموز الوصول قصيرة العمر جداً (5-15 دقيقة). يضمن ذلك أنه حتى إذا سُرقت الرمز، سيكون لدى المهاجم نافذة زمنية محدودة جداً للاستفادة منه. اجمع بين ذلك والدوران لجعل هذه النافذة غير موجودة عملياً.

2. قائمة الرموز المرفوضة (القائمة السوداء)

بالنسبة للسيناريوهات التي تتطلب تسجيل خروج فوري، يمكننا الحفاظ على قائمة مرفوضة من معرفات الرموز الملغاة (JTI). عند تسجيل خروج المستخدم، يتم إضافة JTI إلى مجموعة Redis مع وقت انتهاء صلاحية (TTL) يتطابق مع العمر المتبقي للرمز. أثناء كل طلب، يتحقق الخادم مما إذا كان JTI موجوداً في القائمة المرفوضة.

function isValidToken(jti, token) {
  // التحقق من القائمة المرفوضة أولاً
  if (redis.sIsMember('token_denylist', jti)) {
    return false;
  }
  return true;
}

يضيف هذا النهج تكلفة زمنية ضئيلة ولكنه يضمن الأمان. بالنسبة للأنظمة عالية الحركة، تأكد من تحسين عمليات القائمة المرفوعة باستخدام Redis، والتي تتعامل مع هذه الفحوصات في تعقيد زمني O(1).

الخاتمة

يتطلب تأمين البنى عديمة الحالة الانتقال إلى ما وراء إصدار رموز JWT البسيط. من خلال الجمع بين رموز الوصول قصيرة العمر، ودوران رموز التجديد العدواني، وآليات الإلغاء المستهدفة، يمكن للمطورين تقليل تأثير سرقة الرموز بشكل كبير. تذكر أن الأمان هو توازن بين قابلية الاستخدام والمخاطر. يضمن تنفيذ هذه الاستراتيجيات أن يظل تطبيقك مرناً ضد التهديدات الحديثة دون التضحية بفوائد قابلية التوسع التي توفرها رموز JWT.

Share: