المستوى المطلوب: متوسط
هذا الدليل يفترض إنك تعرف أساسيات Node.js وExpress، وعندك فكرة عامة عن قواعد البيانات وإرسال البريد. لو لسه مبتدئ تمامًا، ابدأ بمقال "اعمل أول REST API" الأول ثم ارجع هنا.
تسجيل الدخول بدون كلمة مرور: ابنِ Magic Link آمن في Node.js
في نهاية المقال هيكون عندك مسار تسجيل دخول شغّال بدون باسورد: المستخدم يكتب إيميله، يوصله رابط، يدوس عليه، يبقى داخل. من غير كلمات مرور تُنسى ولا صفحات "نسيت كلمة السر".
المشكلة باختصار
كلمة المرور هي أضعف حلقة في أي تطبيق. المستخدم بيختار باسورد ضعيف، بيعيد نفس الباسورد على 10 مواقع، وبينساه فيولّدلك تذاكر دعم. وعلى مستوى الأمان، جزء كبير من الاختراقات بيبدأ من بيانات دخول مسروبة أو مُعاد استخدامها.
الطريقة الشائعة هي تخزين hash للباسورد بـ bcrypt والتعامل مع "نسيت كلمة السر" وقفل الحساب بعد محاولات كتير. ده شغل كتير عشان تحمي حاجة المستخدم نفسه بيكرهها. الـ Magic Link بيشيل الباسورد من المعادلة أصلاً: بدل ما تتحقق من "حاجة يعرفها المستخدم"، بتتحقق من "حاجة يملكها" — وهي صندوق بريده.
الفكرة ببساطة: مثال قبل الشرح العلمي
تخيّل فندق. بدل ما يديك مفتاح دائم للغرفة، موظف الاستقبال يبعتلك كارت مؤقت صالح لليلة واحدة بس. لو الكارت ضاع بكرة، مش مشكلة: انتهت صلاحيته. ولو حد لقاه بعد ما استعملته، مش هيفتح حاجة لأنه اتلغى بعد أول استخدام.
الـ Magic Link هو نفس الكارت المؤقت. رابط فيه رمز عشوائي يوصلك على بريدك، صالح لدقائق معدودة، ويُستعمل مرة واحدة. بعدها بيبقى ورقة ملغية.
علميًا: بنولّد token عشوائي عالي الإنتروبيا (256 بت)، ونخزّن hash بتاعه فقط في قاعدة البيانات مع وقت انتهاء (TTL). نبعت التوكن الخام في الرابط للبريد. لمّا المستخدم يدوس، نحسب hash للتوكن اللي جه، نقارنه باللي مخزّن، ونتأكد إنه لسه صالح — وبعدين نحذفه فورًا عشان الاستخدام الواحد. تخزين الـ hash مش الخام معناه إن حتى لو تسرّبت قاعدة بياناتك، التوكنات المخزّنة عديمة الفائدة.
نبنيه خطوة بخطوة
- ولّد token عشوائي بـ
crypto.randomBytes(32)— عشوائية حقيقية، مشMath.random. - خزّن الـ hash فقط (SHA-256) مع الإيميل ووقت الانتهاء. متخزّنش التوكن الخام أبدًا.
- ابعت الرابط بالبريد والتوكن الخام في الـ URL بس، وارجع نفس الرد دايمًا (عشان متكشفش مين مسجّل عندك).
- عند النقر: تحقّق ثم سجّل الدخول — طابق الـ hash، اتأكد إنه لسه صالح، احذفه، وأنشئ جلسة.
// npm i express nodemailer better-sqlite3
const express = require("express");
const crypto = require("crypto");
const Database = require("better-sqlite3");
const db = new Database("app.db");
db.exec(`CREATE TABLE IF NOT EXISTS magic_tokens (
token_hash TEXT PRIMARY KEY,
email TEXT NOT NULL,
expires_at INTEGER NOT NULL
)`);
const app = express();
app.use(express.json());
const TTL_MS = 15 * 60 * 1000; // صلاحية 15 دقيقة
const sha256 = (v) => crypto.createHash("sha256").update(v).digest("hex");
// (1) اطلب رابط الدخول
app.post("/auth/request", async (req, res) => {
const email = String(req.body.email || "").toLowerCase().trim();
if (!email) return res.status(400).json({ error: "email required" });
const rawToken = crypto.randomBytes(32).toString("base64url"); // 256 بت عشوائية
db.prepare(
"INSERT OR REPLACE INTO magic_tokens (token_hash, email, expires_at) VALUES (?,?,?)"
).run(sha256(rawToken), email, Date.now() + TTL_MS);
const link = `https://app.example.com/auth/verify?token=${rawToken}`;
await sendEmail(email, link); // ابعت الرابط فقط — لا ترجّع التوكن في الـ response
res.json({ ok: true }); // نفس الرد سواء الإيميل مسجّل أو لأ (ضد الـ enumeration)
});
// (2) تحقّق من الرابط وسجّل الدخول
app.get("/auth/verify", (req, res) => {
const raw = String(req.query.token || "");
const row = db.prepare("SELECT * FROM magic_tokens WHERE token_hash = ?").get(sha256(raw));
// استخدام واحد: احذف التوكن فورًا مهما كانت النتيجة
if (row) db.prepare("DELETE FROM magic_tokens WHERE token_hash = ?").run(row.token_hash);
if (!row || row.expires_at < Date.now()) {
return res.status(401).send("رابط غير صالح أو منتهي الصلاحية");
}
// هنا تنشئ الجلسة: cookie موقّع أو JWT للمستخدم row.email
res.send(`تم تسجيل الدخول: ${row.email}`);
});
app.listen(3000);
الكود ده أقل من 45 سطر ويغطّي الأساسيات الأمنية الأربعة: عشوائية قوية، تخزين hash، انتهاء صلاحية، واستخدام واحد. دالة sendEmail بتوصّلها بـ nodemailer أو أي مزوّد زي Resend أو Amazon SES.
سيناريو واقعي بالأرقام
لو عندك تطبيق SaaS بـ 20 ألف مستخدم، الافتراض إن حوالي 20% منهم بيعملوا "نسيت كلمة السر" على الأقل مرة كل ربع سنة — يعني ~4000 عملية استرجاع. كل واحدة ممكن تولّد تذكرة دعم أو تسرّب محاولة. مع Magic Link، مسار "نسيت الباسورد" بيختفي بالكامل لأنه مفيش باسورد أصلاً.
من ناحية الأداء: التحقق من hash لتوكن بيتم في أقل من مللي ثانية، مقابل عملية bcrypt.compare اللي متعمّدة تبقى بطيئة (50–100 مللي ثانية لكل محاولة). التكلفة الحقيقية اتنقلت لمكان تاني: زمن وصول الإيميل، وده جوهر المقايضة.
المقايضات الصريحة (trade-offs)
- بتكسب: صفر كلمات مرور تُدار، مسار استرجاع أبسط، وسطح هجوم أصغر (مفيش باسوردات تُسرَّب). بتخسر: الدخول بقى معتمد على سرعة وصول الإيميل — تأخير من ثانيتين لـ 30 ثانية أحيانًا.
- بتكسب: أمان أعلى ضد إعادة استخدام كلمات المرور. بتخسر: لو صندوق بريد المستخدم اتخترق، المهاجم يقدر يدخل — لكن لاحظ إن ده نفس خطر زر "نسيت كلمة السر" الموجود في أي نظام باسورد عادي.
- الافتراض هنا: إن إيصال البريد عندك موثوق. لو نسبة وصول الإيميلات ضعيفة، تجربة الدخول هتبوظ. اقفل الـ endpoint بـ rate limiting (مثلًا 5 طلبات/إيميل في الساعة) عشان متبقاش أداة إزعاج.
متى لا تستخدم Magic Link
مش حل لكل حالة. تجنّبه لو:
- تطبيقك يتطلب أمانًا عاليًا جدًا (بنوك، صحة) — هنا محتاج مصادقة متعددة العوامل، والـ Magic Link لوحده مش كفاية.
- مستخدمينك بيدخلوا عشرات المرات في اليوم — رحلة الإيميل كل مرة هتبقى مزعجة. استعمل جلسات طويلة العمر أو passkeys.
- البريد عند شريحة كبيرة من مستخدمينك غير موثوق أو مشترك بين أكتر من شخص.
التحقق من أنه يشتغل
- ابعت
POST /auth/requestبإيميلك، وتأكد إن الرابط وصل فعلًا. - افتح الرابط: المفروض يسجّل دخولك.
- افتح نفس الرابط تاني: لازم يرفض برسالة "غير صالح" — ده يثبت الاستخدام الواحد.
- استنى أكتر من 15 دقيقة على رابط جديد قبل ما تفتحه: لازم يرفض — ده يثبت انتهاء الصلاحية.
الخطوة التالية
انسخ الكود، وصّل دالة sendEmail بمزوّد حقيقي (ابدأ بـ nodemailer مع حساب SMTP تجريبي)، وضيف express-rate-limit على /auth/request. أول ما تتأكد إن الرابط بيتحرق بعد استخدام واحد وبعد 15 دقيقة، تبقى جاهز تربطه بنظام الجلسات عندك.
المصادر
- OWASP Authentication Cheat Sheet — مبادئ المصادقة الآمنة وتخزين التوكنات: cheatsheetseries.owasp.org
- Node.js Crypto — توليد عشوائية آمنة بـ
crypto.randomBytes: nodejs.org/api/crypto - NIST SP 800-63B — إرشادات المصادقة والمصادقات خارج النطاق (Out-of-Band): pages.nist.gov/800-63-3
- Verizon Data Breach Investigations Report — دور بيانات الدخول المسروقة في الاختراقات: verizon.com/business/resources/reports/dbir