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

نظرة عامة

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

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

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

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

إنشاء التحدي

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

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

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

  1. ضمان عدم استخدام التحدي نفسه أكثر من مرة: إنشاء اختبار تحقُّق جديد في كل محاولة تسجيل دخول تجاهل التحدي بعد كل محاولة تسجيل دخول، سواء نجحت في ذلك أم لم تنجح تجاهل التحدي بعد مدة معينة أيضًا. عدم قبول التحدي نفسه في رد أكثر من مرة
  2. التأكد من أن التحدي آمن من خلال التشفير. من المفترض أن يكون من المستحيل عمليًا تخمين التحدي. لإنشاء اختبار تحدي آمن من جهة الخادم، من الأفضل الاعتماد على مكتبة FIDO من جهة الخادم التي تثق بها. وإذا ابتكرت تحدياتك الخاصة، يمكنك استخدام وظائف التشفير المضمّنة المتاحة في حزمة التكنولوجيا أو البحث عن المكتبات المصمّمة لحالات استخدام التشفير. تتضمن الأمثلة iso-crypto في Node.js أو secrets في بايثون. وفقًا للمواصفات، يجب أن يبلغ طول اختبار التحقق 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 أو الاطّلاع على تفاصيل القائمة الكاملة لعمليات إثبات الملكية في المواصفات.