Kodax

Protected Routes — Authentication & Security

حماية المسارات (Protected Routes)

ثلاث طبقات من الأسئلة

1) هل أنت مسجل؟          ← وسيط المصادقة       ← وإلا 401
2) هل دورك يسمح؟         ← فحص الأدوار          ← وإلا 403
3) هل هذا المورد لك؟     ← فحص الملكية          ← وإلا 403 (أو 404)

1. وسيط المصادقة

يستخرج الرمز ويتحقق منه ويحمّل المستخدم إلى req.user. المثال الكامل يضيف فحصاً مهماً: إذا غيّر المستخدم كلمة مروره بعد إصدار الرمز، يُرفض الرمز القديم (passwordChangedAt > iat) — فمن سرق رمزاً يفقده عندما يغيّر الضحية كلمة المرور.

2. التفويض بالأدوار

const restrictTo = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user?.role)) return res.status(403).json({ error: "ليس لديك صلاحية" });
  next();
};
// app.delete("/api/users/:id", protect, restrictTo("admin"), deleteUser);

الدور يأتي من الخادم (قاعدة البيانات أو رمز وقّعه الخادم)، لا من جسم الطلب ولا من ترويسة يرسلها العميل.

3. الملكية ومنع IDOR

أشهر ثغرة في واجهات API (المرتبة الأولى في OWASP API Top 10): IDOR — تغيير المعرّف في الرابط للوصول لبيانات شخص آخر:

GET /api/orders/1001   ← طلبي
GET /api/orders/1002   ← طلب شخص آخر! هل يمنعه الخادم؟

كون المستخدم مسجلاً لا يعني أنه يملك المورد. الحل: قيّد الاستعلام نفسه بالمالك:

// بدل: Order.findById(req.params.id)
// const order = await Order.findOne({ _id: req.params.id, user: req.user._id });
// if (!order) return res.status(404).json({ error: "غير موجود" });
const orders = [{ id: 1001, user: "u1" }, { id: 1002, user: "u2" }];
const findOwned = (id, userId) => orders.find((o) => o.id === id && o.user === userId);
console.log(findOwned(1001, "u1"), findOwned(1002, "u1"));   // الثاني undefined

إرجاع 404 بدل 403 للموارد التي لا يملكها يمنع حتى معرفة وجودها. والمعرّفات العشوائية (UUID) تصعّب التخمين لكنها ليست بديلاً عن فحص الملكية.

ترتيب الوسائط

app.patch("/api/posts/:id", protect, checkOwnership(Post), validate(schema), updatePost) — المصادقة أولاً دائماً، ثم الصلاحية، ثم التحقق من البيانات.

اختبر الحماية

لكل مسار محمي: طلب بلا رمز (401)، برمز منتهٍ (401)، بمستخدم عادي على مسار المشرف (403)، وبمستخدم آخر على مورد ليس له (403/404). هذه الاختبارات أهم من اختبارات "النجاح".

تمرين

أضف إلى المثال الكامل مساراً GET /api/orders/:id يعتمد على checkOwnership، واكتب أربعة طلبات تجريبية تغطي الحالات أعلاه.

// ===== Auth Middleware =====
const protect = async (req, res, next) => {
  try {
    // 1. Get token
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) {
      return res.status(401).json({ error: 'يجب تسجيل الدخول' });
    }
    
    // 2. Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // 3. Check if user still exists
    const user = await User.findById(decoded.userId);
    if (!user) {
      return res.status(401).json({ error: 'المستخدم غير موجود' });
    }
    
    // 4. Check if password changed after token issued
    if (user.passwordChangedAt > decoded.iat * 1000) {
      return res.status(401).json({ error: 'كلمة المرور تغيرت، أعد تسجيل الدخول' });
    }
    
    req.user = user;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Token غير صالح' });
  }
};

// ===== Role-Based Authorization =====
const restrictTo = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ 
        error: 'ليس لديك صلاحية لهذا الإجراء' 
      });
    }
    next();
  };
};

// ===== Ownership Check =====
const checkOwnership = (Model) => async (req, res, next) => {
  const doc = await Model.findById(req.params.id);
  
  if (!doc) {
    return res.status(404).json({ error: 'غير موجود' });
  }
  
  // Admin يمكنه الوصول لكل شيء
  if (req.user.role === 'admin') {
    req.resource = doc;
    return next();
  }
  
  // المالك فقط يمكنه الوصول
  if (doc.user.toString() !== req.user._id.toString()) {
    return res.status(403).json({ error: 'لا يمكنك الوصول لهذا المورد' });
  }
  
  req.resource = doc;
  next();
};

// ===== Usage Examples =====

// Public route
app.get('/api/posts', getAllPosts);

// Authenticated only
app.get('/api/profile', protect, getProfile);

// Admin only
app.delete('/api/users/:id', protect, restrictTo('admin'), deleteUser);

// Admin or Moderator
app.put('/api/posts/:id/approve', 
  protect, 
  restrictTo('admin', 'moderator'), 
  approvePost
);

// Owner only
app.put('/api/posts/:id', 
  protect, 
  checkOwnership(Post), 
  updatePost
);

app.delete('/api/posts/:id', 
  protect, 
  checkOwnership(Post), 
  deletePost
);

// ===== Combining Middlewares =====
const postRouter = express.Router();

postRouter.use(protect);  // كل routes تحتاج auth

postRouter
  .route('/')
  .get(getAllPosts)
  .post(createPost);

postRouter
  .route('/:id')
  .get(getPost)
  .put(checkOwnership(Post), updatePost)
  .delete(checkOwnership(Post), deletePost);
اختبر فهمك