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

نظرة عامة

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

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

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

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

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

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

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

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

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

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

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

PublicKeyCredentialCreationOptions التي يرسلها الخادم
PublicKeyCredentialCreationOptions التي أرسلها الخادم. يجب ترميز challenge وuser.id وexcludeCredentials.credentials من جهة الخادم إلى base64URL، لكي يمكن عرض PublicKeyCredentialCreationOptions عبر HTTPS.

تتضمّن PublicKeyCredentialCreationOptions حقولاً هي ArrayBuffer، لذا لا تتوافق مع JSON.stringify(). هذا يعني أنّه في الوقت الحالي، من أجل عرض PublicKeyCredentialCreationOptions عبر HTTPS، يجب ترميز بعض الحقول يدويًا على الخادم باستخدام base64URL ثم فك ترميزها على العميل.

  • على الخادم، تتولّى عادةً مكتبة FIDO من جهة الخادم عملية الترميز وفك الترميز.
  • على الجهاز العميل، يجب إجراء الترميز وفك الترميز يدويًا في الوقت الحالي. سيكون ذلك أسهل في المستقبل، إذ ستتوفّر طريقة لتحويل الخيارات بتنسيق JSON إلى PublicKeyCredentialCreationOptions. اطّلِع على حالة التنفيذ في Chrome.

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

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

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } 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: isoBase64URL.toBuffer(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: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // 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. يمكنك الاطّلاع على تفاصيل حول ما يحدث في الخلفية في الملحق: التحقّق من صحة رد التسجيل.

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

استخدِم جدولاً مخصّصًا لبيانات اعتماد المفتاح العام المرتبطة بمفاتيح المرور. يمكن للمستخدم الحصول على كلمة مرور واحدة فقط، ولكن يمكنه الحصول على مفاتيح مرور متعددة، مثل مفتاح مرور تتم مزامنته من خلال "سلسلة مفاتيح iCloud" من Apple ومفتاح آخر من خلال "مدير كلمات المرور في 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,
      credentialPublicKey,
      credentialID,
      credentialBackedUp
    } = registrationInfo;

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

    const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
    const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);

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

    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      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 من بيانات العميل، وهي على الويب البيانات التي يراها المتصفّح. يحتوي على مصدر RP والتحدي وandroidPackageName إذا كان العميل تطبيق Android. بصفتك RP، يتيح لك قراءة 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 أو التعرّف على القائمة الكاملة لعمليات التحقّق في المواصفات.

التالي

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