Kodax

مقدمة في Authentication — Authentication & Security

مقدمة في المصادقة (Authentication)

سؤالان مختلفان

| | المصادقة (Authentication) | التفويض (Authorization) |

|---|---|---|

| السؤال | من أنت؟ | ماذا يُسمح لك؟ |

| مثال | تسجيل الدخول بكلمة المرور | المستخدم يعدّل منشوراته فقط؛ المشرف يحذف أي منشور |

| الفشل | 401 Unauthorized | 403 Forbidden |

بعد تسجيل الدخول: كيف يتذكر الخادم المستخدم؟

HTTP بلا حالة: كل طلب مستقل. هناك طريقتان رئيسيتان:

1. الجلسات (Sessions)

الخادم ينشئ جلسة (في الذاكرة أو Redis أو قاعدة البيانات) ويعطي المتصفح معرّفها في ملف تعريف ارتباط. كل طلب يحمل المعرّف تلقائياً.

- ✅ إلغاء فوري (احذف الجلسة)، والمعرّف لا يحمل بيانات.

- ❌ يحتاج تخزيناً مشتركاً عند تعدد الخوادم.

2. الرموز (Tokens / JWT)

الخادم يوقّع رمزاً يحتوي هوية المستخدم ويعطيه للعميل، والعميل يرسله مع كل طلب. الخادم يتحقق من التوقيع دون الرجوع لقاعدة البيانات.

- ✅ مناسب للتطبيقات الجوالة والخدمات المتعددة.

- ❌ صعب الإلغاء قبل انتهاء صلاحيته — لذلك تكون قصيرة العمر مع Refresh Tokens (درس قادم).

لتطبيق ويب تقليدي على نطاق واحد، الجلسات غالباً أبسط وأأمن.

ملفات تعريف ارتباط آمنة

| الخاصية | الحماية |

|---|---|

| HttpOnly | JavaScript في الصفحة لا يستطيع قراءته ← حماية من سرقته عبر XSS |

| Secure | يُرسل عبر HTTPS فقط |

| SameSite=Lax أو Strict | لا يُرسل مع طلبات من مواقع أخرى ← حماية من CSRF |

| Max-Age | مدة صلاحية محددة |

أخطاء شائعة وما يمنعها

| المشكلة | الوقاية |

|---|---|

| تخمين كلمات المرور آلياً | تحديد معدل المحاولات (rate limiting) والقفل المؤقت |

| رسالة "البريد غير موجود" مقابل "كلمة المرور خاطئة" | رسالة موحدة: "بيانات الدخول غير صحيحة" — لا تكشف وجود الحساب |

| تثبيت الجلسة (Session Fixation) | معرّف جلسة جديد بعد الدخول (req.session.regenerate) |

| أسرار في الكود | متغيرات البيئة |

| كلمات مرور مخزنة كنص | تجزئة بطيئة بملح (الدرس التالي) |

| إرجاع بيانات حساسة | لا تُرجع hash كلمة المرور أو رموز الاستعادة في أي استجابة |

لا تبنِ كل شيء بنفسك

المصادقة سهلة الخطأ. للمشاريع الحقيقية فكّر في حلول مُختبرة: Supabase Auth، Firebase Auth، Auth.js، Clerk... وافهم ما تفعله (هذه الدورة) لتستخدمها بشكل صحيح.

تمرين

ارسم تسلسل طلبات تسجيل الدخول ثم طلب صفحة محمية بالجلسات، ثم بـ JWT: ماذا يرسل العميل في كل طلب؟ وأين تُخزَّن الحالة؟ وماذا يحدث عند تسجيل الخروج في كل حالة؟

// ===== Session-based Authentication =====
const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,   // سر طويل عشوائي من متغيرات البيئة — لا يُكتب في الكود
  resave: false,
  saveUninitialized: false,
  cookie: { 
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000  // 1 day
  }
}));

// Login
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  
  if (!user || !await user.comparePassword(password)) {
    return res.status(401).json({ error: 'بيانات خاطئة' });
  }
  
  // معرّف جلسة جديد بعد الدخول: يمنع Session Fixation
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: 'خطأ في الجلسة' });
    req.session.userId = user._id;
    req.session.role = user.role;
    res.json({ success: true, user: { name: user.name } });
  });
});

// Protected Route
const requireAuth = (req, res, next) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'يجب تسجيل الدخول' });
  }
  next();
};

app.get('/profile', requireAuth, async (req, res) => {
  const user = await User.findById(req.session.userId).select('-password');   // لا تُرجع hash كلمة المرور
  res.json(user);
});

// Logout
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ error: 'خطأ في تسجيل الخروج' });
    }
    res.clearCookie('connect.sid');
    res.json({ success: true });
  });
});
اختبر فهمك