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

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

من جهة الخادم

إنشاء خيارات

أنشِئ نقطة نهاية تنشئ مَعلمات طلب WebAuthn باستخدام مكتبة تم فحصها وفقًا للمعايير.

  1. استخدام معرّف الجهة المعتمدة المحدّد مسبقًا: استخدِم معرّف الجهة المعتمدة المناسب المحدّد مسبقًا كسلسلة ثابتة.
  2. إنشاء اختبار: أنشِئ مخزنًا مؤقتًا عشوائيًا آمنًا من الناحية التشفيرية وذا إنتروبيا عالية، وخزِّنه بأمان في جلسة المستخدم، ورمِّزه بتنسيق Base64URL.
  3. ربط بيانات الاعتماد القابلة للاكتشاف: حدِّد مصفوفة فارغة [] لـ allowCredentials. يطلب هذا الإجراء بيانات اعتماد قابلة للاكتشاف، ما يعني أنّ المستخدم لا يحتاج إلى إدخال اسم المستخدم أولاً، بل سيعرض مقدّم مفتاح المرور الحسابات المتاحة.
  4. مستوى التحقّق من المستخدم: اضبطه على userVerification: "preferred" (أو "required" إذا كان ذلك مطلوبًا صراحةً بموجب سياسات الامتثال في الشركة).
    • يجب الاحتفاظ بمستوى قيد userVerification المطلوب داخل سجلّ جلسة الخادم في نقطة نهاية الخيارات، بدلاً من تمريره من العميل عبر سلاسل طلبات البحث. يتيح ذلك لنقطة نهاية التحقّق فرض قيود مطابقة صارمة بأمان بدون خطر تلاعب العميل.
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

نقطة نهاية التحقّق

تحقَّق بأمان من التأكيد الذي يعرضه العميل لمصادقة المستخدم:

  1. التحقّق من صحة اختبار الجلسة: يمكنك فرض مطابقة صارمة للاختبار بين استجابة العميل والاختبار المتوقّع المخزَّن في الجلسة.
  2. فرض الإعدادات المفضّلة للتحقّق من المستخدم:
    • السماح بأدوات المصادقة التي لا تتطلّب التحقّق من المستخدم (مثل أقفال شاشة أداة المصادقة غير المفعَّلة) إذا طلبت الجلسة expectedUserVerification "preferred"، من خلال تمرير requireUserVerification: false إلى مكتبة التحقّق من جهة الخادم إذا طُلب منك ذلك من قِبل "required"، عليك فرض إدخال المقاييس الحيوية أو رقم التعريف الشخصي بشكل صارم.
  3. خطأ في الخادم 404: إذا لم يتم العثور على معرّف بيانات الاعتماد الذي يعرضه العميل في قاعدة البيانات، يجب عرض خطأ في بروتوكول HTTP 404 صريح ليتمكّن العميل من تشغيل Signal API.

المنطق من جهة العميل

تعليق توضيحي لنموذج HTML

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

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

مسار الزر الصريح

يمكنك تفعيل المصادقة باستخدام مفتاح المرور عندما ينقر المستخدم على زر "تسجيل الدخول باستخدام مفتاح المرور". إلغاء أي طلبات جارية للملء التلقائي للنماذج (Conditional Get) قبل استدعاء طلب مفتاح المرور

مسار التوسّط الشرطي (الملء التلقائي للنموذج)

فعِّل اقتراحات الملء التلقائي للنماذج عند تحميل الصفحة لتقديم مصادقة مفتاح المرور بشكلٍ أصلي عندما يركّز المستخدمون على حقول تسجيل الدخول:

  1. رصد الميزة: يمكنك استدعاء PublicKeyCredential.getClientCapabilities() عند تحميل الصفحة وتخطّي تسجيل الدخول باستخدام مفتاح المرور إذا لم يكن conditionalGet متاحًا.
  2. خيارات فك الترميز: فك ترميز كائن JSON لبيانات الاعتماد التي تم جلبها باستخدام PublicKeyCredential.parseRequestOptionsFromJSON().
  3. استدعاء Get الشرطي: يمكنك استدعاء navigator.credentials.get() باستخدام mediation: "conditional" وتمرير إشارة AbortController. يؤدي ذلك إلى تسجيل ميزة "الملء التلقائي" بدون عرض مربّع حوار مفتاح المرور.
  4. فصل استثناءات Try/Catch: يمكنك تضمين طلب navigator.credentials.get في مجموعة try/catch:
    • ‫NotAllowedError: ألغى المستخدم طلب تسجيل الدخول باستخدام مفتاح المرور أو انتهت مهلة الطلب.
    • ‫AbortError: تم إلغاء طلب المصادقة آليًا.
  5. Call Signal API: يمكنك تضمين طلب التحقّق من الخادم fetch() في كتلة try/catch:
    • عرض رسالة خطأ للمستخدم ليفهم المشكلة
    • يجب الاتصال بـ signalUnknownCredential() فقط عندما يستجيب الخادم صراحةً بحالة HTTP 404 (لم يتم العثور على بيانات الاعتماد) ويكون المستخدم غير مصادَق عليه.
    • يجب أن تكون المَعلمة credentialId التي تم تمريرها إلى signalUnknownCredential() هي سلسلة معرّف بيانات الاعتماد المرمَّزة بنظام Base64URL (مثل encoded.id)، وليس عنصر ArrayBuffer الأولي credential.rawId.
  6. ترميز الردّ: يمكنك ترميز بيانات الاعتماد AuthenticatorAssertionResponse باستخدام .toJSON() قبل إرسالها إلى الخادم لإثبات الهوية.
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

الاستراتيجيات الاحتياطية

الاحتياطي لرصد ميزة مفتاح المرور

يجب دائمًا تثبيت "webauthn-polyfills" واستيراده في السياق. يُرجى العِلم أنّه طالما أنّ PublicKeyCredential متوافق، فإنّ PublicKeyCredential.getClientCapabilities متوافق أيضًا.

import 'webauthn-polyfills';

الاحتياطي لمزامنة Signal API

واجهة WebAuthn Signal API‏ (webauthn-signals) هي تحسين تدريجي يُستخدَم لإبقاء مدراء كلمات المرور متزامنين مع حالة بيانات الاعتماد على الخادم.

  • التجربة البديلة: يتم حظرها من خلال if (PublicKeyCredential.signalUnknownCredential). وإذا لم يكن متوافقًا، يتم تجاوز مزامنة التحقّق في الخلفية بسلاسة بدون ظهور استثناءات في المتصفّح.

Easy JSON Serialization Fallback

يجب دائمًا تثبيت "webauthn-polyfills" واستيراده في السياق. يُرجى العِلم أنّه طالما أنّ PublicKeyCredential متوافق، يكون PublicKeyCredential.parseRequestOptionsFromJSON وPublicKeyCredential.prototype.toJSON متوافقَين أيضًا.

import 'webauthn-polyfills';