تأمين موقعك باستخدام مصادقة ثنائية باستخدام مفتاح أمان (WebAuthn)

1. العناصر التي سيتم إنشاؤها

ستبدأ مع تطبيق ويب أساسي يدعم تسجيل الدخول المستندة إلى كلمة المرور.

وبعد ذلك، ستضيف دعم المصادقة الثنائية عبر مفتاح أمان استنادًا إلى WebAuthn. لإجراء ذلك، عليك تنفيذ ما يلي:

  • طريقة تتيح للمستخدم تسجيل بيانات اعتماد WebAuthn.
  • مسار المصادقة الثنائية حيث يُطلب من المستخدم العامل الثاني، وهو بيانات اعتماد WebAuthn، في حال تسجيله.
  • واجهة إدارة بيانات الاعتماد: قائمة ببيانات الاعتماد التي تتيح للمستخدمين إعادة تسمية بيانات الاعتماد وحذفها.

16ce77744061c5f7.png

ألقِ نظرة على تطبيق الويب المكتمل وجرِّبه.

2. لمحة عن WebAuthn

أساسيات WebAuthn

أسباب استخدام WebAuthn؟

يُعد التصيّد الاحتيالي مشكلة أمان كبيرة على الويب: تستفيد معظم عمليات اختراق الحساب من كلمات المرور الضعيفة أو المسروقة التي تتم إعادة استخدامها عبر المواقع الإلكترونية. كانت الاستجابة الجماعية لهذه المشكلة هي مصادقة متعددة العوامل، ولكن عمليات التنفيذ مجزأة ولا يزال العديد منها لا يعالج التصيّد الاحتيالي بشكلٍ كافٍ.

واجهة برمجة تطبيقات Web Authentication أو WebAuthn هي بروتوكول موحد لمقاومة التصيّد الاحتيالي يمكن لأي تطبيق ويب استخدامه.

آلية العمل

المصدر: webauthn.guide

يتيح WebAuthn للخوادم تسجيل المستخدمين ومصادقةهم باستخدام تشفير المفتاح العام بدلاً من كلمة مرور. يمكن للمواقع الإلكترونية إنشاء بيانات اعتماد تتألف من مفتاحَي تشفير عام والخاص.

  • يتم تخزين المفتاح الخاص بشكلٍ آمن على جهاز المستخدم.
  • يتم إرسال المفتاح العام ومعرّف بيانات الاعتماد التي تم إنشاؤها عشوائيًا إلى الخادم للتخزين.

يستخدم الخادم المفتاح العام لإثبات هوية المستخدم. ولا يكون ذلك سريًا، لأنه لا جدوى منه بدون المفتاح الخاص المقابل.

المزايا

يتضمن WebAuthn ميزتين رئيسيتين:

  • لا سر مشترك: لا يخزّن الخادم أي سر. وهذا يجعل قواعد البيانات أقل جذبًا للمخترقين، لأن المفاتيح العامة غير مفيدة لهم.
  • بيانات الاعتماد المحدّدة النطاق: لا يمكن استخدام بيانات اعتماد تم تسجيلها في site.example على evil-site.example. وهذا يجعل WebAuthn مضادًا للتصيّد الاحتيالي.

حالات الاستخدام

تتمثَّل إحدى حالات استخدام WebAuthn في المصادقة الثنائية باستخدام مفتاح أمان. وقد يكون هذا الأمر ملائمًا لتطبيقات الويب على وجه التحديد.

دعم المتصفح

إنه مكتوب بواسطة W3C وFIDO، بمشاركة Google وMozilla وMicrosoft وYubico وغيرها.

مسرد المصطلحات

  • Authenticator: يمثّل كيانًا برمجيًا أو جهازًا يمكنه تسجيل مستخدم وتأكيده لاحقًا امتلاك بيانات الاعتماد المسجّلة. هناك نوعان من المصادقة:
  • برنامج المصادقة في التجوال: برنامج مصادقة يمكن استخدامه مع أي جهاز يحاول المستخدم تسجيل الدخول منه. مثال: مفتاح أمان USB، هاتف ذكي.
  • مُصدِّق النظام الأساسي: برنامج مصادقة مضمّن في جهاز المستخدم. مثال: ميزة Touch ID من Apple
  • بيانات الاعتماد: مفتاحَا التشفير العام والخاص
  • المجموعة المعتمَدة: الموقع الإلكتروني الذي يحاول مصادقة المستخدم (خادم)
  • خادم FIDO: الخادم الذي يتم استخدامه للمصادقة. FIDO هي مجموعة من البروتوكولات التي طوّرها تحالف FIDO، أحد هذه البروتوكولات هو WebAuthn.

في ورشة العمل هذه، سنستخدم "أداة مصادقة التجوال".

3- قبل البدء

الأشياء التي تحتاج إليها

لإكمال هذا الدرس التطبيقي حول الترميز، ستحتاج إلى ما يلي:

  • فهم أساسي لـ WebAuthn.
  • معرفة أساسية بلغة JavaScript وHTML.
  • متصفح حديث يتوافق مع WebAuthn.
  • مفتاح أمان متوافق مع U2F.

يمكنك استخدام أحد الخيارات التالية كمفتاح أمان:

  • هاتف Android يعمل بنظام التشغيل Android>=7 (Nougat) والذي يشغِّل Chrome. في هذه الحالة، ستحتاج أيضًا إلى جهاز يعمل بنظام التشغيل Windows أو macOS أو نظام التشغيل Chrome مع بلوتوث مُفعّل.
  • مفتاح USB، مثل YubiKey.

6539dc7ffec2538c.png

المصدر: https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

ما ستتعرَّف عليه

ستتعلّم ⚡

  • كيفية تسجيل مفتاح أمان واستخدامه كعامل ثانٍ لمصادقة WebAuthn.
  • كيفية جعل هذه العملية سهلة للمستخدم.

لن يعود بإمكانك تعلّم ❌

  • كيفية إنشاء خادم FIDO: الخادم الذي يتم استخدامه للمصادقة. ولا بأس في ذلك، لأنك عادةً ما ستعتمد على عمليات تنفيذ خادم FIDO الحالي، كتطبيق ويب أو مطوّر مواقع. تأكد دائمًا من التحقق من وظائف وجودة عمليات تنفيذ الخادم التي تعتمد عليها. في هذا الدرس التطبيقي حول الترميز، يستخدم خادم FIDO SimpleWebAuthn. للحصول على خيارات أخرى، يُرجى الاطِّلاع على صفحة FIDO Alliance الرسمية. بالنسبة إلى المكتبات المفتوحة المصدر، راجِع webauthn.io أو AwesomeWebAuthn.

إخلاء المسؤولية

يجب على المستخدم إدخال كلمة مرور لتسجيل الدخول. ومع ذلك، لتبسيط الأمور في هذا الدرس التطبيقي، لا يتم تخزين كلمة المرور ولا وضع علامة عليها. في التطبيق الحقيقي، يمكنك التحقق من أنه صحيح من جانب الخادم.

يتم تنفيذ عمليات فحص الأمان الأساسية، مثل عمليات التحقق من إطار الشفافية والموافقة، والتحقق من صحة الجلسة، وتطهير الإدخالات في هذا الدرس التطبيقي حول الترميز. ومع ذلك، لا يتم اتخاذ العديد من إجراءات الأمان، على سبيل المثال، ليس هناك حد أقصى لكلمات المرور لمنع هجمات القوة العمياء. ولا يهم هنا لأنه لا يتم تخزين كلمات المرور، ولكن تأكد من عدم استخدام هذا الرمز كما هو في مرحلة الإنتاج.

4. إعداد تطبيق المصادقة

إذا كنت تستخدم هاتفًا يعمل بنظام التشغيل Android كمصادقة

  • تأكَّد من أنّ متصفِّح Chrome محدَّث على كلٍّ من الكمبيوتر المكتبي والهاتف.
  • على كل من الكمبيوتر المكتبي والهاتف، افتح Chrome وسجِّل الدخول باستخدام الملف الشخصي نفسه⏤الملف الشخصي الذي تريد استخدامه لورشة العمل هذه.
  • فعِّل المزامنة لهذا الملف الشخصي على جهاز كمبيوتر سطح المكتب وهاتفك. ويمكنك استخدام chrome://settings/syncSetup لإجراء ذلك.
  • فعِّل البلوتوث على كل من الكمبيوتر المكتبي والهاتف.
  • في Chrome Desktop الذي سجّلت الدخول إليه باستخدام الملف الشخصي نفسه، افتح webauthn.io.
  • أدخِل اسم مستخدم بسيطًا. اترك نوع المصادقة ونوع برنامج المصادقة مع القيمتين بدون وغير محدد (تلقائي). انقر على تسجيل.

6b49ff0298f5a0af.png

  • من المفترض أن يتم فتح نافذة متصفّح تطلب منك إثبات هويتك. اختَر هاتفك من القائمة.

ffebe58ac826eaf2.png 852de328fcd4eb42.png

  • من المفترض أن تتلقّى على هاتفك إشعارًا بعنوان إثبات هويتك. انقر عليها.
  • على هاتفك، سيُطلب منك رمز رقم التعريف الشخصي الخاص بهاتفك (أو المس مستشعر بصمات الإصبع). أدخِله.
  • على webauthn.io على جهاز سطح المكتب، من المفترض أن يظهر المؤشر "Success"

fc0acf00a4d412fa.png

  • على webauthn.io على جهاز سطح المكتب، انقر على زر "تسجيل الدخول".
  • مرة أخرى، من المفترض أن يتم فتح نافذة متصفّح، اختَر هاتفك في القائمة.
  • على هاتفك، انقر على الإشعار الذي يظهر على الشاشة وأدخِل رقم التعريف الشخصي (أو المس مستشعر بصمات الإصبع).
  • من المفترض أن يخبرك webauthn.io بأنك سجّلت الدخول. يعمل هاتفك بشكل صحيح كمفتاح أمان. الآن، تم الانتهاء من ورشة العمل.

في حال استخدام مفتاح أمان USB كبرنامج مصادقة

  • على جهاز كمبيوتر سطح المكتب في Chrome، افتح webauthn.io.
  • أدخِل اسم مستخدم بسيطًا. اترك نوع المصادقة ونوع برنامج المصادقة مع القيمتين بدون وغير محدد (تلقائي). انقر على تسجيل.
  • من المفترض أن يتم فتح نافذة متصفّح تطلب منك إثبات هويتك. اختَر مفتاح أمان USB في القائمة.

ffebe58ac826eaf2.png 9fe75f04e43da035.png

  • يُرجى إدخال مفتاح الأمان في الكمبيوتر المكتبي والنقر عليه.

923d5adb8aa8286c.png

  • على webauthn.io على جهاز سطح المكتب، من المفترض أن يظهر المؤشر "Success"

fc0acf00a4d412fa.png

  • على webauthn.io على جهاز سطح المكتب، انقر على زر تسجيل الدخول.
  • مرة أخرى، من المفترض أن يتم فتح نافذة متصفّح، اختَر مفتاح أمان USB في القائمة.
  • المس المفتاح.
  • من المفترض أن يخبرك Webauthn.io بأنك سجّلت الدخول. يعمل مفتاح أمان USB بشكل صحيح لتتمكّن من الوصول إلى ورشة العمل

7e1c0bb19c9f3043.png

5. الإعداد

في هذا الدرس التطبيقي حول الترميز، ستستخدِم Glitch، وهو محرّر رموز على الإنترنت ينشر الرمز تلقائيًا وعلى الفور.

نسخ رمز إجراء التفعيل

افتح مشروع إجراء التفعيل.

انقر على زر Remix.

يؤدي هذا إلى إنشاء نسخة من رمز إجراء التفعيل. لديك الآن رمز خاص بك لتعديله. إنّ شوكتك (المعروفة باسم "remix" in glitch) هي المكان الذي ستجري فيه كل العمل على هذا الدرس التطبيقي حول الترميز.

cf2b9f552c9809b6.png

استكشاف رمز إجراء التفعيل

استكشِف رمز إجراء التفعيل الذي أنشأته لفترة قصيرة.

لاحظ أنه ضمن libs، تم توفير مكتبة بالاسم auth.js من قبل. إنها مكتبة مخصّصة تراعي منطق المصادقة من جهة الخادم. تستخدم مكتبة fido كجهة اعتمادية.

6- تنفيذ تسجيل بيانات الاعتماد

تنفيذ تسجيل بيانات الاعتماد

أول ما نحتاج إليه لإعداد المصادقة الثنائية باستخدام مفتاح أمان هو السماح للمستخدم بإنشاء بيانات اعتماد.

لنبدأ أولاً بإضافة وظيفة من شأنها تنفيذ ذلك في الرمز من جهة العميل.

في public/auth.client.js، يُرجى ملاحظة أن هناك دالة باسم registerCredential()لا تفعل أي شيء حتى الآن. أضِف الرمز التالي إليها:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    "/auth/credential-options",
    "POST"
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
    }
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Send the encoded credential to the backend for storage
  return await _fetch("/auth/credential", "POST", encodedCredential);
}

تجدر الإشارة إلى أنه قد تم تصدير هذه الوظيفة لك من قبل.

إليك ما يفعله registerCredential:

  • تجلب هذه السياسة خيارات إنشاء بيانات الاعتماد من الخادم (/auth/credential-options).
  • ونظرًا لأن خيارات الخادم تعود مرة أخرى بالترميز، فإنها تستخدم وظيفة الأداة decodeServerOptions لفك تشفيرها.
  • تنشئ هذه الأداة بيانات اعتماد من خلال طلب بيانات من واجهة برمجة تطبيقات الويب navigator.credential.create. عند استدعاء navigator.credential.create، يستحوذ المتصفّح على خيار ويطلب من المستخدم اختيار مفتاح أمان.
  • يتم فك ترميز بيانات الاعتماد التي تم إنشاؤها حديثًا
  • وتُسجِّل هذه السياسة جانب بيانات الاعتماد الجديد من جانب الخادم من خلال تقديم طلب إلى /auth/credential يحتوي على بيانات الاعتماد المُشفَّرة.

جانبًا: إلقاء نظرة على رمز الخادم

يجري registerCredential() استدعاءين للخادم، لذا لنلقِ نظرة على ما يحدث في الخلفية.

خيارات إنشاء بيانات الاعتماد

عندما يقدِّم العميل طلبًا إلى (/auth/credential-options)، ينشئ الخادم عنصر خيارات ويرسله إلى العميل مرة أخرى.

يتم بعد ذلك استخدام هذا الكائن بواسطة البرنامج في طلب إنشاء بيانات الاعتماد الفعلي:

navigator.credentials.create({
    publicKey: {
    // Options generated server-side
    ...credentialCreationOptions
// ...
}

ما إذًا ما تم استخدامه في credentialCreationOptions على مستوى registerCredential للعملاء، والذي نفّذته في الخطوة السابقة؟

ألقِ نظرة على رمز الخادم ضمن router.post("/credential-options", ....

لنلقِ نظرة على كل موقع، ولكن إليك بعض المواقع المثيرة للاهتمام التي يمكنك الاطّلاع عليها في عنصر خيارات رمز الخادم، والتي تم إنشاؤها باستخدام مكتبة fido2 وعرضها في النهاية إلى البرنامج:

  • تصف rpName وrpId المؤسسة التي تسجِّل المستخدم وتصادق عليه. تذكّر أنه في WebAuthn، يتم تحديد بيانات الاعتماد لنطاق معيّن وهو ميزة أمنية، ويتم استخدام rpName وrpId هنا لتحديد نطاق بيانات الاعتماد. rpId الصالح هو على سبيل المثال اسم المضيف لموقعك. لاحِظ كيف سيتم تعديل هذه السياسات تلقائيًا عند بدء مشروع المشروع 🧘🏻 ♀️
  • excludeCredentials هو قائمة بيانات الاعتماد؛ حيث لا يمكن إنشاء بيانات الاعتماد الجديدة في برنامج مصادقة يحتوي أيضًا على أحد بيانات الاعتماد المدرجة في excludeCredentials. في الدرس التطبيقي حول الترميز، يتضمّن excludeCredentials قائمة ببيانات الاعتماد الحالية لهذا المستخدم. باستخدام هذه القاعدة وuser.id، نحرص على ضمان أنّ كل بيانات اعتماد ينشئها المستخدم ستكون في برنامج مصادقة مختلف (مفتاح الأمان). تُعد هذه ممارسة جيدة لأنها تعني أنه إذا سجّل مستخدم بيانات اعتماد متعددة، ستتم إضافتها إلى مُصادقات مختلفة (مفاتيح الأمان)، وبالتالي لن يؤدي فقدان مفتاح أمان واحد إلى قفل المستخدم من حسابه.
  • يحدِّد authenticatorSelection نوع برنامج المصادقة الذي تريد السماح به في تطبيق الويب. لنلقِ نظرة فاحصة على authenticatorSelection:
    • residentKey: preferred تعني أن هذا التطبيق لا يفرض بيانات الاعتماد القابلة للاكتشاف من جهة العميل. بيانات الاعتماد القابلة للاكتشاف من جهة العميل هي نوع خاص من بيانات الاعتماد يتيح مصادقة المستخدم بدون الحاجة إلى تحديدها أولاً. لقد أعددنا preferred هنا لأن هذا الدرس التطبيقي حول الترميز يركز على التنفيذ الأساسي، وبيانات الاعتماد القابلة للاكتشاف هي لعمليات أكثر تقدمًا.
    • لا يتوفّر requireResidentKey إلا للتوافق مع الأنظمة القديمة مع WebAuthn v1.
    • userVerification: preferred تعني أنه في حال كانت المصادقة تتوافق مع ميزة إثبات هوية المستخدم، على سبيل المثال، في حال كان مفتاح الأمان الحيوية أو مفتاحًا يتضمَّن ميزة رقم التعريف الشخصي مُضمَّنة، سيطلب الطرف المُعتمِد منها عند إنشاء بيانات الاعتماد. وإذا لم تُصدِر المصادقة مفتاح الأمان الأساسي، لن يطلب الخادم التحقُّق من المستخدم.
  • تصف ​​pubKeyCredParam، حسب ترتيب التفضيل، الخصائص المشفرة المطلوبة لبيانات الاعتماد.

وجميع هذه الخيارات هي قرارات ينبغي على تطبيق الويب اتخاذها لنموذج الأمان. لاحظ أنه على الخادم، يتم تحديد هذه الخيارات في عنصر authSettings واحد.

التحدي

وهناك قسم آخر أكثر إثارة للاهتمام هنا هو req.session.challenge = options.challenge;.

ولأن WebAuthn هو بروتوكول تشفير، يعتمد ذلك على تحديات عشوائية لتجنُّب إعادة تشغيل الهجمات، عندما يسرق مهاجم حمولة لإعادة تشغيل المصادقة، عندما لا يكون مالك المفتاح الخاص الذي من شأنه تفعيل المصادقة.

للحد من هذه المشاكل، يتم إنشاء اختبار تحقُّق على الخادم، ويتم توقيعه على الفور، وبعد ذلك ستتم مقارنة التوقيع بما هو متوقع. ويؤدي ذلك إلى التحقق من أن المستخدم يفصل المفتاح الخاص في وقت إنشاء بيانات الاعتماد.

رمز تسجيل بيانات الاعتماد

ألقِ نظرة على رمز الخادم ضمن router.post("/credential", ....

هذا هو المكان الذي يتم فيه تسجيل بيانات الاعتماد من جهة الخادم.

ماذا يحدث الآن؟

إنّ إحدى أهم الميزات في هذا الرمز هي مكالمة إثبات الملكية عبر fido2.verifyAttestationResponse:

  • يتم التحقق من اختبار التحقّق، ما يضمن إنشاء بيانات الاعتماد من قِبل شخص احتضن المفتاح الخاص في وقت الإنشاء.
  • يتم أيضًا التحقّق من رقم تعريف الطرف المعتمَد المرتبط بأصله. وهذا يضمن أن تكون بيانات الاعتماد مرتبطة بتطبيق الويب هذا (فقط تطبيق الويب هذا).

إضافة هذه الوظيفة إلى واجهة المستخدم

الآن وقد تم إعداد دالة إنشاء بيانات الاعتماد، يصبح ``registerCredential(), جاهزًا ويمكن السماح بها's لتصبح متاحة للمستخدم.

ستنفّذ ذلك من صفحة الحساب، لأن هذا هو المكان المعتاد لإدارة المصادقة.

في ترميز account.html&#39، أسفل اسم المستخدم، يوجد div فارغ جدًا مع فئة تصميم class="flex-h-between". سنستخدم div هذه لعناصر واجهة المستخدم المتعلّقة بوظيفة 2FA.

إضافة ino div هذا:

  • عنوان مكتوب عليه "المصادقة الثنائية"
  • زرّ لإنشاء بيانات اعتماد
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

أسفل div هذا، أضف div بيانات الاعتماد التي سنحتاج إليها لاحقًا:

<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>

في النص البرمجي المضمّن في account.html، يمكنك استيراد الدالة التي أنشأتها للتو وأضف دالة register التي تستدعيها، بالإضافة إلى معالج أحداث مرفق بالزر الذي أنشأته للتو.

// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);

// Register a credential
async function register() {
  let user = {};
  try {
    const user = await registerCredential();
  } catch (e) {
    // Alert the user that something went wrong
    if (Array.isArray(e)) {
      alert(
        // `msg` not `message`, this is the key's name as per the express validator API
        `Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
      );
    } else {
      alert(`Registration failed. ${e}`);
    }
  }
}

عرض بيانات الاعتماد للمستخدم لمشاهدتها

الآن وبعد أن أضفت الوظيفة لإنشاء بيانات الاعتماد، يحتاج المستخدمون إلى طريقة للاطّلاع على بيانات الاعتماد التي أضافوها.

وتُعدّ صفحة الحساب مكانًا جيدًا لإجراء ذلك.

في نظام التشغيل account.html، ابحث عن الدالة التي تحمل الاسم updateCredentialList().

أضِف إليه الرمز التالي الذي يجري استدعاء الخلفية لاسترجاع جميع بيانات الاعتماد المسجّلة للمستخدم الذي سجّل الدخول حاليًا، وتعرض بيانات الاعتماد التي تم إرجاعها:

// Update the list that displays credentials
async function updateCredentialList() {
  // Fetch the latest credential list from the backend
  const response = await _fetch('/auth/credentials', 'GET');
  const credentials = response.credentials || [];
  // Generate the credential list as HTML and pass remove/rename functions as args
  const credentialListHtml = getCredentialListHtml(
    credentials,
    removeEl,
    renameEl
  );
  // Display the list of credentials in the DOM
  const list = document.querySelector('#credentials');
  render(credentialListHtml, list);
}    

في الوقت الحالي، لا أمانع removeEl وrenameEl؛ سوف تتعرف عليهم لاحقًا في هذا الدرس التطبيقي حول الترميز.

أضِف مكالمة واحدة إلى updateCredentialList في بداية النص البرمجي المضمّن، في account.html. من خلال هذه المكالمة، يتم جلب بيانات الاعتماد المتاحة عندما يصل المستخدم إلى صفحة حسابه.

<script type="module">
    // ... (imports)
    // Initialize the credential list by updating it once on page load
    updateCredentialList();

يمكنك الآن استدعاء updateCredentialList عند اكتمال registerCredential بنجاح، بحيث تعرض القوائم بيانات الاعتماد التي تم إنشاؤها حديثًا:

async function register() {
  let user = {};
  try {
    // ...
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

جرّب ذلك الآن. 🏡🏻 😅

لقد انتهيت من تسجيل بيانات الاعتماد. يمكن للمستخدمين الآن إنشاء بيانات اعتماد مستندة إلى مفتاح الأمان، وتمثيلها مرئيًا في صفحة الحساب.

جرّب هذه الميزة:

  • تسجيل الخروج.
  • تسجيل الدخول - باستخدام أي مستخدم وكلمة مرور. كما ذكرنا سابقًا، إنّ كلمة المرور لا يتم التحقّق من صحتها بشكل صحيح، وذلك لتبسيط الأمور في هذا الدرس التطبيقي حول الترميز. أدخِل أي كلمة مرور غير فارغة.
  • بعد الانتقال إلى صفحة الحساب، انقر على إضافة بيانات اعتماد.
  • سيُطلب منك إدراج مفتاح أمان ولمسه. افعل ذلك.
  • عند إنشاء بيانات اعتماد ناجحة، يجب عرض بيانات الاعتماد في صفحة الحساب.
  • أعِد تحميل صفحة الحساب. وسيتمّ عرض بيانات الاعتماد.
  • في حال توفُّر مفتاحَين، جرِّب إضافة مفتاحَي أمان مختلفَين كبيانات اعتماد. ويجب عرض كليهما.
  • حاول إنشاء بيانات اعتماد باستخدام برنامج المصادقة نفسه (مفتاح)، ستلاحظ أنه لن يكون مدعومًا. هذا متعمّد، ويرجع ذلك إلى استخدامنا لـ excludeCredentials في الخلفية.

7- تفعيل مصادقة العامل الثاني

يمكن للمستخدمين تسجيل بيانات الاعتماد وإلغاء تسجيلها، ولكن يتم عرض بيانات الاعتماد ولم يتم استخدامها فعليًا بعد.

لقد حان الوقت الآن لاستخدامها، وإعداد المصادقة الثنائية الثنائية.

في هذا القسم، ستغير مسار المصادقة في تطبيق الويب من هذا المسار الأساسي:

6ff49a7e520836d0.png

في هذا المسار الثنائي:

e7409946cd88efc7.png

تنفيذ مصادقة العامل الثاني

لنبدأ أولاً بإضافة الوظائف التي نحتاج إليها وتنفيذ التواصل مع الخلفية، وسنضيف هذه البيانات في الواجهة الأمامية في الخطوة التالية.

ما تحتاج إلى تنفيذه هنا هو وظيفة تستخدم مصادقة بيانات اعتماد للمستخدم.

في public/auth.client.js، ابحث عن الدالة الفارغة authenticateTwoFactor، وأضِف إليها الرمز التالي:

async function authenticateTwoFactor() {
  // Fetch the 2F options from the backend
  const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
  // Decode them
  const decodedOptions = decodeServerOptions(optionsFromServer);
  // Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.get({
    publicKey: decodedOptions
  });
  // Encode the credential
  const encodedCredential = encodeCredential(credential);
  // Send it to the backend for verification
  return await _fetch("/auth/authenticate-two-factor", "POST", {
    credential: encodedCredential
  });
}

تجدر الإشارة إلى أنه قد تم تصدير هذه الوظيفة لك مسبقًا؛ وسنحتاج إليها في الخطوة التالية.

إليك ما يفعله authenticateTwoFactor:

  • يتطلب هذا البرنامج خيارَي مصادقة من الخادم. مثلما هي الحال في خيارات إنشاء بيانات الاعتماد التي رأيتها سابقًا، يتم تحديد هذه الخيارات على الخادم وتعتمد على نموذج الأمان لتطبيق الويب. ابحث في رمز الخادم ضمن router.post("/two-factors-options", ... للحصول على التفاصيل.
  • من خلال الاتصال بـ navigator.credentials.get، يتم السماح للمتصفّح بتولي المسؤولية ويطلب من المستخدم إدراج مفتاح مُسجّل مسبقًا ولمسه. ويؤدي ذلك إلى اختيار بيانات الاعتماد لعملية المصادقة الثنائية هذه المحددة.
  • وبعد ذلك، يتم تمرير بيانات الاعتماد المحدّدة في طلب خلفية لاسترجاع("/auth/auth-two-agent"`. وإذا كانت بيانات الاعتماد صالحة لهذا المستخدم، تتم مصادقة المستخدم.

جانبًا: إلقاء نظرة على رمز الخادم

يُرجى العلم بأنّ server.js تعتني ببعض عمليات التنقُّل والوصول، لأنّها تضمن عدم إمكانية الوصول إلى صفحة الحساب إلا للمستخدمين الذين تمت مصادقتهم، وتنفيذ بعض عمليات إعادة التوجيه اللازمة.

الآن، ألقِ نظرة على رمز الخادم ضمن router.post("/initialize-authentication", ....

وهناك نقطتان مهمتان يجب ملاحظتهما:

  • يتم التحقق من كل من كلمة المرور وبيانات الاعتماد في آن واحد في هذه المرحلة. هذا إجراء أمني: بالنسبة إلى المستخدمين الذين تم إعداد المصادقة الثنائية لهم، لا نريد أن تبدو مسارات واجهة المستخدم مختلفة استنادًا إلى ما إذا كانت كلمة المرور صحيحة أم لا. لذلك نتحقّق من كلٍّ من كلمة المرور وبيانات الاعتماد في آنٍ واحد.
  • في حال كانت كلمة المرور وبيانات الاعتماد صالحة، نكمل عملية المصادقة من خلال استدعاء completeAuthentication(req, res);. وهذا يعني أنه في الممارسة العملية، تم تبديل الجلسات ، من جلسة auth مؤقتة لم تتم مصادقة المستخدم فيها حتى الآن، إلى الجلسة الرئيسية main التي تمت مصادقة المستخدم فيها.

تضمين صفحة مصادقة العامل الثاني في تدفق المستخدم

في مجلد views، لاحظ الصفحة الجديدة second-factor.html.

ويحتوي هذا الزر على زر استخدام مفتاح الأمان، ولكنه لا يؤدي حاليًا إلى تنفيذ أي إجراء.

يمكنك إجراء هذا الزر لإجراء مكالمة مع authenticateTwoFactor() عند النقر.

  • في حال نجاح authenticateTwoFactor()، يمكنك إعادة توجيه المستخدم إلى صفحة الحساب.
  • في حال عدم نجاح ذلك، نبّه المستخدم بحدوث خطأ. في التطبيق الحقيقي، يمكنك تنفيذ رسائل خطأ أكثر فائدة، وبساطة في هذا العرض التوضيحي، لن نستخدم سوى تنبيه النافذة.
    <main>
...
    </main>
    <script type="module">
      import { authenticateTwoFactor, authStatuses } from "/auth.client.js";

      const button = document.querySelector("#authenticateButton");
      button.addEventListener("click", async e => {
        try {
          // Ask the user to authenticate with the second factor; this will trigger a browser prompt
          const response = await authenticateTwoFactor();
          const { authStatus } = response;
          if (authStatus === authStatuses.COMPLETE) {
            // The user is properly authenticated => Navigate to the Account page
            location.href = "/account";
          } else {
            throw new Error("Two-factor authentication failed");
          }
        } catch (e) {
          // Alert the user that something went wrong
          alert(`Two-factor authentication failed. ${e}`);
        }
      });
    </script>
  </body>
</html>

استخدام مصادقة العامل الثاني

أصبحت جاهزًا الآن لإضافة خطوة مصادقة ثانية.

ما عليك الآن هو إضافة هذه الخطوة من index.html، للمستخدمين الذين هيأوا المصادقة الثنائية.

322a5c49d865a0d8.png

في index.html، أسفل location.href = "/account";، أضِف الرمز الذي ينقل المستخدم بشروط إلى صفحة المصادقة الثانية إذا كان قد تم إعداد المصادقة الثنائية.

في هذا الدرس التطبيقي حول الترميز، يؤدي إنشاء بيانات اعتماد إلى تفعيل المستخدم للمصادقة الثنائية تلقائيًا.

لاحظ أن server.js ينفِّذ أيضًا التحقق من جلسة من جهة الخادم، مما يضمن إمكانية وصول المستخدمين الذين تمت مصادقتهم فقط إلى account.html.

const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
  // The user is properly authenticated => navigate to account
  location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
  // Navigate to the two-factor-auth page because two-factor-auth is set up for this user
  location.href = '/second-factor';
}

جرّب ذلك الآن. 🏡🏻 😅

  • سجّل الدخول باستخدام مستخدم جديد johndoe.
  • تسجيل الخروج.
  • سجّل الدخول إلى حسابك باسم johndoe، وتأكد من أنه لا يلزم سوى كلمة مرور.
  • إنشاء بيانات اعتماد ويعني ذلك أنك فعّلت المصادقة الثنائية باسم johndoe.
  • تسجيل الخروج.
  • أدخِل اسم المستخدم johndoe وكلمة المرور.
  • يمكنك الاطّلاع على كيفية انتقالك تلقائيًا إلى صفحة المصادقة الثنائية.
  • (جرّب الدخول إلى صفحة الحساب على /account، ولاحظ كيف ستتم إعادة توجيهك إلى صفحة الفهرس لأنك لم تتم مصادقتها بالكامل: أنت تفتقد عاملاً ثانيًا.)
  • عُد إلى صفحة مصادقة العامل الثاني، وانقر على استخدام مفتاح الأمان للمصادقة الثانية.
  • لقد سجّلت الدخول الآن ويُفترض أن تظهر لك صفحة حسابك.

8- تسهيل استخدام بيانات الاعتماد

لقد انتهيت من إنجاز الوظائف الأساسية للمصادقة الثنائية باستخدام مفتاح أمان 🚀

ولكن... هل لاحظت؟

في الوقت الحالي، ليست قائمة بيانات الاعتماد مناسبة إلى حد كبير، فمعرّف بيانات الاعتماد والمفتاح العام عبارة عن سلاسل طويلة غير مفيدة عند إدارة بيانات الاعتماد. البشر ليسوا جيدين جدًا بالسلاسل والأرقام الطويلة 🤖

لذا لنبدأ تحسين ذلك وإضافة وظائف لتسمية بيانات الاعتماد وإعادة تسميتها باستخدام سلاسل يمكن للمستخدمين قراءتها.

إلقاء نظرة على إعادة تسمية بيانات الاعتماد

لتوفير الوقت في تنفيذ هذه الوظيفة التي لا تنفّذ أي إجراء أساسي جدًا، تمت إضافة دالة لإعادة تسمية بيانات الاعتماد لك في رمز بدء التشغيل في auth.client.js:

async function renameCredential(credId, newName) {
  const params = new URLSearchParams({
    credId,
    name: newName
  });
  return _fetch(
    `/auth/credential?${params}`,
    "PUT"
  );
}

هذه استدعاء عادي لتغيير قاعدة البيانات: يرسل البرنامج طلب PUT إلى الخلفية، مع رقم تعريف بيانات اعتماد واسم جديد لبيانات الاعتماد هذه.

تنفيذ أسماء بيانات اعتماد مخصّصة

في account.html، لاحِظ الدالة الفارغة rename.

يُرجى إضافة الرمز التالي إليه:

// Rename a credential
async function rename(credentialId) {
  // Let the user input a new name
  const newName = window.prompt(`Name this credential:`);
  // Rename only if the user didn't cancel AND didn't enter an empty name
  if (newName && newName.trim()) {
    try {
      // Make the backend call to rename the credential (the name is sanitized) server-side
      await renameCredential(credentialId, newName);
    } catch (e) {
      // Alert the user that something went wrong
      if (Array.isArray(e)) {
        alert(
          // `msg` not `message`, this is the key's name as per the express validator API
          `Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
        );
      } else {
        alert(`Renaming failed. ${e}`);
      }
    }
    // Refresh the credential list to display the new name
    await updateCredentialList();
  }
}

قد يكون من المنطقي تسمية بيانات الاعتماد فقط بعد إنشائها بنجاح. لذا، لنبدأ إنشاء بيانات اعتماد بدون اسم. وعند إنشائها بنجاح، يجب إعادة تسمية بيانات الاعتماد. سيؤدي هذا إلى إجراء مكالمتَين في الخلفية.

استخدام الدالة rename في register()، للسماح للمستخدمين بتسمية بيانات الاعتماد عند التسجيل:

async function register() {
  let user = {};
  try {
    const user = await registerCredential();
    // Get the latest credential's ID (newly created credential)
    const allUserCredentials = user.credentials;
    const newCredential = allUserCredentials[allUserCredentials.length - 1];
    // Rename it
    await rename(newCredential.credId);
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

تجدر الإشارة إلى أنه سيتم التحقق من صحة إدخال المستخدم وتطهيره في الخلفية:

  check("name")
    .trim()
    .escape()

عرض بيانات الاعتماد

يمكنك التوجه إلى getCredentialHtml في templates.js.

لاحظ أن هناك رمزًا تم استخدامه مسبقًا لعرض اسم بيانات الاعتماد في أعلى بطاقة بيانات الاعتماد:

// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
 const { name, credId, publicKey } = credential;
 return html`
    <div class="credential-card">
      <div class="credential-name">
        ${name
          ? html`
              ${name}
            `
          : html`
              <span class="unnamed">(Unnamed)</span>
            `}
      </div>
     // ...
    </div>
  `;
};

جرّب ذلك الآن. 🏡🏻 😅

  • إنشاء بيانات اعتماد
  • سيُطلب منك تسميتها.
  • أدخِل اسمًا جديدًا وانقر على حسنًا.
  • تتم الآن إعادة تسمية بيانات الاعتماد.
  • كرّر ذلك وتحقّق من أن الأمور تعمل بشكل سلس أيضًا عند ترك حقل الاسم فارغًا.

تفعيل إعادة تسمية بيانات الاعتماد

قد يحتاج المستخدمون إلى إعادة تسمية بيانات الاعتماد، على سبيل المثال، إنهم يضيفون مفتاحًا ثانيًا ويريدون إعادة تسمية مفتاحهم الأول للتمييز بينها بشكل أفضل.

في account.html، ابحث عن الدالة الفارغة جدًا renameEl وأضِف إليها الرمز التالي:

// Rename a credential via HTML element
async function renameEl(el) {
  // Define the ID of the credential to update
  const credentialId = el.srcElement.dataset.credentialId;
  // Rename the credential
  await rename(credentialId);
  // Refresh the credential list to display the new name
  await updateCredentialList();
}

الآن، في getCredentialHtml في templates.js، ضمن قسم div، يمكنك إضافة الرمز التالي: يضيف هذا الرمز زر إعادة التسمية إلى نموذج بطاقة بيانات الاعتماد. وعند النقر عليه، سيستدعي هذا الزر دالة renameEl التي أنشأناها للتو:

const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
 <div class="flex-end">
  <button
    data-credential-id="${credId}"
    @click="${renameEl}"
    class="secondary right"
  >
   Rename
  </button>
 </div>
 // ...
  `;
};

جرّب ذلك الآن. 🏡🏻 😅

  • انقر على إعادة تسمية.
  • أدخِل اسمًا جديدًا عندما يُطلب منك ذلك.
  • انقر على حسنًا.
  • من المفترض أن تتم إعادة تسمية بيانات الاعتماد بنجاح، وسيتم تحديث القائمة تلقائيًا.
  • من المفترض أن تظل إعادة تحميل الصفحة تعرض الاسم الجديد (وهذا يدل على أن الاسم الجديد لا يزال تابعًا من جانب الخادم).

عرض تاريخ إنشاء بيانات الاعتماد

تاريخ الإنشاء غير متوفر في بيانات الاعتماد التي تم إنشاؤها عبر navigator.credential.create().

ولكن بما أنّ هذه المعلومات قد تكون مفيدة للمستخدم لتمييزها بين بيانات الاعتماد، أجرينا تعديلات على المكتبة الجانبية للخادم في رمز إجراء التفعيل وأضِفنا حقل creationDate يساوي Date.now() عند تخزين بيانات الاعتماد الجديدة.

في templates.js ضمن class="creation-date" div، أضِف ما يلي لعرض معلومات تاريخ الإنشاء للمستخدم:

<div class="creation-date">
  <label>Created:</label>
  <div class="info">
    ${new Date(creationDate).toLocaleDateString()}
    ${new Date(creationDate).toLocaleTimeString()}
  </div>
</div>

9- استخدام الرموز المتوافقة مع المستقبل

لقد طلبنا من المستخدم حتى الآن تسجيل برنامج مصادقة التجوال البسيط الذي يتم استخدامه بعد ذلك كعامل ثانٍ أثناء تسجيل الدخول.

أحد الأساليب الأكثر تقدمًا هي الاعتماد على نوع مصادقة أكثر فعالية، وهو مُصادق التجوال (UVRA). يمكن أن يوفّر UVRA عاملَي مصادقة ومقاومة تصيّد احتيالي خلال مسارات تسجيل الدخول بخطوتين.

والأفضل أنّك تدعم كلا الأسلوبين. ولإجراء ذلك، تحتاج إلى تخصيص تجربة المستخدم:

  • إذا كان لدى المستخدم فقط برنامج مصادقة بسيط (غير مُتحقق من هوية المستخدم) للتجوال، فاسمح له باستخدامه لإكمال عملية تقوية الحساب مقاومة للتصيُّد الاحتيالي، وسيكون عليه أيضًا كتابة اسم المستخدم وكلمة المرور. هذا ما يجريه درسنا التطبيقي حول الترميز.
  • وإذا كان هناك مستخدم آخر يتضمن أداة مصادقة أكثر تطورًا لإثبات هوية المستخدم أثناء التجوال، سيكون بإمكانه تخطي خطوة كلمة المرور، وربما حتى خطوة اسم المستخدم، أثناء تشغيل الحساب.

اطّلِع على مزيد من المعلومات عن ذلك في بدء تشغيل الحساب غير القابل للتصيّد الاحتيالي باستخدام تسجيل الدخول الاختياري بدون كلمة مرور.

في هذا الدرس التطبيقي حول الترميز، لن نخصّص تجربة المستخدم بشكل فعلي، ولكننا سنُعدّ قاعدة الترميز بحيث تحصل على البيانات التي تحتاجها لتخصيص تجربة المستخدم.

تحتاج إلى أمرين:

  • عليك إعداد residentKey: preferred في إعدادات الخلفية. سبق أن تمّ تنفيذ هذا الإجراء نيابةً عنك.
  • يمكنك إعداد طريقة لمعرفة ما إذا تم إنشاء بيانات الاعتماد القابلة للاكتشاف (والتي تُعرف أيضًا باسم مفتاح الإقامة).

لمعرفة ما إذا تم إنشاء بيانات اعتماد قابلة للاكتشاف أم لا:

  • يمكنك إجراء طلب بحث عن قيمة credProps عند إنشاء بيانات الاعتماد (credProps: true).
  • يمكنك إجراء طلب بحث عن قيمة transports عند إنشاء بيانات الاعتماد. وسيساعدك هذا في تحديد ما إذا كان النظام الأساسي الأساسي يتيح وظيفة UVRA، أي ما إذا كان هاتفًا جوّالاً على سبيل المثال.
  • تخزين قيمة credProps وtransports في الخلفية سبق إجراء ذلك في رمز إجراء التفعيل. ألق نظرة على auth.js إذا كنت مهتمًا.

لنتعرّف على القيمة credProps والسمة transports ونرسلها إلى الخلفية. في auth.client.js، عدّل registerCredential كما يلي:

  • إضافة حقل extensions عند الاتصال بـ navigator.credentials.create
  • يمكنك إعداد encodedCredential.transports وencodedCredential.credProps قبل إرسال بيانات الاعتماد إلى الخلفية للتخزين.

يجب أن يظهر registerCredential على النحو التالي:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    '/auth/credential-options',
    'POST'
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
      extensions: {
        credProps: true,
      },
    },
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Set transports and credProps for more advanced user flows
  encodedCredential.transports = credential.response.getTransports();
  encodedCredential.credProps =
    credential.getClientExtensionResults().credProps;
  // Send the encoded credential to the backend for storage
  return await _fetch('/auth/credential', 'POST', encodedCredential);
}

10- ضمان التوافق مع عدة متصفحات

التوافق مع المتصفحات غير Chromium

في دالة registerCredential في public/auth.client.js'، نحن ناستدعاء credential.response.getTransports() في بيانات الاعتماد التي تم إنشاؤها حديثًا لحفظ هذه المعلومات في الخلفية كتلميح للخادم.

ومع ذلك، لا يتم تنفيذ getTransports() حاليًا في جميع المتصفّحات (على عكس getClientExtensionResults التي تتوافق مع جميع المتصفّحات): سيؤدي طلب getTransports() إلى عرض خطأ في Firefox وSafari، ما يمنع إنشاء بيانات الاعتماد في هذه المتصفّحات.

لضمان تشغيل الرمز في جميع المتصفّحات الرئيسية، يمكن إنهاء مكالمة encodedCredential.transports في شرط:

if (credential.response.getTransports) {
  encodedCredential.transports = credential.response.getTransports();
}

لاحظ أنه على الخادم، تم تعيين transports على transports || []. في Firefox وSafari، لن تكون قائمة transports هي undefined ولكنها قائمة [] فارغة، ما يمنع الأخطاء.

تحذير المستخدمين الذين يستخدمون متصفِّحات لا تتوافق مع WebAuthn

1e9c1be837d66ce8.png

على الرغم من توافق WebAuthn في جميع المتصفحات الرئيسية، من المفيد عرض تحذير في المتصفحات التي لا تتوافق مع WebAuthn.

في index.html، لاحظ وجود div هذا:

<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>

في النص البرمجي المضمّن في index.html&#39، يمكنك إضافة الرمز التالي لعرض إعلان البانر في المتصفّحات التي لا تتوافق مع WebAuthn:

// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
  document.querySelector('#warningbanner').classList.remove('invisible');
}

في أحد تطبيقات الويب الحقيقية، يمكنك تنفيذ شيء أكثر تفصيلاً وآلية آلية احتياطية مناسبة لهذه المتصفحات، ولكن يوضح لك هذا كيفية التحقق من دعم WebAuthn.

11- أحسنت.

✨لقد اكتملت العملية.

لقد نفّذت المصادقة الثنائية باستخدام مفتاح أمان.

في هذا الدرس التطبيقي حول الترميز، تناولنا الأساسيات. إذا كنت تريد استكشاف WebAuthn for 2FA بدرجة أكبر، إليك بعض الأفكار لما يمكنك تجربته بعد ذلك:

  • إضافة معلومات &لمعرفة معلومات "آخر استخدام"&quot؛ إلى بطاقة بيانات الاعتماد. ويُعدّ ذلك معلومات مفيدة للمستخدمين لتحديد ما إذا كان يتم استخدام مفتاح أمان معيَّن بشكل نشط أم لا، خاصةً إذا كان قد تم تسجيل عدة مفاتيح.
  • نفِّذ إجراءات أكثر صرامة لتصحيح الأخطاء ورسائل خطأ أكثر دقة.
  • يُرجى الاطّلاع على auth.js واستكشاف ما يحدث عند تغيير جزء من authSettings، وخاصةً عند استخدام مفتاح يتيح إثبات ملكية المستخدم.