إدارة مفاتيح المرور

يوضّح هذا الدليل بالتفصيل كيفية السماح للمستخدمين بعرض مفاتيح المرور المسجّلة وإعادة تسميتها وحذفها مع الحفاظ على مزامنة بيانات الاعتماد المحفوظة وتفاصيل ملف المستخدم الشخصي بين الخادم ومديري كلمات المرور لدى المستخدم باستخدام Signal API.

العمليات من جهة الخادم

يجب أن تتيح طبقة قاعدة البيانات الخلفية ونقاط النهاية إجراءات CRUD الشائعة لبيانات الاعتماد المسجّلة، بالإضافة إلى تعديل تفاصيل ملف المستخدم الشخصي. بشكل منفصل عن المكتبات الخاصة بإطار العمل، يعرض الخادم نقاط نهاية من أجل:

  1. عرض جميع بيانات اعتماد المستخدم: يمكنك جلب جميع سجلات StoredPasskeyCredential التي تطابق رقم تعريف المستخدم الذي سجّل الدخول.
  2. تعديل أسماء بيانات الاعتماد: يمكنك قبول اسم مستعار مخصّص جديد لرقم تعريف بيانات اعتماد معيّن وحفظ التعديل على الخادم.
  3. حذف بيانات الاعتماد: يمكنك إزالة معرّف بيانات اعتماد معيّن من قاعدة البيانات.
  4. تعديل تفاصيل حساب المستخدم: يمكنك قبول القيم المعدَّلة لـ name (اسم المستخدم) وdisplayName في الملف الشخصي للمستخدم الذي سجّل الدخول حتى يتمكّن العميل من مزامنتها مع مزوّد مفتاح المرور.
// Node.js routing example for credential CRUD and user profile rename
router.get('/api/credentials', checkUserAuthenticated, async (req, res) => {
  const list = await db.findCredentialsByUserId(req.user.id);
  return res.json(list);
});

router.put('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
  const { id } = req.params;
  const { name } = req.body;
  const cred = await db.findCredentialById(id);
  if (!cred || cred.passkeyUserId !== req.user.id) {
    return res.status(404).json({ error: 'Credential not found.' });
  }
  cred.name = name;
  await db.saveCredential(cred);
  return res.json(cred);
});

router.delete('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
  const { id } = req.params;
  const cred = await db.findCredentialById(id);
  if (!cred || cred.passkeyUserId !== req.user.id) {
    return res.status(404).json({ error: 'Credential not found.' });
  }
  await db.deleteCredential(id);
  return res.json({ success: true });
});

router.put('/api/user', checkUserAuthenticated, async (req, res) => {
  const { name, displayName } = req.body;
  const updatedUser = await db.updateUser(req.user.id, { name, displayName });
  return res.json(updatedUser);
});

واجهة مستخدم الإدارة من جهة العميل

يمكنك عرض لوحة إعدادات مخصّصة تتيح للمستخدمين مراجعة خيارات المصادقة المسجّلة وتفاصيل الملف الشخصي للحساب وإدارتها بسهولة:

  1. عرض القائمة المحفوظة: يمكنك جلب القائمة من نقطة النهاية وعرض صفوف بيانات الاعتماد الفردية. إذا كان الردّ فارغًا، اعرِض رسالة مفيدة تشير إلى عدم توفّر بيانات (مثل "لم يتم العثور على أي مفاتيح مرور").
  2. ربط البيانات الوصفية لـ AAGUID: لكل مفتاح مرور، ابحث عن سمة aaguid في السجلّ المحلي لعرض تفاصيل مقدّم الخدمة. لمزيد من التفاصيل، يُرجى الاطّلاع على قسم تحديد مقدّم خدمة مفتاح المرور من معرّف AAGUID.
  3. متطلبات واجهة المستخدم لكل عنصر: يجب أن يعرض كل صف داخل حاوية القائمة ما يلي:
    • رمز مقدّم الخدمة: صورة مشتقّة من معرّف AAGUID أو معرّف موارد منتظم (URI) للبيانات.
    • المزوّد/الاسم المخصّص: الاسم المشتق من معرّف AAGUID أو السلسلة التي أعاد المستخدم تسميتها
    • تاريخ التسجيل: الطابع الزمني الأولي registeredAt المحفوظ في قاعدة البيانات بتنسيق يمكن لشخص عادي قراءته لعرضه على العميل.
    • تاريخ آخر استخدام: الطابع الزمني الأولي للحقبة lastUsedAt المحفوظ في قاعدة البيانات والمنسَّق كتاريخ يمكن لشخص عادي قراءته (إذا كان متوفّرًا) لعرضه على العميل.
    • زر إعادة التسمية: يؤدي إلى تفعيل إجراء إعادة التسمية لتعديل الاسم المخصّص لبيانات الاعتماد على الخادم (PUT /api/credential/:id). ملاحظة: إعادة تسمية الاسم المخصّص لمفتاح مرور فردي هي تغيير في التصنيف من جهة الخادم ولا تؤدي إلى استدعاء signalCurrentUserDetails().
    • زر الحذف: يؤدي إلى حذف بيانات الاعتماد (DELETE /api/credential/:id) ومزامنة معرّفات بيانات الاعتماد المتبقية من خلال signalAllAcceptedCredentials().
  4. واجهة المستخدم لإعادة تسمية حساب المستخدم:
    • يجب توفير عنصر تحكّم يتيح للمستخدم تعديل اسم مستخدم حسابه (name) أو displayName (PUT /api/user)، ومزامنة تفاصيل المستخدم المعدَّلة على الفور مع مدير كلمات المرور من خلال PublicKeyCredential.signalCurrentUserDetails().
  5. زر "إنشاء مفتاح مرور" الشرطي:
    • يجب توفير زر بارز لتفعيل تسجيل "إنشاء مفتاح مرور" في صفحة الإدارة. قبل عرض هذا العنصر في واجهة المستخدم، يجب أن ترصد الصفحة الميزات باستخدام PublicKeyCredential.getClientCapabilities() للتأكّد من أنّ أداة المصادقة على المنصة متوافقة. إذا لم تكن مفاتيح المرور متاحة، يمكنك إخفاء هذا الزر وتشجيع المستخدمين على التسجيل في ميزة "المصادقة المتعدّدة العوامل" العادية بدلاً من ذلك.
    • السماح بتسجيل مفتاح أمان من خلال حذف authenticatorSelection.authenticatorAttachment في طلب navigator.credentials.create()

مزامنة واجهة برمجة التطبيقات Signal

تتيح واجهة Signal API للتطبيق إبلاغ مدراء كلمات المرور بحالات بيانات الاعتماد، ما يحافظ على مزامنة خزائن المستخدم وقاعدة البيانات الخلفية بشكل متزامن.

  • قاعدة رصد الميزات وترميز المَعلمات:
    • يجب دائمًا رصد ميزات طرق Signal API قبل استدعائها (if (PublicKeyCredential.signalAllAcceptedCredentials) وif (PublicKeyCredential.signalCurrentUserDetails)).
    • يجب أن تكون جميع مَعلمات userId ومعرّفات بيانات الاعتماد التي يتم تمريرها إلى طرق Signal API ‏ (signalAllAcceptedCredentials وsignalCurrentUserDetails) سلاسل بترميز Base64URL.
  • بدء مزامنة تحميل الصفحة:
    • يجب أن يستدعي التطبيق signalAllAcceptedCredentials() تلقائيًا عند تحميل صفحة الإدارة (على سبيل المثال، داخل متتبِّع الأحداث DOMContentLoaded أو إعداد الوحدة أو عنصر الربط الخاص بمكوّن إطار العمل، مثل useEffect).
  • مزامنة تعديلات الإدارة:
    • يجب أن يستدعي التطبيق signalAllAcceptedCredentials() فورًا ضمن معالج النقر على حذف بيانات الاعتماد بعد جلبها.
    • يجب أن يستدعي التطبيق signalCurrentUserDetails() فورًا ضمن معالج النقر لإعادة تسمية حساب المستخدم (name أو displayName) بعد الجلب.
// Client-side management synchronization ES module
import { listFetch, renameCredentialFetch, updateUserFetch, deleteFetch } from './api.js';

// Base64URL-encoded User ID string (illustration only)
const base64UrlUserId = "M2YPl-KGnA8";

async function syncAcceptedCredentials(currentCredentialsList) {
  if (!window.PublicKeyCredential || !PublicKeyCredential.signalAllAcceptedCredentials) return;
  try {
    const credentialIds = currentCredentialsList.map(c => c.id); // Array of Base64URL credential ID strings
    
    await PublicKeyCredential.signalAllAcceptedCredentials({
      rpId, // RP ID must match the one defined on the server
      userId: base64UrlUserId, // User ID Base64URL-encoded string
      allAcceptedCredentialIds: credentialIds
    });
  } catch (e) {
    console.error('SignalAllAcceptedCredentials sync failure:', e);
  }
}

async function loadManagementPanel() {
  const response = await listFetch();
  const list = await response.json();
  
  renderUI(list);
  // Sync accepted credentials on initial load
  await syncAcceptedCredentials(list);
}

async function performDelete(credentialId) {
  const response = await deleteFetch(credentialId);
  if (response.ok) {
    const updatedResponse = await listFetch();
    const updatedList = await updatedResponse.json();
    
    renderUI(updatedList);
    // Sync remaining accepted credentials after deletion
    await syncAcceptedCredentials(updatedList);
  }
}

// Renaming a passkey's nickname updates the server record only
async function performCredentialRename(credentialId, newCredentialName) {
  const response = await renameCredentialFetch(credentialId, { name: newCredentialName });
  if (response.ok) {
    const updatedResponse = await listFetch();
    renderUI(await updatedResponse.json());
  }
}

// Renaming the user's account username or displayName triggers signalCurrentUserDetails
async function performUserRename(rpId, userId, updatedName, updatedDisplayName) {
  const response = await updateUserFetch({ name: updatedName, displayName: updatedDisplayName });
  if (response.ok && window.PublicKeyCredential && PublicKeyCredential.signalCurrentUserDetails) {
    try {
      await PublicKeyCredential.signalCurrentUserDetails({
        rpId, // RP ID must match the one defined on the server
        userId, // Base64URL-encoded user ID
        name: updatedName, // Updated username
        displayName: updatedDisplayName // Updated display name
      });
    } catch (e) {
      console.error('SignalCurrentUserDetails sync failure:', e);
    }
  }
}

window.addEventListener('DOMContentLoaded', loadManagementPanel);

تحديد موفِّر مفتاح المرور من معرّف AAGUID

معرّف شهادة المصادقة الفريد عالميًا (AAGUID) هو معرّف مكوّن من 128 بت يمثّل طراز أداة المصادقة، وليس مثيلاً معيّنًا. ويتم تضمينه في بيانات المصادقة أثناء تسجيل مفتاح المرور، ويمكن استخدامه لتحديد مقدّم مفتاح المرور (مثل "مدير كلمات المرور في Google" أو iCloud Keychain أو 1Password) الذي أنشأ بيانات الاعتماد.

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

1. سجلّ AAGUID

يتوفّر تعيين JSON الذي يديره المنتدى لمعرّفات AAGUID لأسماء مقدّمي الخدمات ورموزهم على الرابط التالي:

https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json

يتضمّن كل إدخال المخطّط التالي:

{
  "<aaguid-uuid>": {
    "name": "Provider Name",
    "icon_light": "data:image/png;base64,...",
    "icon_dark": "data:image/png;base64,..."
  }
}

2. استخدام معرّف AAGUID بعد التسجيل

بعد التحقّق من استجابة التسجيل، اقرأ aaguid من نتيجة التسجيل وابحث عنه في السجلّ لملء name وproviderIcon لبيانات الاعتماد:

قبل البحث عن معرّف AAGUID في السجلّ، تحقَّق مما إذا كان يساوي '00000000-0000-0000-0000-000000000000'. في هذه الحالة، يمكنك تخطّي البحث في السجلّ وضبط name على قيمة احتياطية (مثل اسم الجهاز من وكيل المستخدم أو "موفّر مفتاح مرور غير معروف") وproviderIcon على undefined. يتم البحث في السجلّ عن معرّفات AAGUID غير الصفرية فقط.

import aaguids from './aaguids.json' with { type: 'json' };

const { aaguid } = registrationInfo;
if (aaguid === '00000000-0000-0000-0000-000000000000') {
  // use the device name as the passkey provider based on
  // the information derived from the user agent string,
  // or just say "Unknown passkey provider"
} else {
  const provider = aaguids[aaguid];
  const credential = {
    // ...other fields
    aaguid,
    name: provider?.name || 'Unknown passkey provider',
    providerIcon: provider?.icon_light,
  };
}

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

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

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

import 'webauthn-polyfills';

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

إذا كان المتصفّح لا يتوافق مع PublicKeyCredential.parseRequestOptionsFromJSON، استخدِم "webauthn-polyfills":

<script type="module"> 
  if (!PublicKeyCredential.parseRequestOptionsFromJSON) { 
     await import('https://unpkg.com/webauthn-polyfills'); 
   } 
 </script> 

سيؤدي ذلك أيضًا إلى إتاحة استخدام PublicKeyCredential.prototype.toJSON.