استخدام مفاتيح المرور من خلال ميزة الملء التلقائي للنموذج في تطبيق ويب

1. قبل البدء

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

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

  • زر ينشئ مفتاح مرور بعد أن يسجّل المستخدم الدخول
  • واجهة مستخدم تعرض قائمة بمفاتيح المرور المسجّلة
  • نموذج تسجيل الدخول الحالي الذي يتيح للمستخدمين تسجيل الدخول باستخدام مفتاح مرور مسجّل من خلال ميزة "الملء التلقائي للنماذج"

المتطلبات الأساسية

أهداف الدورة التعليمية

  • كيفية إنشاء مفتاح مرور
  • كيفية مصادقة المستخدمين باستخدام مفتاح مرور
  • كيفية السماح لأحد النماذج باقتراح مفتاح مرور كخيار لتسجيل الدخول

2. طريقة الإعداد

في هذا الدرس التطبيقي حول الترميز، ستنسخ تطبيقًا تجريبيًا غير مكتمل من GitHub، ثم ستكمل عملية تنفيذ ميزة استخدام مفاتيح المرور.

استنساخ المشروع

  1. افتح المشروع على GitHub.
  2. استنساخ المشروع أو تنزيله

ac587c53b746785a.png

تشغيل المشروع

  1. افتح نافذة Terminal واكتب cd start لتغيير الدليل.
  2. نفِّذ الأمر npm install لتثبيت تبعيات المشروع.
  3. إنشاء المشروع وتشغيله باستخدام npm run build && IS_LOCAL=1 npm run start
  4. افتح http://localhost:8080/ في المتصفّح.

فحص الحالة الأولية للموقع الإلكتروني

  1. على الموقع الإلكتروني، أدخِل اسم مستخدم عشوائيًا، ثم انقر على التالي.
  2. أدخِل كلمة مرور عشوائية، ثم انقر على تسجيل الدخول. يتم تجاهل كلمة المرور، ولكن يتم إثبات هويتك وتنتقل إلى الصفحة الرئيسية.
  3. إذا أردت تغيير اسمك المعروض، يمكنك إجراء ذلك، وهذا كل ما يمكنك فعله في الحالة الأولية.
  4. انقُروا على الخروج.

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

لمزيد من المعلومات حول طريقة عمل مفاتيح المرور، يُرجى الاطّلاع على المقالة كيف تعمل مفاتيح المرور؟.

3- إضافة إمكانية إنشاء مفتاح مرور

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

9b84dbaec66afe9c.png

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

إنشاء الدالة registerCredential()

  1. في أداة تعديل الرموز التي تختارها، افتح الدليل start.
  2. انتقِل إلى ملف public/client.js، ثم انتقِل إلى نهايته.
  3. بعد التعليق ذي الصلة، أضِف الدالة registerCredential() التالية:

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to create a passkey: Create a credential.

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

};

تنشئ هذه الدالة مفتاح مرور وتسجّله على الخادم.

الحصول على التحدي والخيارات الأخرى من نقطة نهاية الخادم

قبل إنشاء مفتاح مرور، عليك طلب مَعلمات لتمريرها في WebAuthn من الخادم، بما في ذلك سؤال التحقّق. WebAuthn هي واجهة برمجة تطبيقات متصفّح تتيح للمستخدم إنشاء مفتاح مرور والمصادقة باستخدام مفتاح المرور. لحسن الحظ، لديك نقطة نهاية خادم تستجيب لمثل هذه المَعلمات في هذا الدرس العملي.

  • للحصول على التحدّي وخيارات أخرى من نقطة نهاية الخادم، أضِف الرمز التالي إلى نص الدالة registerCredential() بعد التعليق ذي الصلة:

public/client.js

// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
const _options = await _fetch('/auth/registerRequest');

يتضمّن مقتطف الرمز التالي خيارات نموذجية تتلقّاها من الخادم:

{
  challenge: *****,
  rp: {
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },  
  pubKeyCredParams: [{
    alg: -7, type: "public-key"
  },{
    alg: -257, type: "public-key"
  }],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal', 'hybrid'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

لا يشكّل البروتوكول بين الخادم والعميل جزءًا من مواصفات WebAuthn. ومع ذلك، تم تصميم خادم هذا الدرس التطبيقي حول الترميز لعرض ملف JSON مشابه قدر الإمكان لقائمة PublicKeyCredentialCreationOptions التي يتم تمريرها إلى واجهة برمجة التطبيقات navigator.credentials.create() WebAuthn.

الجدول التالي ليس شاملاً، ولكنّه يحتوي على المَعلمات المهمة في قاموس PublicKeyCredentialCreationOptions:

المعلّمات

الأوصاف

challenge

تمثّل هذه السمة اختبارًا من إنشاء الخادم في عنصر ArrayBuffer لعملية التسجيل هذه. هذا الإعداد مطلوب ولكن لا يتم استخدامه أثناء التسجيل إلا عند إجراء التصديق، وهو موضوع متقدّم لا يتم تناوله في هذا الدرس العملي.

user.id

معرّف فريد للمستخدم يجب أن تكون هذه القيمة عبارة عن عنصر ArrayBuffer لا يتضمّن معلومات تحديد الهوية الشخصية، مثل عناوين البريد الإلكتروني أو أسماء المستخدمين. تكون القيمة العشوائية المكوّنة من 16 بايت التي يتم إنشاؤها لكل حساب مناسبة.

user.name

يجب أن يحتوي هذا الحقل على معرّف فريد للحساب يمكن للمستخدم التعرّف عليه، مثل عنوان بريده الإلكتروني أو اسم المستخدم. ويظهر في أداة اختيار الحساب. (إذا كنت تستخدم اسم مستخدم، استخدِم القيمة نفسها المستخدَمة في مصادقة كلمة المرور).

user.displayName

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

rp.id

رقم تعريف الجهة المعتمِدة (RP) هو نطاق. يمكن للموقع الإلكتروني تحديد نطاقه أو لاحقة قابلة للتسجيل. على سبيل المثال، إذا كان أصل RP هو https://login.example.com:1337، يمكن أن يكون معرّف RP إما login.example.com أو example.com. إذا تم تحديد معرّف الجهة الاعتمادية على أنّه example.com، يمكن للمستخدم المصادقة على login.example.com أو على أي نطاقات فرعية أخرى من example.com.

pubKeyCredParams

يحدّد هذا الحقل خوارزميات المفتاح العام التي يتيحها الطرف المعتمد. ننصحك بضبطه على [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. يحدّد ذلك إمكانية استخدام ECDSA مع P-256 وRSA PKCS#1، وتوفير هذه الميزات يضمن التغطية الكاملة.

excludeCredentials

توفّر هذه السمة قائمة بمعرّفات بيانات الاعتماد المسجّلة مسبقًا لمنع تسجيل الجهاز نفسه مرّتين. في حال توفُّرها، يجب أن يحتوي العنصر transports على نتيجة استدعاء الدالة getTransports() أثناء تسجيل كل بيانات اعتماد. يمكنك الاطّلاع على مزيد من المعلومات في مستنداتنا حول كيفية منع إنشاء مفتاح مرور جديد في حال توفّر مفتاح.

authenticatorSelection.authenticatorAttachment

اضبط القيمة على "platform". يشير ذلك إلى أنّك تريد أداة مصادقة مضمّنة في جهاز النظام الأساسي، وبالتالي لن يُطلب من المستخدم إدخال أي شيء مثل مفتاح أمان USB.

authenticatorSelection.requireResidentKey

اضبطها على true قيمة منطقية. يمكن استخدام بيانات الاعتماد القابلة للاكتشاف (المفتاح المقيم) بدون أن يقدّم الخادم معرّف بيانات الاعتماد، وبالتالي تكون متوافقة مع ميزة "الملء التلقائي". يمكنك الاطّلاع على مزيد من المعلومات في نظرة متعمّقة حول بيانات الاعتماد القابلة للاكتشاف.

authenticatorSelection.userVerification

اضبطها على القيمة "preferred" أو احذفها لأنّها القيمة التلقائية. يشير هذا الحقل إلى ما إذا كان تأكيد هوية المستخدم الذي يستخدم قفل شاشة الجهاز هو "required" أو "preferred" أو "discouraged". عند ضبط القيمة على "preferred"، سيُطلب من المستخدم إثبات هويته عندما يكون الجهاز متوافقًا. مزيد من المعلومات في نظرة متعمّقة حول التحقّق من هوية المستخدم

إنشاء بيانات اعتماد

  1. في نص الدالة registerCredential() بعد التعليق ذي الصلة، حوِّل بعض المَعلمات المرمَّزة باستخدام Base64URL إلى ثنائية، وتحديدًا السلسلتَين user.id وchallenge، ونسخ السلسلة id المُضمَّنة في الصفيف excludeCredentials. يمكن إجراء ذلك باستخدام الدالة PublicKeyCredential.parseCreationOptionsFromJSON():

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.

// Deserialize and decode the `PublicKeyCredential.parseCreationOptionsFromJSON()`.
const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);
  1. في السطر التالي، اضبط authenticatorSelection.authenticatorAttachment على "platform" وauthenticatorSelection.requireResidentKey على true. يسمح هذا الخيار باستخدام أداة مصادقة على المنصة (الجهاز نفسه) مع إمكانية بيانات الاعتماد القابلة للاكتشاف.

public/client.js

// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
  authenticatorAttachment: 'platform',
  requireResidentKey: true
}
  1. في السطر التالي، استدعِ الدالة navigator.credentials.create() لإنشاء بيانات اعتماد.

public/client.js

// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
  publicKey: options,
});

من خلال هذا الطلب، يحاول المتصفّح إثبات هوية المستخدم باستخدام قفل شاشة الجهاز.

تسجيل بيانات الاعتماد في نقطة نهاية الخادم

بعد أن يثبت المستخدم هويته، يتم إنشاء مفتاح مرور وتخزينه. يتلقّى الموقع الإلكتروني عنصر بيانات اعتماد يحتوي على مفتاح عام يمكنك إرساله إلى الخادم لتسجيل مفتاح المرور.

يحتوي مقتطف الرمز التالي على مثال على عنصر بيانات اعتماد:

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "attestationObject": *****,
    "transports": ["internal", "hybrid"]
  },
  "authenticatorAttachment": "platform"
}

الجدول التالي ليس شاملاً، ولكنه يتضمّن المَعلمات المهمة في العنصر PublicKeyCredential:

المعلّمات

الأوصاف

id

معرّف مفتاح المرور الذي تم إنشاؤه، وهو مرمَّز بنظام Base64URL. يساعد هذا المعرّف المتصفّح في تحديد ما إذا كان مفتاح مرور مطابقًا متوفّرًا على الجهاز عند المصادقة. يجب تخزين هذه القيمة في قاعدة البيانات على الخلفية.

rawId

تمثّل هذه السمة إصدار العنصر من معرّف بيانات الاعتماد.ArrayBuffer

response.clientDataJSON

عنصر ArrayBuffer يشفّر بيانات العميل.

response.attestationObject

كائن شهادة مشفر ArrayBuffer ويحتوي على معلومات مهمة، مثل رقم تعريف الجهة الاعتمادية، والعلامات، والمفتاح العام.

response.transports

قائمة بوسائل النقل التي يتوافق معها الجهاز: يعني "internal" أنّ الجهاز يتوافق مع مفتاح مرور. يشير الرمز "hybrid" إلى أنّ الجهاز يتيح أيضًا المصادقة على جهاز آخر.

authenticatorAttachment

تعرِض القيمة "platform" عند إنشاء بيانات الاعتماد هذه على جهاز متوافق مع مفاتيح المرور.

لإرسال عنصر بيانات الاعتماد إلى الخادم، اتّبِع الخطوات التالية:

  1. رمِّز المَعلمات الثنائية لبيانات الاعتماد باستخدام Base64URL حتى يمكن تسليمها إلى الخادم كسلسلة. يمكنك استخدام .toJSON() لإجراء ذلك:

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

// Encode and serialize the `PublicKeyCredential`.
const credential = JSON.stringify(cred);
  1. في السطر التالي، أرسِل العنصر إلى الخادم:

public/client.js

return await _fetch('/auth/registerResponse', credential);

عند تشغيل البرنامج، يعرض الخادم HTTP code 200، ما يشير إلى أنّ بيانات الاعتماد مسجّلة.

أصبحت لديك الآن الدالة registerCredential() الكاملة.

مراجعة رمز الحلّ لهذا القسم

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.

export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.

  const _options = await _fetch('/auth/registerRequest');

  // TODO: Add an ability to create a passkey: Create a credential.

  // Deserialize and decode the `PublicKeyCredential.parseCreationOptionsFromJSON()`.
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);

  // Use platform authenticator and discoverable credential.
  options.authenticatorSelection = {
    authenticatorAttachment: 'platform',
    requireResidentKey: true
  }

  // Invoke the WebAuthn create() method.
  const cred = await navigator.credentials.create({
    publicKey: options,
  });

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

  // Encode and serialize the `PublicKeyCredential`.
  const credential = JSON.stringify(cred);

  return await _fetch('/auth/registerResponse', credential);
};

4. إنشاء واجهة مستخدم لتسجيل بيانات اعتماد مفتاح المرور وإدارتها

بعد أن أصبحت وظيفة registerCredential() متاحة، تحتاج إلى زر لتفعيلها. عليك أيضًا عرض قائمة بمفاتيح المرور المسجّلة.

bfa4e7cdda47669e.png

إضافة رمز HTML للعنصر النائب

  1. في المحرّر، انتقِل إلى الملف views/home.html.
  2. بعد التعليق ذي الصلة، أضِف عنصرًا نائبًا لواجهة المستخدم يعرض زرًا لتسجيل مفتاح مرور وقائمة بمفاتيح المرور:

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3>Your registered passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mdui-button id="create-passkey" class="hidden" icon="fingerprint" type="button">Create a passkey</mdui-button>

العنصر div#list هو العنصر النائب للقائمة.

التحقّق من إمكانية استخدام مفتاح المرور

لعرض خيار إنشاء مفتاح مرور للمستخدمين الذين لديهم أجهزة متوافقة مع مفاتيح المرور فقط، عليك أولاً التحقّق مما إذا كانت WebAuthn متاحة. في هذه الحالة، عليك إزالة الفئة hidden لعرض الزر إنشاء مفتاح مرور.

للتحقّق مما إذا كانت إحدى البيئات تتيح استخدام مفاتيح المرور، اتّبِع الخطوات التالية:

  1. في نهاية ملف views/home.html بعد التعليق ذي الصلة، اكتب شرطًا يتم تنفيذه إذا كانت قيم window.PublicKeyCredential وPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable وPublicKeyCredential.isConditionalMediationAvailable هي true.

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  1. في نص الشرط، تحقَّق ممّا إذا كان الجهاز يمكنه إنشاء مفتاح مرور، ثم تحقَّق ممّا إذا كان يمكن اقتراح مفتاح المرور في عملية الملء التلقائي للنموذج.

views/home.html

try {
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    // Is conditional UI available in this browser?
    if (capabilities.conditionalGet === true &&
        capabilities.passkeyPlatformAuthenticator === true) {
  1. إذا تم استيفاء جميع الشروط، اعرض الزر لإنشاء مفتاح مرور. بخلاف ذلك، اعرض رسالة تحذير.

views/home.html

      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

عرض مفاتيح المرور المسجَّلة في قائمة

  1. حدِّد الدالة renderCredentials() التي تسترد مفاتيح المرور المسجّلة من الخادم وتعرضها في قائمة. لحسن الحظ، لديك نقطة نهاية الخادم /auth/getKeys لجلب مفاتيح المرور المسجّلة للمستخدم الذي سجّل الدخول.

views/home.html

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = res.length > 0 ? html`
    <mdui-list>
      ${res.map(cred => html`
        <mdui-list-item>
          ${cred.name || 'Unnamed'}
          <mdui-button-icon data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed'}" @click="${rename}" icon="edit" slot="end-icon"></mdui-button-icon>
          <mdui-button-icon data-cred-id="${cred.id}" @click="${remove}" icon="delete" slot="end-icon"></mdui-button-icon>
        </mdui-list-item>`)}
    </mdui-list>` : html`
    <mdui-list>
      <mdui-list-item>No credentials found.</mdui-list-item>
    </mdui-list>`;
  render(creds, list);
};
  1. في السطر التالي، استدعِ الدالة renderCredentials() لعرض مفاتيح المرور المسجّلة فور وصول المستخدم إلى الصفحة /home كعملية تهيئة.

views/home.html

renderCredentials();

إنشاء مفتاح مرور وتسجيله

لإنشاء مفتاح مرور وتسجيله، عليك استدعاء الدالة registerCredential() التي نفّذتها سابقًا.

لتفعيل الدالة registerCredential() عند النقر على الزر إنشاء مفتاح مرور، اتّبِع الخطوات التالية:

  1. في ملف بعد عنصر نائب HTML، ابحث عن عبارة import التالية:

views/home.html

import { 
  $, 
  _fetch, 
  loading, 
  updateCredential, 
  unregisterCredential, 
} from '/client.js';
  1. في نهاية نص عبارة import، أضِف الدالة registerCredential().

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
  registerCredential
} from '/client.js';
  1. في نهاية الملف بعد التعليق ذي الصلة، حدِّد الدالة register() التي تستدعي الدالة registerCredential() وواجهة مستخدم التحميل، واستدعِ الدالة renderCredentials() بعد التسجيل. يوضّح ذلك أنّ المتصفّح ينشئ مفتاح مرور ويعرض رسالة خطأ عند حدوث مشكلة.

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  1. في نص الدالة register()، يمكنك رصد الاستثناءات. يعرض الإجراء navigator.credentials.create() الخطأ InvalidStateError عندما يكون مفتاح مرور متوفّرًا على الجهاز. يتم فحص ذلك باستخدام مصفوفة excludeCredentials. في هذه الحالة، يمكنك عرض رسالة ذات صلة للمستخدم. ويعرض أيضًا الخطأ NotAllowedError عندما يلغي المستخدم مربّع حوار المصادقة. يمكنك تجاهله في هذه الحالة.

views/home.html

  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};
  1. في السطر الذي يلي الدالة register()، اربط الدالة register() بحدث click للزر إنشاء مفتاح مرور.

views/home.html

createPasskey.addEventListener('click', register);

مراجعة رمز الحلّ لهذا القسم

views/home.html

<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3>Your registered passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mdui-button id="create-passkey" icon="fingerprint" type="button">Create a passkey</mdui-button>

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import { 
  $, 
  _fetch, 
  loading, 
  updateCredential, 
  unregisterCredential, 
  registerCredential 
} from '/client.js';

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');

// Is WebAuthn available in this browser?
if (window.PublicKeyCredential &&
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
  PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    // Is conditional UI available in this browser?
    if (capabilities.conditionalGet === true &&
      capabilities.passkeyPlatformAuthenticator === true) {
      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {
      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {
  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.

async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
    <mdui-list>
      ${res.map(cred => html`
        <mdui-list-item>
          ${cred.name || 'Unnamed'}
          <mdui-button-icon data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed'}" @click="${rename}" icon="edit" slot="end-icon"></mdui-button-icon>
          <mdui-button-icon data-cred-id="${cred.id}" @click="${remove}" icon="delete" slot="end-icon"></mdui-button-icon>
        </mdui-list-item>`)}
    </mdui-list>` : html`
    <mdui-list>
      <mdui-list-item>No credentials found.</mdui-list-item>
    </mdui-list>`}`;
  render(creds, list);
};

renderCredentials();

// TODO: Add an ability to create a passkey: Create and register a passkey.

async function register() {
  try {
    // Start the loading UI.
    loading.start();
    // Start creating a passkey.
    await registerCredential();
    // Stop the loading UI.
    loading.stop();
    // Render the updated passkey list.
    renderCredentials();
  } catch (e) {
    // Stop the loading UI.
    loading.stop();
    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');
      // A NotAllowedError indicates the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      return;
      // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};

createPasskey.addEventListener('click', register);

التجربة الآن

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

لتجربة هذه الميزة، اتّبِع الخطوات التالية:

  1. على الموقع الإلكتروني، سجِّل الدخول باستخدام اسم مستخدم وكلمة مرور عشوائيين.
  2. انقر على إنشاء مفتاح مرور.
  3. أثبِت هويتك باستخدام قفل شاشة الجهاز.
  4. تأكَّد من تسجيل مفتاح مرور وظهوره ضمن قسم مفاتيح المرور المسجّلة في صفحة الويب.

مفاتيح المرور المسجَّلة والمدرَجة في الصفحة /home

إعادة تسمية مفاتيح المرور المسجّلة وإزالتها

من المفترض أن تتمكّن من إعادة تسمية مفاتيح المرور المسجّلة أو حذفها من القائمة. يمكنك الاطّلاع على طريقة عملها في الرمز البرمجي الذي يتضمّنه دليل التدريب العملي.

في Chrome، يمكنك إزالة مفاتيح المرور المسجّلة من chrome://settings/passkeys على الكمبيوتر أو من مدير كلمات المرور في الإعدادات على Android.

للحصول على معلومات حول كيفية إعادة تسمية مفاتيح المرور المسجّلة وإزالتها على المنصات الأخرى، يُرجى الاطّلاع على صفحات الدعم الخاصة بهذه المنصات.

5- إضافة إمكانية المصادقة باستخدام مفتاح مرور

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

إنشاء الدالة authenticate()

  • في ملف public/client.js بعد التعليق ذي الصلة، أنشئ دالة باسم authenticate() تتحقّق من المستخدم محليًا ثم من الخادم:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.

};

الحصول على التحدي والخيارات الأخرى من نقطة نهاية الخادم

قبل أن تطلب من المستخدم المصادقة، عليك طلب مَعلمات لتمريرها في WebAuthn من الخادم، بما في ذلك سؤال التحقّق.

  • في نص الدالة authenticate() بعد التعليق ذي الصلة، استدعِ الدالة _fetch() لإرسال طلب POST إلى الخادم:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.

// Base64URL decode the challenge.
const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);

تم تصميم خادم هذا الدرس التطبيقي حول الترميز لعرض JSON مشابه قدر الإمكان لقائمة PublicKeyCredentialRequestOptions التي يتم تمريرها إلى واجهة برمجة التطبيقات navigator.credentials.get() WebAuthn. يتضمّن مقتطف الرمز التالي أمثلة على الخيارات التي من المفترض أن تتلقّاها:

{
  "challenge": *****,
  "rpId": "localhost",
  "allowCredentials": []
}

الجدول التالي ليس شاملاً، ولكنّه يحتوي على المَعلمات المهمة في قاموس PublicKeyCredentialRequestOptions:

المعلّمات

الأوصاف

challenge

تحدٍ من إنشاء الخادم في عنصر ArrayBuffer هذا الإجراء مطلوب لمنع هجمات إعادة الإرسال. لا تقبل التحدي نفسه في ردّ مرتين.

rpId

رقم تعريف الجهة الاعتمادية هو نطاق. يمكن للموقع الإلكتروني تحديد نطاقه أو لاحقة قابلة للتسجيل. يجب أن تتطابق هذه القيمة مع المَعلمة rp.id المستخدَمة عند إنشاء مفتاح المرور.

allowCredentials

تُستخدَم هذه السمة للعثور على أدوات مصادقة مؤهَّلة لهذه المصادقة. يمكنك تمرير مصفوفة فارغة أو تركها بدون تحديد للسماح للمتصفّح بعرض أداة اختيار الحساب. مزيد من المعلومات حول سلوك allowCredentials

userVerification

اضبطها على القيمة "preferred" أو احذفها لأنّها القيمة التلقائية. يشير هذا الحقل إلى ما إذا كان التحقّق من هوية المستخدم باستخدام قفل شاشة الجهاز "required" أو "preferred" أو "discouraged". عند ضبط القيمة على "preferred"، سيُطلب من المستخدم إثبات هويته عندما يكون الجهاز متوافقًا. مزيد من المعلومات حول سلوك التحقّق من المستخدم

التحقّق من هوية المستخدم محليًا والحصول على بيانات اعتماد

  1. في نص الدالة authenticate() بعد التعليق ذي الصلة، أعِد تحويل المَعلمة challenge إلى ثنائية:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
  1. مرِّر مصفوفة فارغة إلى المَعلمة allowCredentials لفتح أداة اختيار الحساب عند مصادقة المستخدم:

public/client.js

// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];

يستخدم أداة اختيار الحساب معلومات المستخدم المخزّنة مع مفتاح المرور.

  1. استدعِ طريقة navigator.credentials.get() مع الخيار mediation: 'conditional':

public/client.js

// Invoke the WebAuthn get() method.
const cred = await navigator.credentials.get({
  publicKey: options,

  // Request a conditional UI.
  mediation: 'conditional'
});

يطلب هذا الخيار من المتصفّح اقتراح مفاتيح مرور بشكل مشروط كجزء من ميزة "الملء التلقائي للنماذج".

التحقّق من بيانات الاعتماد

بعد أن يثبت المستخدم هويته محليًا، من المفترض أن تتلقّى عنصر بيانات اعتماد يحتوي على توقيع يمكنك التحقّق منه على الخادم.

يتضمّن مقتطف الرمز التالي مثالاً على الكائن PublicKeyCredential:

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "authenticatorData": *****,
    "signature": *****,
    "userHandle": *****
  },
  authenticatorAttachment: "platform"
}

الجدول التالي ليس شاملاً، ولكنه يتضمّن المَعلمات المهمة في العنصر PublicKeyCredential:

المعلّمات

الأوصاف

id

رقم التعريف المشفّر Base64URL لبيانات اعتماد مفتاح المرور التي تمّت المصادقة عليها.

rawId

تمثّل هذه السمة إصدار العنصر من معرّف بيانات الاعتماد.ArrayBuffer

response.clientDataJSON

عنصر ArrayBuffer من بيانات العميل يحتوي هذا الحقل على معلومات، مثل اختبار التحقّق والمصدر الذي يحتاج خادم RP إلى التحقّق منه.

response.authenticatorData

عنصر ArrayBuffer من بيانات المصادقة يحتوي هذا الحقل على معلومات مثل رقم تعريف الشريك الترويجي.

response.signature

عنصر ArrayBuffer خاص بالتوقيع هذه القيمة هي أساس بيانات الاعتماد ويجب التحقّق منها على الخادم.

response.userHandle

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

authenticatorAttachment

تعرض هذه السمة السلسلة "platform" عندما تأتي بيانات الاعتماد هذه من الجهاز المحلي. بخلاف ذلك، يتم عرض السلسلة "cross-platform"، لا سيما عندما يستخدم المستخدم هاتفًا لتسجيل الدخول. إذا كان على المستخدم استخدام هاتف لتسجيل الدخول، اطلب منه إنشاء مفتاح مرور على الجهاز المحلي.

لإرسال عنصر بيانات الاعتماد إلى الخادم، اتّبِع الخطوات التالية:

  1. في نص الدالة authenticate() بعد التعليق ذي الصلة، يجب ترميز المَعلمات الثنائية لبيانات الاعتماد حتى يمكن تسليمها إلى الخادم كسلسلة. يمكنك استخدام .toJSON() لإجراء ذلك:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Verify the credential.
// Encode and serialize the `PublicKeyCredential`.
const credential = JSON.stringify(cred);
  1. أرسِل العنصر إلى الخادم:

public/client.js

return await _fetch(`/auth/signinResponse`, credential);

عند تشغيل البرنامج، يعرض الخادم HTTP code 200، ما يشير إلى أنّه تم التحقّق من بيانات الاعتماد.

أصبحت لديك الآن الدالة الكاملة authentication().

مراجعة رمز الحلّ لهذا القسم

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the 
  challenge and other options from the server endpoint.
  const options = await _fetch('/auth/signinRequest');

  // TODO: Add an ability to authenticate with a passkey: Locally verify 
  the user and get a credential.
  // Base64URL decode the challenge.
  options.challenge = base64url.decode(options.challenge);

  // The empty allowCredentials array invokes an account selector 
  by discoverable credentials.
  options.allowCredentials = [];

  // Invoke the WebAuthn get() function.
  const cred = await navigator.credentials.get({
    publicKey: options,

    // Request a conditional UI.
    mediation: 'conditional'
  });

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const authenticatorData = 
  base64url.encode(cred.response.authenticatorData);
  const signature = base64url.encode(cred.response.signature);
  const userHandle = base64url.encode(cred.response.userHandle);

  credential.response = {
    clientDataJSON,
    authenticatorData,
    signature,
    userHandle,
  };

  return await _fetch(`/auth/signinResponse`, credential);
};

6. إضافة مفاتيح مرور إلى ميزة "الملء التلقائي" في المتصفّح

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

ومع ذلك، لا يتم نقل جميع المستخدمين من كلمة المرور إلى مفتاح المرور في الوقت نفسه. يعني ذلك أنّه لا يمكنك إزالة كلمات المرور إلى أن ينتقل جميع المستخدمين إلى مفاتيح المرور، لذا عليك ترك نموذج تسجيل الدخول المستند إلى كلمة المرور إلى حين ذلك. مع ذلك، إذا تركت نموذج كلمة مرور وزر مفتاح مرور، سيضطر المستخدمون إلى الاختيار بينهما لتسجيل الدخول. من الناحية المثالية، يجب أن تكون عملية تسجيل الدخول بسيطة.

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

d616744939063451.png

تفعيل واجهة مستخدم شرطية

لتفعيل واجهة مستخدم شرطية، ما عليك سوى إضافة الرمز المميّز webauthn في السمة autocomplete لحقل إدخال. بعد ضبط الرمز المميّز، يمكنك استدعاء الطريقة navigator.credentials.get() باستخدام السلسلة mediation: 'conditional' لتشغيل واجهة مستخدم قفل الشاشة بشكل مشروط.

  • لتفعيل واجهة مستخدم شرطية، استبدِل حقول إدخال اسم المستخدم الحالية بملف HTML التالي بعد التعليق ذي الصلة في ملف view/index.html:

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<mdui-text-field id="username" label="Username" name="username" autocomplete="username webauthn" autofocus></mdui-text-field>

رصد الميزات واستدعاء WebAuthn وتفعيل واجهة مستخدم شرطية

  1. في ملف view/index.html بعد التعليق ذي الصلة، استبدِل عبارة import الحالية بالرمز التالي:

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
  $,
  _fetch,
  loading,
  authenticate 
} from "/client.js";

يستورد هذا الرمز الدالة authenticate() التي نفّذتها سابقًا.

  1. تأكَّد من توفّر العنصر window.PulicKeyCredential ومن أنّ الطريقة PublicKeyCredential.isConditionalMediationAvailable() تعرض القيمة true، ثم استدعِ الدالة authenticate():

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
if (window.PublicKeyCredential &&
    PublicKeyCredential.getClientCapabilities) {
  try {

    // Is conditional UI available in this browser?
      const capabilities = await PublicKeyCredential.getClientCapabilities();
      if (capabilities.conditionalGet) {

      // If conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $("#username").value = user.username;
        loading.start();
        location.href = "/home";
      } else {
        throw new Error("User not found.");
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== "NotAllowedError") {
      console.error(e);
      alert(e.message);
    }
  }
}

مراجعة رمز الحلّ لهذا القسم

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<mdui-text-field id="username" label="Username" name="username" autocomplete="username webauthn" autofocus></mdui-text-field>

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import { 
  $, 
  _fetch, 
  loading, 
  authenticate 
} from '/client.js';

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.        

// Is WebAuthn available on this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.getClientCapabilities) {
  try {
    // Is conditional UI available in this browser?
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    if (capabilities.conditionalGet) {
      // If conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {
        // Proceed only when authentication succeeds.
        $('#username').value = user.username;
        loading.start();
        location.href = '/home';
      } else {
        throw new Error('User not found.');
      }
    }
  } catch (e) {
    loading.stop();
    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== 'NotAllowedError') {
      console.error(e);
      alert(e.message);
    }
  }
}

التجربة الآن

نفّذت عملية إنشاء مفاتيح المرور وتسجيلها وعرضها والمصادقة عليها على موقعك الإلكتروني.

لتجربة هذه الميزة، اتّبِع الخطوات التالية:

  1. انتقِل إلى علامة التبويب "المعاينة".
  2. سجِّل الخروج إذا لزم الأمر.
  3. انقر على مربّع نص اسم المستخدم. يظهر مربّع حوار.
  4. اختَر الحساب الذي تريد تسجيل الدخول باستخدامه.
  5. أثبِت هويتك باستخدام قفل شاشة الجهاز. تتم إعادة توجيهك إلى صفحة /home وتسجيل الدخول.

مربّع حوار يطلب منك إثبات هويتك باستخدام كلمة المرور أو مفتاح المرور المحفوظَين

7. تهانينا!

لقد أكملت هذا الدرس التطبيقي حول الترميز. إذا كانت لديك أي أسئلة، يمكنك طرحها على القائمة البريدية FIDO-DEV أو على StackOverflow باستخدام العلامة passkey.

مزيد من المعلومات