اعمل مصادقة ثنائية TOTP في Node.js بـ otplib خطوة بخطوة
مستوى المقال: متوسط (Intermediate).
محتاج تعرف أساسيات Node.js وإزاي بيشتغل تسجيل الدخول عندك. مش محتاج أي خلفية في التشفير؛ هنبني المفهوم من الصفر بمثال بسيط قبل ما نلمس الكود.
في آخر المقال هيكون عندك مصادقة ثنائية شغّالة فعلاً: المستخدم بيمسح رمز QR مرة واحدة، وبعدها كل تسجيل دخول بيطلب رقم من 6 خانات بيتغيّر كل 30 ثانية. الكود كله أقل من 40 سطر Node.js، وبصفر تكلفة رسائل SMS.
المشكلة باختصار
كلمة المرور لوحدها مش كفاية. لو تسرّبت قاعدة بيانات، أو المستخدم استعمل نفس الباسورد في موقع تاني اتخترق، الحساب بيبقى مكشوف. المصادقة الثنائية بتضيف عامل تاني: حاجة المستخدم بيملكها (موبايله)، مش بس حاجة بيعرفها (الباسورد).
الرقم اللي بيوضّح ليه ده مهم: مايكروسوفت نشرت إن تفعيل المصادقة متعددة العوامل بيوقف أكثر من 99.9% من محاولات اختراق الحسابات الآلية. ده مش رقم تسويقي، ده فرق بين حساب بيتسرق وحساب بيصمد.
إيه هو TOTP بمثال بسيط الأول
تخيّل إن عندك انت وصاحبك نفس كتاب الطبخ بالظبط، ومتفقين على قاعدة: كل نص ساعة تفتحوا الصفحة اللي رقمها يساوي الساعة الحالية، وتطبخوا الطبق اللي فيها. انتوا الاتنين في مكانين مختلفين، ومن غير ما تكلّموا بعض، هتطبخوا نفس الطبق في نفس اللحظة، لأن عندكم نفس الكتاب ونفس قاعدة الوقت. لو حد تالت سمع الطبق بتاع الساعة دي، مش هينفعه بعد نص ساعة لأن الطبق هيتغيّر.
دي بالظبط فكرة TOTP. السر المشترك (Shared Secret) هو كتاب الطبخ، والوقت هو الصفحة. الموبايل والسيرفر عندهم نفس السر، وكل واحد فيهم بيحسب نفس الرقم لوحده بناءً على الوقت الحالي، من غير ما يبعتوا السر لبعض في كل مرة.
علميًا: TOTP (Time-Based One-Time Password) معرّف في المواصفة RFC 6238، وهو امتداد لـ HOTP في RFC 4226. الحساب باختصار: بنحسب عدّاد زمني T = floor(unixtime / 30)، وبنعمل HMAC-SHA1(secret, T)، وناتج الـ 20 بايت بنعمل له "اقتطاع ديناميكي" (Dynamic Truncation) يطلّع رقم 31-bit، وبناخد منه mod 1,000,000 عشان نوصل لـ 6 خانات. الافتراض إن ساعة الموبايل وساعة السيرفر قريبين من بعض في حدود ثواني.
الأدوات والافتراضات
هنستخدم Node.js مع مكتبة otplib (تطبيق ناضج ومتوافق مع RFC 6238) ومكتبة qrcode لتوليد رمز الـ QR. التطبيق على جهة المستخدم بيكون Google Authenticator أو Authy أو 1Password — كلهم بيقروا نفس صيغة otpauth:// القياسية. الشرح مبني على فرضية إن عندك نظام تسجيل دخول شغّال أصلاً، وكل اللي ناقصك هو طبقة العامل التاني.
الخطوات التنفيذية
1) ثبّت المكتبات
# otplib لتوليد والتحقق من الرموز، qrcode لرسم رمز الـ QR
npm install otplib qrcodeالنتيجة المتوقعة: المكتبتين اتضافوا في node_modules بدون أخطاء.
2) ولّد سرًا فريدًا لكل مستخدم
السر ده بيتولّد مرة واحدة وقت ما المستخدم يفعّل المصادقة الثنائية، وبيتخزّن مرتبط بحسابه.
const { authenticator } = require('otplib');
// سر Base32 جديد لكل مستخدم — مثال: "JBSWY3DPEHPK3PXP"
const secret = authenticator.generateSecret();النتيجة المتوقعة: نص Base32 قصير. ده هو "كتاب الطبخ" اللي هيتشارك بين السيرفر والموبايل.
3) ابنِ رابط otpauth وحوّله لرمز QR
const qrcode = require('qrcode');
// الرابط القياسي اللي تطبيق المصادقة بيفهمه
const otpauth = authenticator.keyuri('ahmed@site.com', 'Haies LMS', secret);
// صورة QR كـ Data URL تعرضها في صفحة الإعداد
const qrDataUrl = await qrcode.toDataURL(otpauth);النتيجة المتوقعة: صورة QR المستخدم بيمسحها بتطبيق المصادقة، فيتخزّن السر عنده على الموبايل.
4) أكّد الإعداد قبل ما تفعّله
بدل ما تفعّل المصادقة على طول، اطلب من المستخدم يكتب أول رمز ظاهر عنده. لو صح، يبقى الإعداد تمام والسر اتسجّل عنده صح.
// userToken = الرقم اللي المستخدم كتبه من تطبيقه
const setupOk = authenticator.check(userToken, secret);
if (setupOk) {
// فعّل 2FA على الحساب دلوقتي بس
}5) تحقّق في كل تسجيل دخول مع نافذة وقت
ساعة الموبايل ممكن تكون متقدّمة أو متأخرة شوية. عشان كده بنسمح بنافذة تسامح صغيرة.
// window: 1 يقبل الخطوة السابقة والحالية والتالية (تسامح ~30 ثانية)
authenticator.options = { window: 1 };
const isValid = authenticator.check(userToken, secret);
// true لو الكود صح وضمن النافذة، false غير كدهالنتيجة المتوقعة: الدخول بيعدّي بس لو الرقم صح وفي وقته.
6) ولّد أكواد طوارئ (Recovery Codes)
لو المستخدم ضيّع موبايله، لازم يكون عنده طريقة يرجع بيها. ولّد له 10 أكواد طوارئ يستخدم كل واحد مرة واحدة.
const crypto = require('crypto');
const recoveryCodes = Array.from({ length: 10 }, () =>
crypto.randomBytes(5).toString('hex')
);
// مهم: خزّن "هاش" كل كود (bcrypt أو argon2) مش الكود نفسه7) خزّن السر مشفّرًا في قاعدة البيانات
السر ده لو اتسرّب بنص واضح، أي مهاجم هيقدر يولّد نفس الأرقام. شفّره وقت التخزين (مثلاً بـ AES-256-GCM بمفتاح من خدمة إدارة مفاتيح)، وفكّ التشفير وقت التحقق بس.
تأكّد إنه شغّال
السكربت ده بيحاكي الموبايل والسيرفر في نفس اللحظة، فتقدر تجرّبه محليًا قبل ما توصّله بنظامك.
const { authenticator } = require('otplib');
const secret = authenticator.generateSecret();
const token = authenticator.generate(secret); // نفس اللي بيطلعه الموبايل
console.log(token, authenticator.check(token, secret));
// مثال للمخرجات: 418263 trueلو طلعلك true، يبقى التوليد والتحقق بيشتغلوا مع بعض صح.
المقايضات اللي لازم تنتبه لها
- حجم النافذة (window). بتكسب تسامح أكبر لفرق الساعة، بتخسر أمان. مع
window=1بيبقى في 3 أكواد صالحة في أي لحظة، يعني احتمال التخمين العشوائي حوالي 3 من مليون لكل محاولة. لو رفعتها لـ 10، الاحتمال بيتضاعف 7 مرات تقريبًا. خليها 1 إلا لو عندك مشكلة فعلية في تزامن الساعات. - TOTP مقابل رسائل SMS. TOTP بيشتغل بدون نت وبدون تكلفة رسالة (الرسالة بتكلّف غالبًا بين سنت وخمس سنتات)، ومش معرّض لهجوم تبديل الشريحة (SIM swap). المقابل: المستخدم لازم يركّب تطبيق، ولو ضيّع موبايله من غير أكواد طوارئ بيتقفل بره.
- تحديد المحاولات إجباري. امنع أكثر من 5 محاولات خاطئة لكل كود. من غير كده، المهاجم يقدر يجرّب بكثافة على نافذة المليون احتمال.
متى لا تستخدم هذه الطريقة
TOTP مش مقاوم للتصيّد (Phishing). لو المهاجم عمل صفحة مزيّفة ونقل الكود لحظيًا للموقع الحقيقي، الكود بيعدّي. لو بتحمي حسابات عالية الخطورة وعايز مقاومة تصيّد حقيقية، روح لـ WebAuthn/Passkeys (FIDO2) بدل TOTP. وكمان لو جمهورك مش هيركّب تطبيق مصادقة أصلاً، ممكن يكون رابط الدخول السحري (Magic Link) أو OTP على الإيميل أنسب لمعدل إكمال أعلى، رغم إنه أضعف أمنيًا.
الخطوة التالية
افتح مسار تسجيل الدخول عندك، وضيف الخطوة 5 (التحقق مع window=1) بعد التأكد من الباسورد مباشرة، ورا rate limiter يسمح بـ 5 محاولات بس لكل كود. شغّل سكربت "تأكّد إنه شغّال" الأول محليًا؛ لو رجّعلك true، انت جاهز توصّله بنظامك الحقيقي.
المصادر
- RFC 6238 — TOTP: Time-Based One-Time Password Algorithm، IETF.
- RFC 4226 — HOTP: An HMAC-Based One-Time Password Algorithm، IETF.
- توثيق مكتبة otplib الرسمي (yeojz/otplib) على GitHub.
- OWASP Authentication Cheat Sheet و Multifactor Authentication Cheat Sheet.
- Microsoft Security: إحصائية فعالية المصادقة متعددة العوامل في إيقاف اختراق الحسابات الآلي.