المستوى المطلوب: متوسط
هذا الدليل يفترض إنك تعرف أساسيات 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);