المصادقة الثنائية من الميزات التي تبدو مكتملة قبل أن تكتمل فعلًا بوقت طويل. فتوليد رمز QR والتحقق من ستة أرقام يمثّل نحو ثلث العمل، أما الباقي — رموز الاسترداد، والجلسة نصف المُصادقة، وتحديد المحاولات، والتأكد من استحالة تخطي العامل الثاني — فهو ما يحدد إن كانت تحمي أحدًا فعلًا.
يغطي هذا الدليل تنفيذًا كاملًا في Laravel. الإصدارات وقت الكتابة: Laravel 12/13 وFortify 1.37 وgoogle2fa-laravel 3.0 وbacon-qr-code 3.1.
ممّ تحمي المصادقة الثنائية فعلًا
كن واضحًا بشأن نموذج التهديد، لأنه يحدد أي طريقة تستحق التنفيذ.
تحمي المصادقة الثنائية من بيانات الاعتماد المسروقة: كلمات مرور تسربت في اختراق، أو أُعيد استخدامها عبر مواقع، أو خُمّنت. وهذا هجوم شائع جدًا توقفه المصادقة الثنائية تمامًا.
لكن TOTP لا يحمي من صفحة تصيّد مقنعة. فإن خُدع المستخدم فأدخل كلمة مروره ورمزه المكوّن من ستة أرقام في موقع المهاجم، أمكن للمهاجم تمريرهما فورًا. ولا يقاوم ذلك إلا العوامل المرتبطة بالنطاق — مفاتيح المرور والمفاتيح العتادية — لأن المتصفح يرفض التوقيع لنطاق خاطئ.
إذًا فـ TOTP تحسّن كبير على كلمة المرور وحدها، ومفاتيح المرور خطوة أبعد. وكلاهما يستحق الوجود، ولا أحدهما «نهاية المطاف».
اختيار الأسلوب
| Fortify | TOTP مخصص | مفاتيح المرور | |
|---|---|---|---|
| جهد الإعداد | منخفض | متوسط | متوسط إلى مرتفع |
| التحكم بالتجربة | محدود بتدفقه | كامل | كامل |
| مقاوم للتصيّد | لا | لا | نعم |
| رموز الاسترداد | مدمجة | تبنيها بنفسك | تبنيها بنفسك |
| يعمل دون إنترنت | نعم | نعم | حسب الجهاز |
| الأنسب حين | تطبيق وتدفق اعتياديان | تحتاج تدفقًا خاصًا | الأمان هو المنتج |
إن كان تطبيقك اعتياديًا إلى حد معقول فاستخدم Fortify. واكتبها بنفسك حين تحتاج تحكمًا لا يمنحك إياه: شاشة تحقق مختلفة، أو نموذج جلسة مختلف، أو مصادقة ثنائية إلزامية لبعض الأدوار واختيارية لغيرها.
المسار السريع: Fortify
composer require laravel/fortify
php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
php artisan migrate
// config/fortify.php
'features' => [
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
]),
],
ضبط confirm على true أهم مما يبدو. فبدونه تُفعَّل المصادقة الثنائية لحظة طلب المستخدم لها، حتى لو لم ينجح في مسح رمز QR إطلاقًا، فيُحبس خارج حسابه. ومع التأكيد يجب أن يثبت المستخدم رمزًا صحيحًا واحدًا قبل أن يصبح العامل مطلوبًا.
بناؤها بنفسك
composer require pragmarx/google2fa-laravel bacon/bacon-qr-code
Schema::table('users', function (Blueprint $table) {
$table->text('two_factor_secret')->nullable();
$table->text('two_factor_recovery_codes')->nullable();
$table->timestamp('two_factor_confirmed_at')->nullable();
});
العمودان من نوع text لأنهما سيحملان قيمًا مشفّرة أطول بكثير من البيانات الخام. شفّرهما عبر cast كي لا يلمس النص الصريح قاعدة البيانات:
// app/Models/User.php
protected function casts(): array
{
return [
'two_factor_secret' => 'encrypted',
'two_factor_recovery_codes' => 'encrypted:array',
'two_factor_confirmed_at' => 'datetime',
];
}
public function hasTwoFactorEnabled(): bool
{
return ! is_null($this->two_factor_secret)
&& ! is_null($this->two_factor_confirmed_at);
}
لاحظ أن hasTwoFactorEnabled() يشترط وجود السر والتأكيد معًا. فالمستخدم في منتصف التسجيل ليس محميًا بعد، ويجب ألا يُطالَب بالتحقق كأنه كذلك.
التسجيل: السر ورمز QR
$google2fa = new Google2FA();
$secret = $google2fa->generateSecretKey();
// يُحفظ في الجلسة حتى التأكيد — فالسر غير المؤكَّد يجب ألا يُخزَّن،
// وإلا فقد يحبس تسجيلٌ ناقص المستخدمَ خارج حسابه.
$request->session()->put('2fa.pending_secret', $secret);
$uri = $google2fa->getQRCodeUrl(
config('app.name'),
$request->user()->email,
$secret
);
اعرض السر دائمًا كنص إلى جانب رمز QR، فبعض المستخدمين يستعملون تطبيق المصادقة على الجهاز نفسه ولا يمكنهم تصوير شاشتهم.
تأكيد التسجيل
// window: 1 يقبل الخطوتين المجاورتين من ثلاثين ثانية،
// وهو ما يغطي انحراف الساعة المعتاد بين هاتف المستخدم والخادم.
if (! $google2fa->verifyKey($secret, $request->code, 1)) {
throw ValidationException::withMessages([
'code' => __('الرمز غير صالح. تحقق من ساعة جهازك وأعد المحاولة.'),
]);
}
$recovery = collect(range(1, 8))->map(fn () => Str::random(10).'-'.Str::random(10));
$user->forceFill([
'two_factor_secret' => $secret,
'two_factor_recovery_codes' => $recovery->all(),
'two_factor_confirmed_at' => now(),
])->save();
اعرض رموز الاسترداد مرة واحدة فقط بعد التسجيل مباشرة، واجعلها قابلة للتنزيل. فالمستخدم الذي يفقد هاتفه دونها لا يملك طريق عودة لا يمر بتحققك اليدوي من هويته.
بوابة التحقق: ما تخطئ فيه معظم التنفيذات
بعد كلمة مرور صحيحة يصبح المستخدم نصف مُصادَق. يجب ألا يكون مسجَّل الدخول بعد، لكنك تحتاج تذكّر هويته ريثما يجلب رمزه.
والخطأ الشائع هو استدعاء Auth::login() ثم التحويل إلى شاشة التحقق. عندها يكون المستخدم مُصادقًا فعلًا، وأي شيء يتخطى التحويل — كتابة رابط مباشرة، أو نداء API، أو تبويب قديم — يتجاوز المصادقة الثنائية كليًا.
if ($user->hasTwoFactorEnabled()) {
// ليس تسجيل دخول، بل مجرد إشارة إلى تحقق معلّق.
$request->session()->put('2fa.challenge_user', $user->id);
$request->session()->put('2fa.challenge_at', now()->timestamp);
return redirect()->route('two-factor.challenge');
}
Auth::login($user, $request->boolean('remember'));
$request->session()->regenerate();
// أنهِ التحقق المهجور بدل تركه مفتوحًا إلى ما لا نهاية.
abort_if(! $userId || now()->timestamp - $startedAt > 300, 419);
$key = '2fa:'.$userId;
if (RateLimiter::tooManyAttempts($key, 5)) {
throw ValidationException::withMessages([
'code' => __('محاولات كثيرة. حاول بعد قليل.'),
]);
}
تحديد المحاولات ليس اختياريًا. فستة أرقام تعني مليون احتمال، وهو رقم يبدو كافيًا حتى تدرك أن نقطة نهاية بلا تقييد يمكن قصفها آلاف المرات في الثانية. وخمس محاولات كل خمس دقائق تُلغي ذلك عمليًا.
رموز الاسترداد، بشكل صحيح
$codes = collect($user->two_factor_recovery_codes ?? []);
$match = $codes->first(fn ($stored) => hash_equals($stored, $code));
if (! $match) {
return false;
}
// استخدام واحد فقط — احرقه فورًا.
$user->forceFill([
'two_factor_recovery_codes' => $codes->reject(fn ($c) => $c === $match)->values()->all(),
])->save();
تفصيلان مهمان هنا: استخدم hash_equals بدل === كي لا يسرّب زمن المقارنة معلومات، واحذف الرمز عند استخدامه، فرمز استرداد يظل صالحًا بعد استعماله ليس إلا كلمة مرور أضعف.
أخطاء تجعل المصادقة الثنائية قابلة للتجاوز
- تسجيل دخول المستخدم قبل التحقق. أشيع خلل على الإطلاق. فإن كانت الجلسة مُصادقة أصبح التحقق شكليًا.
- نسيان المداخل الأخرى. رموز API، وكوكي «تذكّرني»، وتسجيل الدخول الاجتماعي، والروابط السحرية، وانتحال الهوية — كلها يجب أن تحترم العامل الثاني وإلا صارت طريق الالتفاف حوله.
- غياب تحديد المحاولات. ستة أرقام تسقط سريعًا أمام تخمين غير مقيّد.
- تخزين السر كنص صريح. فتسريب قاعدة البيانات يسلّم العاملين معًا.
- عدم تجديد الجلسة بعد الدخول. يترك بابًا لتثبيت الجلسة.
- السماح لإعادة تعيين كلمة المرور بتعطيل المصادقة الثنائية. فمن يملك صندوق البريد يجب ألا يستطيع إزالة العامل الذي يحمي من ذلك تحديدًا.
اختبارها
public function test_a_user_with_2fa_is_not_logged_in_by_password_alone(): void
{
$user = User::factory()->withTwoFactor()->create();
$this->post('/login', [
'email' => $user->email,
'password' => 'password',
])->assertRedirect(route('two-factor.challenge'));
$this->assertGuest();
}
assertGuest() هو التوكيد المهم هنا، لأنه يثبت أن المستخدم غير مُصادق فعلًا أثناء التحقق — وهي بالضبط الخاصية التي تفتقر إليها معظم التنفيذات المكسورة.
إلى أين يتجه هذا: مفاتيح المرور
TOTP أساس متين لكنه قابل للتصيّد، بينما ترتبط مفاتيح المرور بنطاقك عبر المتصفح، فلا يستطيع موقع مشابه الحصول على توقيع صالح مهما كان مقنعًا.
وموقف عملي في 2026: قدّم مفاتيح المرور كعامل أساسي، وأبقِ TOTP بديلًا لمن لا تدعم أجهزتهم ذلك، وأبقِ رموز الاسترداد تحتهما. ولا تحذف TOTP، فدعم الأجهزة جيد لكنه غير شامل، وعامل لا يستطيع أحد استخدامه أسوأ من عامل أضعف يستطيعه الجميع.
أسئلة شائعة
هل أستخدم Laravel Fortify أم أبني المصادقة الثنائية بنفسي؟
استخدم Fortify ما لم تحتج تحكمًا لا يوفّره. فهو يقدّم التسجيل والتأكيد ورموز الاسترداد وتدفق التحقق، ويصونه فريق إطار العمل. وابنِها بنفسك حين تحتاج شاشة تحقق خاصة أو نموذج جلسة مختلفًا أو فرضًا حسب الدور.
كيف أولّد رمز QR للمصادقة الثنائية في Laravel؟
ولّد سرًا عبر google2fa، وابنِ رابط otpauth بـ getQRCodeUrl()، واعرضه عبر bacon-qr-code بصيغة SVG. واعرض السر كنص أيضًا لمن يستعمل تطبيق المصادقة على الجهاز نفسه.
أين ينبغي تخزين سر TOTP؟
في سجل المستخدم، مشفّرًا عبر cast من نوع encrypted. ولا تخزّنه كنص صريح أبدًا، فتسريب قاعدة البيانات سيكشف العاملين معًا. واستخدم عمود text لأن النص المشفّر أطول بكثير من السر.
كم رمز استرداد ينبغي إصداره؟
ثمانية خيار شائع. اعرضها مرة واحدة عند التسجيل، واجعلها قابلة للتنزيل، وألغِ كل رمز عند استخدامه، وأتح للمستخدم توليد مجموعة جديدة بعد تأكيد كلمة مروره.
لماذا يظهر رمز المصادقة غير صالح رغم صحته؟
غالبًا بسبب انحراف الساعة بين جهاز المستخدم والخادم. تحقّق بنافذة قدرها 1 لتقبل الخطوتين المجاورتين، وتأكد من مزامنة ساعة الخادم عبر NTP.
هل يمكن تجاوز المصادقة الثنائية؟
نعم، إذا نُفِّذت بشكل سيئ. والأسباب المعتادة هي تسجيل دخول المستخدم قبل التحقق، ونسيان مداخل أخرى مثل رموز API أو الروابط السحرية، وترك نقطة نهاية الرمز بلا تقييد. كما أن TOTP قابل للتصيّد لحظيًا، وهو ما تحلّه مفاتيح المرور.
هل أستخدم الرسائل النصية للمصادقة الثنائية؟
يُفضَّل تجنّبها. فالرسائل النصية عرضة لانتحال شريحة الاتصال والاعتراض، وتعتمد على شبكة الهاتف. وتطبيق TOTP أقوى ومجاني ويعمل دون إنترنت. أبقِ الرسائل النصية ملاذًا أخيرًا لمن لا يستطيع غيرها.
هل تحل مفاتيح المرور محل TOTP؟
في النهاية نعم، لكن ليس بعد. فمفاتيح المرور تقاوم التصيّد لأن المتصفح يربطها بنطاقك، وهي العامل الأساسي الأفضل. أبقِ TOTP بديلًا ما دام دعم الأجهزة والمتصفحات متفاوتًا، ورموز الاسترداد تحتهما.
التعليقات (0)
اترك تعليقًا
لا توجد تعليقات بعد. كن أول من يشارك أفكاره!