مصادقة مفتاح المرور من جهة الخادم

نظرة عامة

في ما يلي نظرة عامة على الخطوات الرئيسية التي تتضمّنها المصادقة باستخدام مفتاح المرور:

عملية المصادقة باستخدام مفتاح المرور

  • حدِّد التحدي والخيارات الأخرى اللازمة للمصادقة باستخدام مفتاح مرور. أرسِلها إلى العميل لتتمكّن من تمريرها إلى طلب مصادقة مفتاح المرور (navigator.credentials.get على الويب). بعد أن يؤكّد المستخدم المصادقة باستخدام مفتاح المرور، يتم حلّ طلب المصادقة باستخدام مفتاح المرور وعرض بيانات اعتماد (PublicKeyCredential) تحتوي على تأكيد المصادقة.
  • تحقَّق من تأكيد المصادقة.
  • إذا كان تأكيد المصادقة صالحًا، يمكنك مصادقة المستخدم.

تتناول الأقسام التالية تفاصيل كل خطوة.

إنشاء التحدّي

من الناحية العملية، يمثّل التحدي مصفوفة من وحدات البايت العشوائية، ويتم تمثيله كعنصر ArrayBuffer.

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

لضمان تحقيق التحدي للغرض منه، يجب عليك:

  1. تأكَّد من عدم استخدام التحدي نفسه أكثر من مرة. إنشاء اختبار جديد في كل محاولة تسجيل دخول تجاهَل التحدي بعد كل محاولة تسجيل دخول، سواء نجحت أو فشلت. ويمكنك أيضًا تجاهل التحدي بعد مدة معيّنة. لا تقبل أبدًا التحدي نفسه في الرد أكثر من مرة.
  2. التأكّد من أنّ التحدي آمن من الناحية التشفيرية: يجب أن يكون من المستحيل عمليًا تخمين التحدي. لإنشاء تحدٍّ آمن من الناحية التشفيرية من جهة الخادم، من الأفضل الاعتماد على مكتبة FIDO من جهة الخادم التي تثق بها. إذا أنشأت تحدياتك الخاصة بدلاً من ذلك، استخدِم وظيفة التشفير المضمّنة المتاحة في حزمة التكنولوجيا، أو ابحث عن المكتبات المصمّمة لحالات استخدام التشفير. ومن الأمثلة على ذلك iso-crypto في Node.js أو secrets في Python. وفقًا للمواصفات، يجب أن يتألف التحدي من 16 بايت على الأقل ليتم اعتباره آمنًا.

بعد إنشاء تحدٍّ، احفظه في جلسة المستخدم للتحقّق منه لاحقًا.

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

أنشئ خيارات طلب بيانات الاعتماد كعنصر publicKeyCredentialRequestOptions.

لإجراء ذلك، يمكنك الاعتماد على مكتبة FIDO من جهة الخادم. وستوفّر عادةً دالة خدمية يمكنها إنشاء هذه الخيارات لك. على سبيل المثال، توفّر SimpleWebAuthn الدالة generateAuthenticationOptions.

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

يمكن أن تكون بعض حقول publicKeyCredentialRequestOptions ثوابت. يجب تحديد العناصر الأخرى ديناميكيًا على الخادم:

  • ‫rpId: معرّف الطرف المعتمِد الذي تتوقّع أن تكون بيانات الاعتماد مرتبطة به، مثل example.com. لن تنجح المصادقة إلا إذا كان معرّف الطرف المعتمِد الذي تقدّمه هنا يتطابق مع معرّف الطرف المعتمِد المرتبط ببيانات الاعتماد. لتعبئة معرّف الجهة المعتمدة، استخدِم القيمة نفسها التي ضبطتها في publicKeyCredentialCreationOptions أثناء تسجيل بيانات الاعتماد.
  • challenge: يشير هذا إلى جزء من البيانات سيوقّعه مقدّم مفتاح المرور لإثبات أنّ المستخدم يملك مفتاح المرور في وقت طلب المصادقة. راجِع التفاصيل في مقالة إنشاء التحدّي.
  • ‫allowCredentials: مصفوفة من بيانات الاعتماد المقبولة لعملية المصادقة هذه. أدخِل مصفوفة فارغة للسماح للمستخدم باختيار مفتاح مرور متاح من قائمة يعرضها المتصفّح. راجِع جلب تحدٍّ من خادم الجهة المعتمدة ونظرة متعمّقة على بيانات الاعتماد القابلة للاكتشاف للحصول على التفاصيل.
  • userVerification: تشير إلى ما إذا كانت عملية التحقّق من المستخدم باستخدام قفل شاشة الجهاز "مطلوبة" أو "مفضّلة" أو "غير مستحسنة". راجِع جلب اختبار من خادم الجهة المعتمدة.
  • timeout: المدة (بالمللي ثانية) التي يمكن للمستخدم أن يستغرقها لإكمال المصادقة. ويجب أن تكون مدة معقولة وأقصر من مدة صلاحية challenge. القيمة التلقائية المقترَحة هي 5 دقائق، ولكن يمكنك زيادتها إلى 10 دقائق، وهي قيمة لا تزال ضمن النطاق المقترَح. تكون المهل الطويلة منطقية إذا كنت تتوقع أن يستخدم المستخدمون سير العمل المختلط، والذي يستغرق عادةً وقتًا أطول قليلاً. إذا انتهت مهلة العملية، سيتم عرض NotAllowedError.

بعد إنشاء publicKeyCredentialRequestOptions، أرسِله إلى العميل.

‫publicKeyCredentialCreationOptions التي أرسلها الخادم
الخيارات التي يرسلها الخادم. challenge يتم فك الترميز من جهة العميل.

مثال على الرمز البرمجي: خيارات إنشاء طلب بيانات الاعتماد

نستخدم مكتبة SimpleWebAuthn في الأمثلة. في هذه الحالة، نُسلِّم إنشاء خيارات طلب بيانات الاعتماد إلى دالة generateAuthenticationOptions.

import {
  generateAuthenticationOptions,
} from '@simplewebauthn/server';

router.post('/signinRequest', csrfCheck, async (req, res) => {

  // Ensure you nest calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Use the generateAuthenticationOptions function from SimpleWebAuthn
    const options = await generateAuthenticationOptions({
      rpID: process.env.HOSTNAME,
      allowCredentials: [],
    });
    // Save the challenge in the user session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).json({ error: e.message });
  }
});

التحقّق من المستخدم وتسجيل دخوله

عندما يتم حلّ navigator.credentials.get بنجاح على العميل، يتم عرض عنصر PublicKeyCredential.

عنصر PublicKeyCredential الذي أرسله الخادم
تعرض السمة navigator.credentials.get القيمة PublicKeyCredential.

‫response هو AuthenticatorAssertionResponse. وهو يمثّل ردّ مزوّد مفتاح المرور على تعليمات العميل لإنشاء ما يلزم لمحاولة المصادقة باستخدام مفتاح مرور على الجهة الموثوقة. ويشتمل على ما يلي:

  • response.authenticatorDataوresponse.clientDataJSON، كما هو الحال في خطوة تسجيل مفتاح المرور.
  • ‫response.signature الذي يحتوي على توقيع لهذه القيم.

أرسِل العنصر PublicKeyCredential إلى الخادم.

على الخادم، نفِّذ ما يلي:

مخطط قاعدة البيانات
مخطط قاعدة البيانات المقترَح يمكنك الاطّلاع على مزيد من المعلومات حول هذا التصميم في مقالة تسجيل مفتاح المرور من جهة الخادم.
  • اجمع المعلومات التي ستحتاج إليها للتحقّق من التأكيد ومصادقة المستخدم:
    • احصل على التحدي المتوقّع الذي خزّنته في الجلسة عند إنشاء خيارات المصادقة.
    • احصل على المصدر المتوقّع ورقم تعريف الطرف المعتمِد.
    • ابحث في قاعدة البيانات عن هوية المستخدم. في حال استخدام بيانات الاعتماد القابلة للاكتشاف، لن تعرف هوية المستخدم الذي يرسل طلب المصادقة. لمعرفة ذلك، يتوفّر لك خياران:
      • الخيار 1: استخدام response.userHandle في عنصر PublicKeyCredential في جدول المستخدمون، ابحث عن passkey_user_id الذي يتطابق مع userHandle.
      • الخيار 2: استخدام بيانات الاعتماد id المتوفّرة في العنصر PublicKeyCredential في جدول بيانات اعتماد المفتاح العام، ابحث عن بيانات الاعتماد id التي تتطابق مع بيانات الاعتماد id المتوفّرة في عنصر PublicKeyCredential. بعد ذلك، ابحث عن المستخدم المقابل باستخدام المفتاح الخارجي passkey_user_id في جدول المستخدمون.
    • ابحث في قاعدة بياناتك عن معلومات بيانات اعتماد المفتاح العام التي تتطابق مع تأكيد المصادقة الذي تلقّيته. لإجراء ذلك، ابحث في جدول بيانات اعتماد المفتاح العام عن بيانات الاعتماد id التي تتطابق مع بيانات الاعتماد idالموجودة في العنصر PublicKeyCredential.
  • التحقّق من تأكيد المصادقة سلِّم خطوة التحقّق هذه إلى مكتبة جهة خادم FIDO، والتي ستوفّر عادةً دالة خدمية لهذا الغرض. على سبيل المثال، توفّر SimpleWebAuthn السمة verifyAuthenticationResponse. يمكنك الاطّلاع على ما يحدث في الخلفية في الملحق: التحقّق من ردّ المصادقة.

  • احذف الاختبار سواء نجحت عملية التحقّق أم لا، وذلك لمنع هجمات إعادة الإرسال.

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

مثال على الرمز البرمجي: التحقّق من المستخدم وتسجيل دخوله

نستخدم مكتبة SimpleWebAuthn في الأمثلة. في هذه الحالة، نُسلّم عملية التحقّق من استجابة المصادقة إلى دالة verifyAuthenticationResponse.

import {
  verifyAuthenticationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/signinResponse', csrfCheck, async (req, res) => {
  const response = req.body;
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Find the credential stored to the database by the credential ID
    const cred = Credentials.findById(response.id);
    if (!cred) {
      throw new Error('Credential not found.');
    }
    // Find the user - Here alternatively we could look up the user directly
    // in the Users table via userHandle
    const user = Users.findByPasskeyUserId(cred.passkey_user_id);
    if (!user) {
      throw new Error('User not found.');
    }
    // Prepare credential information for verification
    const credential = {
      id: cred.id,
      publicKey: isoBase64URL.toBuffer(cred.publicKey),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      credential,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('User verification failed.');
    }

    // Kill the challenge for this session.
    delete req.session.challenge;

    req.session.username = user.username;
    req.session['signed-in'] = 'yes';

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).json({ error: e.message });
  }
});

الملحق: التحقّق من ردّ المصادقة

يتألف التحقّق من ردّ المصادقة من عمليات التحقّق التالية:

  • تأكَّد من أنّ معرّف الجهة المعتمدة يتطابق مع موقعك الإلكتروني.
  • تأكَّد من أنّ مصدر الطلب يتطابق مع مصدر تسجيل الدخول إلى موقعك الإلكتروني. بالنسبة إلى تطبيقات Android، راجِع التحقّق من المصدر.
  • تأكَّد من أنّ الجهاز تمكّن من تقديم التحدي الذي حدّدته له.
  • تأكَّد من أنّ المستخدم قد اتّبع المتطلبات التي تفرضها بصفتك جهة مستندة أثناء المصادقة. إذا كنت تطلب التحقّق من المستخدم، تأكَّد من أنّ علامة uv (تم التحقّق من المستخدم) في authenticatorData هي true. تأكَّد من أنّ علامة up (تواجد المستخدم) في authenticatorData هي true، لأنّ تواجد المستخدم مطلوب دائمًا لمفاتيح المرور.
  • التحقّق من التوقيع للتحقّق من التوقيع، تحتاج إلى ما يلي:
    • التوقيع، وهو التحدي الموقَّع: response.signature
    • المفتاح العام للتحقّق من التوقيع باستخدامه
    • البيانات الأصلية الموقَّعة هذه هي البيانات التي سيتم التحقّق من توقيعها.
    • خوارزمية التشفير التي تم استخدامها لإنشاء التوقيع.

لمزيد من المعلومات حول هذه الخطوات، يمكنك الاطّلاع على الرمز المصدر لـ verifyAuthenticationResponse في SimpleWebAuthn أو الاطّلاع على القائمة الكاملة لعمليات التحقّق في المواصفات.