مقدمة في 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 });
});
});اختبر فهمك