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

1. قبل البدء

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

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

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

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

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

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

المتطلبات

إحدى مجموعات الأجهزة التالية:

  • متصفّح Google Chrome على جهاز Android يعمل بالإصدار 9 من نظام التشغيل Android أو إصدار أحدث، ويُفضّل أن يكون مزوّدًا بمستشعر بيومتري
  • متصفّح Chrome على جهاز Windows يعمل بنظام التشغيل Windows 10 أو إصدار أحدث
  • متصفّح Safari 16 أو إصدار أحدث على هاتف iPhone يعمل بالإصدار 16 من نظام التشغيل iOS أو إصدار أحدث، أو جهاز iPad يعمل بالإصدار 16 من نظام التشغيل iPadOS أو إصدار أحدث
  • متصفّح Safari 16 أو إصدار أحدث أو Chrome على جهاز مكتبي من Apple يعمل بنظام التشغيل macOS Ventura أو إصدار أحدث

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

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

افتح المشروع

  1. افتح المشروع في Glitch.
  2. انقر على إنشاء ريمكس لإنشاء نسخة من مشروع Glitch.
  3. في قائمة التنقّل في أسفل Glitch، انقر على معاينة > المعاينة في نافذة جديدة. سيتم فتح علامة تبويب أخرى في المتصفّح.

زر "المعاينة في نافذة جديدة" في قائمة التنقّل في أسفل Glitch

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

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

في هذه الحالة، على المستخدمين إدخال كلمة مرور في كل مرة يسجّلون فيها الدخول. يمكنك إضافة ميزة مفتاح المرور إلى هذا النموذج ليتمكّن المستخدمون من تسجيل الدخول باستخدام وظيفة قفل الشاشة على الجهاز. يمكنك تجربة الحالة النهائية على https://passkeys-codelab.glitch.me/.

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

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

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

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

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

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

  1. في Glitch، انتقِل إلى الملف public/client.js ثم انتقِل إلى النهاية.
  2. بعد التعليق ذي الصلة، أضِف الدالة 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:

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);

if (options.excludeCredentials) {
  for (let cred of options.excludeCredentials) {
    cred.id = base64url.decode(cred.id);
  }
}
  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 حتى يمكن تسليمها إلى الخادم كسلسلة:

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
  credential.authenticatorAttachment = cred.authenticatorAttachment;
}

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject = base64url.encode(cred.response.attestationObject);

// Obtain transports.
const transports = cred.response.getTransports ? cred.response.getTransports() : [];

credential.response = {
  clientDataJSON,
  attestationObject,
  transports
};
  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 server endpoint.
  const options = await _fetch('/auth/registerRequest');
  
  // TODO: Add an ability to create a passkey: Create a credential.
  // Base64URL decode some values.

  options.user.id = base64url.decode(options.user.id);
  options.challenge = base64url.decode(options.challenge);

  if (options.excludeCredentials) {
    for (let cred of options.excludeCredentials) {
      cred.id = base64url.decode(cred.id);
    }
  }

  // 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.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
  if (cred.authenticatorAttachment) {
    credential.authenticatorAttachment = cred.authenticatorAttachment;
  }

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const attestationObject =  
  base64url.encode(cred.response.attestationObject);

  // Obtain transports.
  const transports = cred.response.getTransports ? 
  cred.response.getTransports() : [];

  credential.response = {
    clientDataJSON,
    attestationObject,
    transports
  };

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

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

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

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

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

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

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3 class="mdc-typography mdc-typography--headline6"> Your registered 
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-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 results = await Promise.all([

    // Is platform authenticator available in this browser?
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

    // Is conditional UI available in this browser?
    PublicKeyCredential.isConditionalMediationAvailable()
  ]);
  1. إذا تم استيفاء جميع الشروط، اعرض الزر لإنشاء مفتاح مرور. بخلاف ذلك، اعرض رسالة تحذير.

views/home.html

    if (results.every(r => r === 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.';
}

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

  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 = html`${res.length > 0 ? html`
    <mwc-list>
      ${res.map(cred => html`
        <mwc-list-item>
          <div class="list-item">
            <div class="entity-name">
              <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}"  
            data-name="${cred.name || 'Unnamed' }" @click="${rename}"  
            icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" 
            icon="delete"></mwc-icon-button>
          </div>
         </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-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 class="mdc-typography mdc-typography--headline6"> Your registered  
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-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');

// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const results = await Promise.all([

      // Is platform authenticator available in this browser?
      PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

      // Is conditional UI available in this browser?
      PublicKeyCredential.isConditionalMediationAvailable()
    ]);
    if (results.every(r => r === 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`
  <mwc-list>
    ${res.map(cred => html`
      <mwc-list-item>
        <div class="list-item">
          <div class="entity-name">
            <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed' }" @click="${rename}" icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" icon="delete"></mwc-icon-button>
          </div>
        </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-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 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);
    }
  }
};

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.
const options = await _fetch('/auth/signinRequest');

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

{
  "challenge": *****,
  "rpId": "passkeys-codelab.glitch.me",
  "allowCredentials": []
}

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

المعلّمات

الأوصاف

challenge

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

rpId

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

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() بعد التعليق ذي الصلة، يجب ترميز المَعلمات الثنائية لبيانات الاعتماد حتى يمكن تسليمها إلى الخادم كسلسلة:

public/client.js

// 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,
};
  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 التي تتيح لك إنشاء حقل إدخال نموذج لاقتراح مفتاح مرور كجزء من عناصر الملء التلقائي بالإضافة إلى كلمات المرور. إذا نقر المستخدم على مفتاح مرور في اقتراحات الملء التلقائي، سيُطلب منه استخدام قفل شاشة الجهاز لإثبات هويته محليًا. هذه تجربة سلسة للمستخدم لأنّ الإجراء الذي يتّخذه المستخدم مطابق تقريبًا لإجراء تسجيل الدخول باستخدام كلمة المرور.

مفتاح مرور مقترَح كجزء من ميزة الملء التلقائي للنماذج

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

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

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

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus />

رصد الميزات واستدعاء 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.isConditionalMediationAvailable
) {
  try {

    // Is conditional UI available in this browser?
    const cma =
      await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // 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. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus 
/>

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 avaiable in this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {

    // Is a conditional UI available in this browser?
    const cma= await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If a 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.

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