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

نظرة عامة

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

مسار مصادقة مفتاح المرور

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

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

أنشِئ التحدي

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

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

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

  1. تأكَّد من عدم استخدام اختبار التحقُّق نفسه أكثر من مرة. إنشاء تحدٍ جديد في كل محاولة تسجيل دخول تجاهل التحدي بعد كل محاولة تسجيل دخول، سواء نجحت أو فشلت. أيضًا تجاهل التحدي بعد مدة معينة. عدم قبول التحدي نفسه في الرد أكثر من مرة.
  2. تأكَّد من أنّ التحدي آمن من خلال التشفير. يجب أن يكون من المستحيل عمليًا تخمين التحدي. لإنشاء اختبار تحقق آمن من جهة الخادم من خلال التشفير، من الأفضل الاعتماد على مكتبة FIDO من جهة الخادم تثق بها. وإذا أنشأت تحدياتك الخاصة بدلاً من ذلك، يمكنك استخدام وظيفة التشفير المضمَّنة المتوفّرة في حزمة التكنولوجيا، أو البحث عن المكتبات المصمّمة لحالات الاستخدام المشفَّرة. من الأمثلة على ذلك iso-crypto في Node.js، أو الأسرار في بايثون. وفقًا للمواصفات، يجب ألا يقل حجم التحدي عن 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 {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} 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 {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  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.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      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 أو الاطّلاع على القائمة الكاملة لعمليات إثبات الملكية في المواصفات.