تسجيل مفتاح المرور من جهة الخادم

نظرة عامة

في ما يلي نظرة عامة على الخطوات الرئيسية لتسجيل مفتاح المرور:

مسار تسجيل مفتاح المرور

  • حدِّد خيارات إنشاء مفتاح مرور. أرسِلها إلى العميل لتتمكّن من تمريرها إلى طلب إنشاء مفتاح المرور: طلب بيانات من واجهة برمجة التطبيقات WebAuthn‏ navigator.credentials.create على الويب، وcredentialManager.createCredential على Android. بعد أن يؤكّد المستخدم إنشاء مفتاح المرور، يتم حلّ طلب إنشاء مفتاح المرور وعرض بيانات الاعتماد PublicKeyCredential.
  • تحقَّق من بيانات الاعتماد وخزِّنها على الخادم.

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

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

الخطوة الأولى التي عليك اتّخاذها على الخادم هي إنشاء عنصر PublicKeyCredentialCreationOptions.

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

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

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

  • rpId: لتعبئة معرّف الطرف المعتمِد على الخادم، استخدِم الدوال أو المتغيّرات من جهة الخادم التي تمنحك اسم مضيف تطبيق الويب، مثل example.com.
  • ‫user.name وuser.displayName: لملء هذين الحقلَين، استخدِم معلومات جلسة المستخدم الذي سجّل الدخول (أو معلومات حساب المستخدم الجديد، إذا كان المستخدم ينشئ مفتاح مرور عند الاشتراك). يكون user.name عادةً عنوان بريد إلكتروني، وهو فريد للجهة المعتمِدة. ‫"user.displayName" هو اسم سهل الاستخدام. يُرجى العِلم أنّ بعض المنصات لن تستخدم displayName.
  • ‫user.id: سلسلة عشوائية فريدة يتم إنشاؤها عند إنشاء الحساب. ويجب أن يكون دائمًا، على عكس اسم المستخدم الذي قد يكون قابلاً للتعديل. يحدّد رقم تعريف المستخدم حسابًا، ولكن يجب ألا يحتوي على أي معلومات تكشف الهوية الشخصية. من المحتمل أن يكون لديك رقم تعريف مستخدم في نظامك، ولكن إذا لزم الأمر، يمكنك إنشاء رقم تعريف خاص بمفاتيح المرور لإبقائه خاليًا من أي معلومات تكشف الهوية الشخصية.
  • excludeCredentials: قائمة بمعرّفات بيانات الاعتماد الحالية لمنع تكرار مفتاح مرور من مقدّم مفاتيح المرور. لتعبئة هذا الحقل، ابحث في قاعدة بياناتك عن بيانات الاعتماد الحالية لهذا المستخدم. يمكنك مراجعة التفاصيل في مقالة منع إنشاء مفتاح مرور جديد إذا كان هناك مفتاح مرور حالي.
  • challenge: بالنسبة إلى تسجيل بيانات الاعتماد، لا يكون التحدي ذا صلة إلا إذا كنت تستخدم المصادقة، وهي تقنية أكثر تقدّمًا لإثبات هوية مزوّد مفتاح المرور والبيانات التي يصدرها. ومع ذلك، حتى إذا لم تستخدم المصادقة، يظل التحدي حقلاً مطلوبًا. تتوفّر تعليمات إنشاء تحدٍّ آمن للمصادقة في مقالة المصادقة باستخدام مفتاح المرور من جهة الخادم.

الترميز وفك الترميز

‫PublicKeyCredentialCreationOptions التي أرسلها الخادم
PublicKeyCredentialCreationOptions التي أرسلها الخادم. يتم ترميز الحقول الثنائية، مثل challenge وuser.id، من جهة الخادم إلى base64URL لكي يتم إرسال الخيارات بتنسيق JSON عبر بروتوكول HTTPS.

يتضمّن عنصر WebAuthn PublicKeyCredentialCreationOptions العادي حقولاً من نوع ArrayBuffer، ولا يمكن تحويلها مباشرةً إلى تسلسل باستخدام JSON.stringify(). لإرسال هذه الخيارات عبر HTTPS، يجب ترميز الحقول الثنائية كسلاسل base64URL:

  • على الخادم، تُنشئ مكتبة FIDO من جهة الخادم (مثل SimpleWebAuthn) تلقائيًا عنصر خيارات قابل للتسلسل بتنسيق JSON (مثل PublicKeyCredentialCreationOptionsJSON) مع حقول بترميز Base64URL.
  • على العميل، تتيح المتصفّحات الحديثة استخدام PublicKeyCredential.parseCreationOptionsFromJSON() لتحويل خيارات JSON مباشرةً إلى PublicKeyCredentialCreationOptions. عند استخدام مكتبات من جهة العميل، مثل @simplewebauthn/browser، تعالج دوال مثل startRegistration({ optionsJSON: options }) عملية التحويل هذه تلقائيًا.

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

نستخدم مكتبة SimpleWebAuthn في الأمثلة. في هذه الحالة، نترك إنشاء خيارات بيانات اعتماد المفتاح العام لدالة generateRegistrationOptions.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers';

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // 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 {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: cred.id,
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = await generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: isoUint8Array.fromUTF8String(user.id),
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        residentKey: 'required',
        userVerification: 'preferred',
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

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

تخزين المفتاح العام

‫PublicKeyCredentialCreationOptions التي أرسلها الخادم
يعرض navigator.credentials.create عنصر PublicKeyCredential.

عندما يتم حلّ navigator.credentials.create بنجاح على العميل، يعني ذلك أنّه تم إنشاء مفتاح مرور بنجاح. يتم عرض عنصر PublicKeyCredential.

يحتوي الكائن PublicKeyCredential على كائن AuthenticatorAttestationResponse، والذي يمثّل ردّ مزوّد مفتاح المرور على تعليمات العميل لإنشاء مفتاح مرور. وتحتوي على معلومات حول بيانات الاعتماد الجديدة التي تحتاجها كجهة مستندة إلى الهوية لإثبات هوية المستخدم لاحقًا. يمكنك الاطّلاع على مزيد من المعلومات حول "AuthenticatorAttestationResponse" في الملحق: AuthenticatorAttestationResponse.

أرسِل العنصر PublicKeyCredential إلى الخادم. بعد استلامه، أثبِت صحته.

سلِّم خطوة التحقّق هذه إلى مكتبة FIDO من جهة الخادم. وستوفّر عادةً دالة خدمية لهذا الغرض. على سبيل المثال، توفّر SimpleWebAuthn السمة verifyRegistrationResponse. يمكنك الاطّلاع على ما يحدث في الخلفية في الملحق: التحقّق من ردّ التسجيل.

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

استخدِم جدولاً مخصّصًا لبيانات اعتماد المفاتيح العامة المرتبطة بمفاتيح المرور. يمكن أن يكون لدى المستخدم كلمة مرور واحدة فقط، ولكن يمكن أن يكون لديه عدة مفاتيح مرور، على سبيل المثال، مفتاح مرور تتم مزامنته عبر Apple iCloud Keychain وآخر عبر "مدير كلمات المرور في Google".

في ما يلي مثال على مخطط يمكنك استخدامه لتخزين معلومات بيانات الاعتماد:

مخطّط قاعدة البيانات لمفاتيح المرور

  • جدول المستخدمون:
    • user_id: رقم تعريف المستخدم الأساسي معرّف عشوائي وفريد ودائم للمستخدم. استخدِم هذا العمود كمفتاح أساسي لجدول المستخدمون.
    • ‫username: اسم مستخدم يحدّده المستخدم، ويمكن تعديله.
    • ‫passkey_user_id: رقم تعريف المستخدم الخاص بمفتاح المرور والذي لا يتضمّن معلومات تكشف الهوية الشخصية، ويمثّله user.id في خيارات التسجيل. عندما يحاول المستخدم المصادقة لاحقًا، ستجعل أداة المصادقة هذاpasskey_user_id متاحًا في ردّ المصادقة في userHandle. ننصحك بعدم ضبط passkey_user_id كمفتاح أساسي. تميل المفاتيح الأساسية إلى أن تصبح معلومات تكشف الهوية الشخصية بحكم الأمر الواقع في الأنظمة، لأنّها تُستخدَم على نطاق واسع.
  • جدول بيانات اعتماد المفتاح العام:
    • id: معرّف بيانات الاعتماد استخدِم هذا المفتاح كمفتاح أساسي لجدول بيانات اعتماد المفتاح العام.
    • public_key: المفتاح العام لبيانات الاعتماد
    • ‫passkey_user_id: استخدِم هذا العمود كمفتاح خارجي لإنشاء رابط مع جدول المستخدمون.
    • backed_up: يتم الاحتفاظ بنسخة احتياطية من مفتاح المرور إذا تمت مزامنته من قِبل موفّر مفتاح المرور. يُعدّ تخزين حالة الاحتفاظ بنسخة احتياطية مفيدًا إذا أردت التفكير في إيقاف كلمات المرور في المستقبل للمستخدمين الذين لديهم مفاتيح مرور في backed_up. يمكنك التحقّق مما إذا تم الاحتفاظ بنسخة احتياطية من مفتاح المرور من خلال فحص علامة BE في authenticatorData، أو باستخدام ميزة مكتبة FIDO من جهة الخادم التي تتوفّر عادةً لتمنحك وصولاً سهلاً إلى هذه المعلومات. قد يكون تخزين أهلية الاحتفاظ بنسخة احتياطية مفيدًا للردّ على استفسارات المستخدمين المحتملة.
    • ‫name: يمثّل هذا الحقل اسمًا معروضًا لبيانات الاعتماد، وهو اختياري، ويتيح للمستخدمين منح بيانات الاعتماد أسماء مخصّصة.
    • ‫transports: مصفوفة من وسائل النقل. يُعدّ تخزين وسائل النقل مفيدًا لتجربة المستخدم في المصادقة. عندما تكون عمليات النقل متاحة، يمكن للمتصفّح أن يتصرّف وفقًا لذلك ويعرض واجهة مستخدم تتطابق مع عملية النقل التي يستخدمها موفّر مفتاح المرور للتواصل مع العملاء، لا سيما في حالات إعادة المصادقة التي لا تكون فيها السمة allowCredentials فارغة.

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

مثال على الرمز: تخزين بيانات الاعتماد

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

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


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // 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 {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

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

    const {
      aaguid,
      credential,
      credentialBackedUp
    } = registrationInfo;

    // Name the credential based on AAGUID
    const name =
      aaguid === undefined ||
      aaguid === '00000000-0000-0000-0000-000000000000' ?
        req.useragent?.platform : aaguids[aaguid]?.name;

    const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);

    // Existing, signed-in user
    const { user } = res.locals;

    // Save the credential
    await Credentials.update({
      id: credential.id,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      transports: credential.transports || response.response.transports,
      backed_up: credentialBackedUp,
      registered_at: new Date().getTime()
    });

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

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

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

الملحق: AuthenticatorAttestationResponse

يحتوي AuthenticatorAttestationResponse على عنصرَين مهمَّين:

  • ‫response.clientDataJSON هي نسخة JSON من بيانات العميل، وهي البيانات التي تظهر للمتصفّح على الويب. ويحتوي على مصدر الجهة الموثوق بها والطلب وandroidPackageName إذا كان العميل تطبيق Android. بصفتك جهة موثوق بها، تتيح لك قراءة clientDataJSON الوصول إلى المعلومات التي رآها المتصفّح في وقت طلب create.
  • response.attestationObjectيحتوي على معلومتَين:
    • ‫attestationStatement، وهي غير ذات صلة ما لم تستخدم المصادقة.
    • ‫authenticatorData هي البيانات كما يراها مزوّد مفتاح المرور. بصفتك جهة مستندة، تتيح لك قراءة authenticatorData الوصول إلى البيانات التي يراها مزوّد مفتاح المرور والتي يتم عرضها في وقت طلب create.

authenticatorDataيحتوي على معلومات أساسية حول بيانات اعتماد المفتاح العام المرتبطة بمفتاح المرور الذي تم إنشاؤه حديثًا:

  • بيانات اعتماد المفتاح العام نفسها ومعرّف فريد لها
  • معرّف الجهة المحظورة المرتبط ببيانات الاعتماد
  • علامات تصف حالة المستخدم عند إنشاء مفتاح المرور: ما إذا كان المستخدم حاضرًا فعليًا، وما إذا تم التحقّق من المستخدم بنجاح (راجِع نظرة متعمّقة على userVerification).
  • ‫AAGUID هو معرّف لموفّر مفتاح المرور، مثل "مدير كلمات المرور في Google". استنادًا إلى معرّف AAGUID، يمكنك تحديد موفّر مفتاح المرور وعرض الاسم في صفحة إدارة مفاتيح المرور. (يُرجى الاطّلاع على تحديد مقدّم مفتاح المرور باستخدام معرّف AAGUID)

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

الملحق: التحقّق من استجابة التسجيل

في الخلفية، يتألف التحقّق من استجابة التسجيل من عمليات التحقّق التالية:

  • تأكَّد من أنّ معرّف الجهة المعتمدة يتطابق مع موقعك الإلكتروني.
  • تأكَّد من أنّ مصدر الطلب هو مصدر متوقّع لموقعك الإلكتروني (عنوان URL للموقع الإلكتروني الرئيسي أو تطبيق Android).
  • إذا كنت تطلب التحقّق من المستخدم، تأكَّد من أنّ علامة التحقّق من المستخدم authenticatorData.uv هي true.
  • من المتوقّع عادةً أن تكون قيمة علامة حضور المستخدم authenticatorData.up هي true، ولكن إذا تم إنشاء بيانات الاعتماد بشكل مشروط، من المتوقّع أن تكون القيمة false.
  • تأكَّد من أنّ العميل تمكّن من تقديم التحدي الذي منحته إياه. إذا لم تستخدم المصادقة، لن يكون هذا الفحص مهمًا. ومع ذلك، يُعدّ تنفيذ هذا الإجراء من أفضل الممارسات، فهو يضمن أنّ الرمز البرمجي جاهز في حال قررت استخدام المصادقة في المستقبل.
  • تأكَّد من أنّ معرّف بيانات الاعتماد لم يتم تسجيله بعد لأي مستخدم.
  • تأكَّد من أنّ الخوارزمية التي يستخدمها موفّر مفتاح المرور لإنشاء بيانات الاعتماد هي خوارزمية أدرجتها (في كل حقل alg من publicKeyCredentialCreationOptions.pubKeyCredParams، والذي يتم تحديده عادةً ضمن مكتبة جهة الخادم ولا يظهر لك). يضمن ذلك أنّه لا يمكن للمستخدمين التسجيل إلا باستخدام الخوارزميات التي اخترت السماح بها.

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

التالي

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