المستوى المطلوب: متوسط — محتاج معرفة أساسية بـ Node.js وسطر الأوامر، وقاعدة بيانات بسيطة تخزّن سرًّا واحدًا لكل مستخدم. مش محتاج أي خبرة تشفير مسبقة.
اعمل مصادقة ثنائية (2FA) بأكواد TOTP في Node.js من الصفر
في أقل من 20 دقيقة هتبني تحقق بخطوتين حقيقي بأكواد TOTP — نفس الأكواد اللي بيولّدها Google Authenticator وMicrosoft Authenticator — من غير أي خدمة خارجية ولا رسائل SMS. النتيجة: حتى لو اتسربت كلمة سر المستخدم، المهاجم مش هيدخل من غير الكود اللي على تليفون صاحب الحساب.
المشكلة باختصار
كلمة السر لوحدها بتفشل في هجوم واحد شائع: credential stuffing. المهاجم بياخد قوائم كلمات سر مسرّبة من مواقع تانية ويجرّبها عندك آليًا. لو عندك موقع بيتعرّض لـ 200 ألف محاولة دخول بكلمات سر صحيحة مسروقة، الباسورد لوحده مش هيوقفهم. الطبقة التانية (حاجة معاك، مش حاجة بتعرفها) هي اللي بتكسر الهجوم ده.
الرقم اللي بيهمك: مايكروسوفت نشرت إن تفعيل المصادقة متعددة العوامل بيوقف أكثر من 99.2% من هجمات اختراق الحسابات الآلية. ده مش تحسين تجميلي، ده فرق بين حساب مخترق وحساب آمن.
إزاي بيشتغل الكود المؤقت TOTP؟
تخيّل إنك وصاحبك اتفقتوا على رقم سري، وكمان الاتنين معاكم نفس الساعة بالظبط. كل نص دقيقة بتخلطوا الرقم السري مع الوقت الحالي وتطلّعوا رقم من 6 خانات. مفيش حد بيكلّم التاني، ومع ذلك الاتنين بيوصلوا لنفس الرقم في نفس اللحظة. لو جه غريب معهوش الرقم السري، مش هيقدر يخمّن الرقم اللي هيظهر.
ده بالظبط اللي بيحصل في TOTP، بس بدقة رياضية. الطرفان (السيرفر والتطبيق على التليفون) بيتشاركوا سرًّا واحدًا وقت التسجيل. عند كل تحقق، كل طرف بيحسب عدّاد زمني: counter = floor(unixTime / 30). بعدين بيمرّر السر والعدّاد على دالة HMAC-SHA1، وياخد 4 بايتات منها (خطوة اسمها dynamic truncation)، ويحوّلها لرقم من 6 خانات. الاتنين بيطلعوا نفس الكود من غير أي اتصال شبكي بينهم. ده المعرّف في RFC 6238، وهو مبني على HOTP في RFC 4226.
الخطوات العملية
- ولّد سرًّا عشوائيًا لكل مستخدم وخزّنه في قاعدة البيانات بترميز Base32 (ده اللي بتفهمه تطبيقات الـ authenticator).
- ابنِ رابط
otpauth://واعرضه كـ QR code علشان المستخدم يمسحه. - اطلب من المستخدم أول كود وتحقّق منه قبل ما تفعّل الميزة — ده بيثبت إن التليفون والسيرفر متزامنين.
- عند كل تسجيل دخول، بعد الباسورد، اطلب الكود وتحقّق منه بنافذة تسامح صغيرة للفروق الزمنية.
- ولّد أكواد احتياطية (backup codes) علشان المستخدم اللي يضيّع تليفونه.
الخطوة الأولى والتانية — توليد السر ورابط الـ QR:
const crypto = require('crypto');
function base32Encode(buf) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = '', out = '';
for (const b of buf) bits += b.toString(2).padStart(8, '0');
for (let i = 0; i + 5 <= bits.length; i += 5)
out += alphabet[parseInt(bits.slice(i, i + 5), 2)];
return out;
}
// سر 160-bit جديد لكل مستخدم، يُخزَّن في DB
const secret = base32Encode(crypto.randomBytes(20));
// رابط قياسي يقرأه أي تطبيق authenticator
const uri = `otpauth://totp/Haies:ali@mail.com`
+ `?secret=${secret}&issuer=Haies&period=30&digits=6`;
// اعرضه كصورة QR (npm i qrcode)
const QRCode = require('qrcode');
const qrDataUrl = await QRCode.toDataURL(uri); // <img src="...">الخطوة الرابعة — التحقق من الكود. ده قلب النظام، ومكتوب من الصفر بـ crypto بدون أي مكتبة:
function base32Decode(b32) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = '';
for (const c of b32.replace(/=+$/, '').toUpperCase()) {
const val = alphabet.indexOf(c);
if (val === -1) continue;
bits += val.toString(2).padStart(5, '0');
}
const bytes = [];
for (let i = 0; i + 8 <= bits.length; i += 8)
bytes.push(parseInt(bits.slice(i, i + 8), 2));
return Buffer.from(bytes);
}
// HOTP حسب RFC 4226
function hotp(secret, counter) {
const buf = Buffer.alloc(8);
buf.writeBigUInt64BE(BigInt(counter));
const hmac = crypto.createHmac('sha1', secret).update(buf).digest();
const offset = hmac[hmac.length - 1] & 0xf;
const bin = ((hmac[offset] & 0x7f) << 24)
| (hmac[offset + 1] << 16)
| (hmac[offset + 2] << 8)
| hmac[offset + 3];
return (bin % 1000000).toString().padStart(6, '0');
}
// TOTP verify بنافذة تسامح ±30 ثانية (RFC 6238)
function verifyTOTP(token, base32Secret, step = 30, window = 1) {
const secret = base32Decode(base32Secret);
const counter = Math.floor(Date.now() / 1000 / step);
for (let i = -window; i <= window; i++) {
if (hotp(secret, counter + i) === token) return counter + i;
}
return null; // كود غير صحيح
}لاحظ إن verifyTOTP بترجّع رقم العدّاد اللي نجح، مش مجرد true. ده مهم: خزّن آخر عدّاد اتقبل، وارفض أي كود بنفس العدّاد أو أقل. من غير الخطوة دي، مهاجم شايف الكود على كتف المستخدم يقدر يعيد استخدامه خلال نفس الـ 30 ثانية.
التحقق من أنه يعمل
شغّل السيرفر، امسح الـ QR بتطبيق Google Authenticator، وناد verifyTOTP('123456', secret) بالكود الظاهر على شاشتك. المفروض ترجع رقم العدّاد. غيّر رقم واحد في الكود — لازم ترجع null. لو الاتنين اشتغلوا صح، النظام سليم. لو الكود الصحيح بيترفض، غالبًا ساعة السيرفر مش مضبوطة؛ فعّل NTP وتأكد إن التوقيت متزامن.
المقايضات وما يجب الانتباه له
TOTP بيوقف تخمين الحسابات لكن مش سحر. الأرقام: 6 خانات تعني مليون احتمال. مع نافذة ±30 ثانية وحد أقصى 5 محاولات في الدقيقة (rate limit إجباري)، احتمال تخمين المهاجم للكود أقل من 0.001% خلال عمر الكود. المكسب ده مقابل تكلفتين واضحتين:
- سر مشترك على السيرفر: لو تسرّبت قاعدة بياناتك، الأسرار كلها تتسرّب. شفّر عمود السر في الـ DB (مثلاً بـ AES-256) واحتفظ بمفتاح التشفير خارج القاعدة.
- مش مقاوم للتصيّد: لو المستخدم اتخدع وكتب الكود في موقع مزيّف، المهاجم يقدر يعيد استخدامه فورًا. الافتراض هنا إن ده مقبول لموقع متوسط المخاطر؛ للحسابات عالية القيمة، Passkeys/WebAuthn بتحل ده لأنها مربوطة بالنطاق.
متى لا تستخدم هذه الطريقة
متستخدمش TOTP لو تقدر تستخدم Passkeys/WebAuthn — دي مقاومة للتصيّد ومفيهاش سر مشترك تخزّنه. وبلاش تمامًا 2FA عبر SMS كبديل؛ هجمات تبديل الشريحة (SIM swap) بتكسرها. وكمان لو تطبيقك داخلي منخفض المخاطر ومستخدميه قليلين وموثوقين، طبقة تانية ممكن تكون تعقيدًا زايدًا بلا عائد حقيقي.
الخطوة التالية
انسخ دالتَي hotp وverifyTOTP فوق في ملف totp.js، وولّد سرًّا واحدًا لنفسك، وامسحه بتطبيق authenticator على تليفونك. جرّب تتحقق من الكود الظاهر. أول ما ترجع الدالة رقم العدّاد بدل null، تكون فهمت النظام كله عمليًا — وبعدها اربطه بجدول المستخدمين عندك.
المصادر
- RFC 6238 — TOTP: Time-Based One-Time Password Algorithm: datatracker.ietf.org/doc/html/rfc6238
- RFC 4226 — HOTP: HMAC-Based One-Time Password Algorithm: datatracker.ietf.org/doc/html/rfc4226
- RFC 4648 — Base32 Encoding: datatracker.ietf.org/doc/html/rfc4648
- Node.js crypto (createHmac, randomBytes): nodejs.org/api/crypto.html
- Microsoft — إحصائية إيقاف MFA لأكثر من 99.2% من هجمات الحسابات: Microsoft Security Blog
- OWASP Authentication Cheat Sheet: cheatsheetseries.owasp.org