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

يوضّح هذا الدليل بالتفصيل كيفية السماح للمستخدمين بتسجيل مفتاح مرور لحساباتهم، ما يوفّر بديلاً آمنًا للغاية لتسجيل الدخول بدون كلمة مرور ومقاومًا للتصيّد الاحتيالي.

متطلبات قاعدة البيانات

لإتاحة عمليات تسجيل مفاتيح المرور، يجب أن يخزّن جدول بيانات الاعتماد في قاعدة البيانات الحقول التالية:

export interface StoredPasskeyCredential {
  id: string; // Base64URL-encoded credential ID (unique lookup key)
  passkeyUserId: string; // Associated application user ID
  credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
  credentialType: "public-key";
  credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
  credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
  aaguid: string; // Authenticator Attestation GUID
  providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
  name: string; // Provider name derived from AAGUID registry
  transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
  lastUsedAt?: number; // Optional epoch timestamp of last sign-in
  registeredAt: number; // Registration epoch timestamp
  counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}

من جهة الخادم

إنشاء خيارات

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

  1. استخدام معرّف الجهة المعتمدة المحدّد مسبقًا: استخدِم معرّف الجهة المعتمدة المناسب المحدّد مسبقًا كسلسلة ثابتة.
  2. إنشاء تحدٍّ آمن: أنشِئ مخزنًا مؤقتًا عشوائيًا عالي الإنتروبيا وآمنًا من الناحية التشفيرية على الخادم، وخزِّنه بأمان في جلسة المستخدم، وشفِّره باستخدام Base64URL لتسليم الخيارات.
  3. تجنُّب تكرار مفاتيح المرور: يمكنك ربط معرّفات بيانات الاعتماد الحالية المسجّلة مسبقًا للمستخدم بمصفوفة خيارات excludeCredentials. يمنع ذلك أداة المصادقة من تسجيل بيانات اعتماد مكرّرة على حساب مقدّم مفتاح المرور نفسه.
  4. فرض بيانات الاعتماد القابلة للاكتشاف: اضبط requireResidentKey: true وresidentKey: "required" في خيارات authenticatorSelection لطلب بيانات اعتماد قابلة للاكتشاف، وهو أمر ضروري لعمليات تسجيل الدخول القابلة للاكتشاف.
  5. ضبط إعدادات "التحقّق من المستخدم": حدِّد userVerification: "preferred" أو userVerification: "required". تتطلب العديد من حالات استخدام الامتثال (مثل الشؤون المالية والرعاية الصحية) 'required' لفرض التحقق من المستخدم عند الإنشاء.
  6. تحديد نطاق المرفقات:
    • مسار الترويج: عند اقتراح إنشاء مفتاح مرور بعد تسجيل الدخول باستخدام كلمة مرور عادية أو بعد الاشتراك، اضبط authenticatorAttachment: "platform" لفرض استخدام أداة المصادقة على المنصة وتخطّي طلبات مفتاح الأمان الخارجي.
    • مسار الإدارة: عند استدعاء واجهة برمجة التطبيقات من لوحة إعدادات أو أمان مخصّصة تتيح استخدام مفاتيح الأمان الخارجية بالإضافة إلى أداة مصادقة النظام الأساسي، يجب حذف السمة authenticatorAttachment بالكامل.
    • ملاحظة: يمكنك قبول علامة طلب promotion: boolean للتعامل مع كلا المسارَين بشكل مشروط باستخدام نقطة نهاية واحدة.
// Options generation example
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
  rp: { id: "example.com", name: "Secure Application" },
  user: {
    id: userBase64UrlId, // Unique base64url string identifying the account
    name: "user@example.com",
    displayName: "Jane Doe",
  },
  pubKeyCredParams: [
    {
      type: "public-key",
      alg: -7,
    },
    {
      type: "public-key",
      alg: -257,
    },
  ],
  excludeCredentials: userExistingCredentials.map((cred) => ({
    type: "public-key",
    id: cred.id,
    transports: cred.transports,
  })),
  authenticatorSelection: {
    residentKey: "required",
    requireResidentKey: true,
    userVerification: "preferred",
    ...(isPromotionFlow && { authenticatorAttachment: "platform" }),
  },
};

التحقق

  1. التحقّق من التحدّي: يمكنك التحقّق من التحدّي بأمان مقارنةً بالتحدّي المتوقّع المرتبط بالجلسة.
  2. التحقّق من حضور المستخدم:
    • يُرجى التأكّد من أنّ علامة "المستخدم حاضر" (UP) التي تم عرضها في بيانات أداة المصادقة التي تم تحليلها هي true لتأكيد حضور المستخدم فعليًا في وقت الإنشاء.
  3. تخفيف متطلبات التحقّق من الهوية في حالة "مفضّل":
    • عندما تحدّد خيارات الإنشاء userVerification: "preferred"، يجب ضبط طلب التحقّق من جهة الخادم باستخدام requireUserVerification: false. وإلا، فإنّ أدوات المصادقة التي تسجّل بدون التحقّق من المستخدم (على سبيل المثال، أقفال الشاشة غير مفعّلة) ستؤدي إلى حدوث أخطاء زائفة في التحقّق من الخادم.

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

  1. حظر واجهة المستخدم عند تحميل الصفحة:
    • عند تحميل الصفحة، يمكنك استدعاء PublicKeyCredential.getClientCapabilities() وإيقاف الزر "إنشاء مفتاح مرور" في حال عدم توفّر conditionalGet أو passkeyPlatformAuthenticator.
  2. استدعاء الإنشاء والتسلسل: يمكنك فك ترميز خيارات الخادم باستخدام PublicKeyCredential.parseCreationOptionsFromJSON() وتمرير الإعدادات الناتجة إلى navigator.credentials.create().
    • يجب استدعاء credential.toJSON() لترميز AuthenticatorAttestationResponse إلى عنصر صالح يمكن تسلسله بتنسيق JSON قبل جلب نقطة نهاية التحقّق.
  3. التعامل مع استثناءات WebAuthn:
    • ‫InvalidStateError: سبق أن تم إعداد مفتاح مرور مطابق (تمت المطابقة حسب excludeCredentials).
    • ‫NotAllowedError: ألغى المستخدم مربّع حوار مفتاح مرور المصادقة أو انتهت مهلته.
    • ‫AbortError: تم إلغاء العملية.
    • ‫SecurityError: أخطاء عدم تطابق المصادر الآمنة (HTTPS) أو معرّف الجهة المعتمدة (مشاكل في الإعدادات)
  4. فصل Try/Catch في Signal API:
    • يجب تضمين طلب التحقّق من جهة الخادم fetch() في كتلة try/catch. يتم استدعاء signalUnknownCredential() عندما يتعذّر جلب عملية التحقّق من الخادم (أي حالة response.ok === false أو أخطاء في الشبكة).
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";

async function registerPasskey(isPromotion = false) {
  // Verify passkey capability and conditional UI are available
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (
    !capabilities.passkeyPlatformAuthenticator ||
    !capabilities.conditionalGet
  ) {
    // Hide "Create passkey" buttons and fall back to password flows instead
    showStandardPasswordFallbackUI();
    return;
  }

  const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
  const publicKey =
    PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // passkey prompt execution
    credential = await navigator.credentials.create({ publicKey });
  } catch (err) {
    if (err.name === "InvalidStateError") {
      console.log("A passkey already exists for this account.");
      alert("A passkey already exists for this account.");
    } else if (err.name === "SecurityError") {
      console.error("Configuration RP ID or Secure Context error.");
      alert("Configuration RP ID or Secure Context error.");
    } else if (err.name === "NotAllowedError") {
      console.log("User cancelled the passkey dialog.");
    } else if (err.name === "AbortError") {
      console.log("The creation operation has been aborted.");
    }
    return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
  }

  // Server Verification phase (Segregated Try/Catch)
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // Server verification failed to verify/authenticate the credential (orphaned)
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Server verification network failure:", serverErr);
    await publickeycredential.signalunknowncredential({
      rpId, // RP ID must match the one defined on the server
      credentialid: encodedresponse.id, // base64url-encoded credential id
    });
  }
}

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

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

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

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

Easy JSON Serialization Fallback

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

import 'webauthn-polyfills';