Get your free and exclusive 80-page Banking Passkey Report
passkey tutorial how to implement passkeys

دليل تعليمي حول مفاتيح المرور: كيفية تطبيقها في تطبيقات الويب

يشرح هذا الدليل التعليمي كيفية تطبيق مفاتيح المرور في تطبيق الويب الخاص بك. نستخدم Node.js (TypeScript)، وSimpleWebAuthn، وVanilla HTML / JavaScript، وMySQL.

Vincent Delitz

Vincent

Created: June 17, 2025

Updated: June 20, 2025


We aim to make the Internet a safer place using passkeys. That's why we want to support developers with tutorials on how to implement passkeys.

1. مقدمة: كيفية تطبيق مفاتيح المرور#

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

Demo Icon

Want to try passkeys yourself in a passkeys demo?

Try Passkeys

إن امتلاك نظام مصادقة حديث وقوي و سهل الاستخدام هو أمر أساسي عندما ترغب في بناء موقع ويب أو تطبيق رائع. لقد ظهرت مفاتيح المرور كحل لهذا التحدي. وبصفتها المعيار الجديد لعمليات تسجيل الدخول، فإنها تبشر بمستقبل خالٍ من عيوب كلمات المرور التقليدية، مما يوفر تجربة تسجيل دخول بدون كلمة مرور حقيقية (وهي ليست آمنة فحسب، بل مريحة للغاية أيضًا).

ما يعبر حقًا عن إمكانات مفاتيح المرور هو الدعم الذي حظيت به. فقد قامت كل المتصفحات الرئيسية، سواء كانت Chrome أو Firefox أو Safari أو Edge، وجميع الشركات المصنعة للأجهزة الهامة (Apple، Microsoft، Google) بدمج الدعم لها. هذا التبني الجماعي يوضح أن مفاتيح المرور هي المعيار الجديد لعمليات تسجيل الدخول.

نعم، هناك بالفعل دروس تعليمية حول دمج مفاتيح المرور في تطبيقات الويب. سواء كان ذلك لأطر عمل الواجهة الأمامية مثل React أو Vue.js أو Next.js، هناك وفرة من الأدلة المصممة للتخفيف من التحديات وتسريع عمليات تطبيق مفاتيح المرور الخاصة بك. ومع ذلك، هناك نقص في دليل تعليمي شامل يظل بسيطًا وأساسيًا. لقد تواصل معنا العديد من المطورين وطلبوا دليلًا تعليميًا يوضح تطبيق مفاتيح المرور لتطبيقات الويب.

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

PasskeyAssessment Icon

Get a free passkey assessment in 15 minutes.

Book free consultation

بنهاية هذه الرحلة، ستكون قد بنيت تطبيق ويب بسيطًا وقابلًا للتطبيق، حيث يمكنك:

  • إنشاء مفتاح مرور
  • استخدام مفتاح المرور لتسجيل الدخول

بالنسبة لأولئك الذين في عجلة من أمرهم أو يرغبون في مرجع، فإن الكود الكامل متاح على GitHub.

هل تشعر بالفضول حول شكل النتيجة النهائية؟ إليك نظرة خاطفة على المشروع النهائي (نعترف بأنه يبدو أساسيًا جدًا ولكن الأشياء المثيرة للاهتمام تكمن تحت السطح):

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

StateOfPasskeys Icon

Want to find out how many people use passkeys?

View Adoption Data

كيف أضيف مفاتيح المرور إلى موقع الإنتاج الخاص بي؟

هذا مثال بسيط جدًا لمصادقة مفتاح المرور. الأشياء التالية لم يتم أخذها في الاعتبار / تنفيذها في هذا الدليل التعليمي أو تم تنفيذها بشكل أساسي جدًا:

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

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

Slack Icon

Become part of our Passkeys Community for updates & support.

Join

2. المتطلبات الأساسية لدمج مفاتيح المرور#

قبل الغوص بعمق في تطبيق مفاتيح المرور، دعنا نلقي نظرة على المهارات والأدوات اللازمة. إليك ما تحتاجه للبدء:

2.1 الواجهة الأمامية: Vanilla HTML & JavaScript#

من الضروري وجود فهم قوي لبنات البناء الأساسية للويب HTML و CSS و JavaScript. لقد أبقينا الأمور بسيطة عن قصد، وتجنبنا أي إطار عمل JavaScript حديث واعتمدنا على Vanilla JavaScript / HTML. الشيء الوحيد الأكثر تطورًا الذي نستخدمه هو مكتبة WebAuthn المساعدة @simplewebauthn/browser.

2.2 الواجهة الخلفية: Node.js (Express) في TypeScript + SimpleWebAuthn#

بالنسبة لواجهتنا الخلفية، نستخدم خادم Node.js (Express) مكتوبًا بلغة TypeScript. لقد قررنا أيضًا العمل مع تطبيق خادم WebAuthn من SimpleWebAuthn (@simplewebauthn/server مع @simplewebauthn/typescript-types). هناك العديد من تطبيقات خادم WebAuthn المتاحة، لذا يمكنك بالطبع استخدام أي منها. بما أننا اخترنا خادم WebAuthn بلغة TypeScript، فإن المعرفة الأساسية بـ Node.js و npm مطلوبة.

2.3 قاعدة البيانات: MySQL#

يتم تخزين جميع بيانات المستخدم والمفاتيح العامة لمفاتيح المرور في قاعدة بيانات. لقد اخترنا MySQL كتقنية لقاعدة البيانات. من المفيد أن يكون لديك فهم أساسي لـ MySQL وقواعد البيانات العلائقية، على الرغم من أننا سنرشدك خلال الخطوات الفردية.

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

مع وجود هذه المتطلبات الأساسية، أنت جاهز تمامًا للغوص في عالم مفاتيح المرور.

Ben Gould Testimonial

Ben Gould

Head of Engineering

I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.

يثق أكثر من 10,000 مطور في Corbado ويجعلون الإنترنت أكثر أمانًا باستخدام مفاتيح المرور. هل لديك أسئلة؟ لقد كتبنا أكثر من 150 مقالًا في المدونة حول مفاتيح المرور.

انضم إلى مجتمع مفاتيح المرور

3. نظرة عامة على البنية: مثال تطبيقي لمفاتيح المرور#

قبل الدخول في الكود والإعدادات، دعنا نلقي نظرة على بنية النظام الذي نريد بناءه. إليك تفصيل للبنية التي سنقوم بإعدادها:

  • الواجهة الأمامية (Frontend): تتكون من زرين، أحدهما لتسجيل المستخدم (إنشاء مفتاح مرور) والآخر للمصادقة (تسجيل الدخول باستخدام مفتاح المرور).
  • الجهاز والمتصفح: بمجرد تشغيل إجراء على الواجهة الأمامية، يدخل الجهاز والمتصفح في الصورة. يسهلان إنشاء والتحقق من مفتاح المرور، ويعملان كوسطاء بين المستخدم والواجهة الخلفية.
  • الواجهة الخلفية (Backend): الواجهة الخلفية هي المكان الذي يحدث فيه السحر الحقيقي في تطبيقنا. تتعامل مع جميع الطلبات التي تبدأها الواجهة الأمامية. تتضمن هذه العملية إنشاء والتحقق من مفاتيح المرور. في صميم عمليات الواجهة الخلفية يوجد خادم WebAuthn. على عكس ما قد يوحي به الاسم، فهو ليس خادمًا مستقلاً. بدلاً من ذلك، هو مكتبة أو حزمة تطبق معيار WebAuthn. الوظيفتان الأساسيتان هما: التسجيل (Sign-up) حيث يقوم المستخدمون الجدد بإنشاء مفاتيح المرور الخاصة بهم والمصادقة (Login): حيث يقوم المستخدمون الحاليون بتسجيل الدخول باستخدام مفاتيح المرور الخاصة بهم. في أبسط صوره، يوفر خادم WebAuthn أربع نقاط نهاية API عامة، مقسمة إلى فئتين: اثنتان للتسجيل واثنتان للمصادقة. وهي مصممة لتلقي البيانات بتنسيق معين، والتي تتم معالجتها بعد ذلك بواسطة خادم WebAuthn. خادم WebAuthn مسؤول عن جميع العمليات التشفيرية اللازمة. من الجوانب الأساسية التي يجب ملاحظتها أن نقاط نهاية API هذه يجب أن يتم تقديمها عبر HTTPS.
  • قاعدة بيانات MySQL: تعمل كعمود فقري للتخزين لدينا، قاعدة بيانات MySQL مسؤولة عن الاحتفاظ ببيانات المستخدم وبيانات الاعتماد المقابلة لها.
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

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

يصف المخطط التالي تدفق العملية أثناء التسجيل (sign-up):

يصف المخطط التالي تدفق العملية أثناء المصادقة (login):

علاوة على ذلك، تجد هنا هيكل المشروع (أهم الملفات فقط):

passkeys-tutorial ├── src # يحتوي على كل الكود المصدري للواجهة الخلفية بلغة TypeScript │ ├── controllers # منطق الأعمال لمعالجة أنواع معينة من الطلبات │ │ ├── authentication.ts # منطق مصادقة مفتاح المرور │ │ └── registration.ts # منطق تسجيل مفتاح المرور │ ├── middleware │ │ ├── customError.ts # إضافة رسائل خطأ مخصصة بطريقة موحدة │ │ └── errorHandler.ts # معالج الأخطاء العام │ ├── public │ │ ├── index.html # ملف HTML الرئيسي في الواجهة الأمامية │ │ ├── css │ │ │ └── style.css # تنسيقات أساسية │ │ └── js │ │ └── script.js # منطق JavaScript (بما في ذلك WebAuthn API) │ ├── routes # تعريفات مسارات API ومعالجاتها │ │ └── routes.ts # مسارات محددة لمفاتيح المرور │ ├── services │ │ ├── credentialService.ts# يتفاعل مع جدول بيانات الاعتماد │ │ └── userService.ts # يتفاعل مع جدول المستخدمين │ ├── utils # دوال وأدوات مساعدة │ | ├── constants.ts # بعض الثوابت (مثل rpID) │ | └── utils.ts # دالة مساعدة │ ├── database.ts # ينشئ الاتصال من Node.js إلى قاعدة بيانات MySQL │ ├── index.ts # نقطة الدخول لخادم Node.js │ └── server.ts # يدير جميع إعدادات الخادم ├── config.json # بعض الإعدادات لمشروع Node.js ├── docker-compose.yml # يحدد الخدمات والشبكات والمجلدات لحاويات Docker ├── Dockerfile # ينشئ صورة Docker للمشروع ├── init-db.sql # يحدد مخطط قاعدة بيانات MySQL الخاصة بنا ├── package.json # يدير تبعيات مشروع Node.js والبرامج النصية └── tsconfig.json # يكوّن كيفية قيام TypeScript بترجمة الكود الخاص بك

4. إعداد قاعدة بيانات MySQL#

عند تطبيق مفاتيح المرور، يعد إعداد قاعدة البيانات مكونًا رئيسيًا. يستخدم نهجنا حاوية Docker تشغل MySQL، مما يوفر بيئة مباشرة ومعزولة ضرورية للاختبار والنشر الموثوق به.

مخطط قاعدة البيانات لدينا بسيط عن قصد، ويضم جدولين فقط. تساعد هذه البساطة في فهم أوضح وصيانة أسهل.

هيكل الجدول المفصل

1. جدول بيانات الاعتماد (Credentials Table): هذا الجدول مركزي لمصادقة مفتاح المرور، ويخزن بيانات اعتماد مفتاح المرور. الأعمدة الحرجة:

  • credential_id: معرف فريد لكل بيانات اعتماد. يعد اختيار نوع البيانات الصحيح لهذا الحقل أمرًا حيويًا لتجنب أخطاء التنسيق.
  • public_key: يخزن المفتاح العام لكل بيانات اعتماد. كما هو الحال مع credential_id، فإن نوع البيانات والتنسيق المناسبين أمران حاسمان.

2. جدول المستخدمين (Users Table): يربط حسابات المستخدمين ببيانات الاعتماد المقابلة لها.

لاحظ أننا أطلقنا على الجدول الأول اسم credentials لأن هذا يتوافق مع خبرتنا وما توصي به المكتبات الأخرى بأنه أكثر ملاءمة (على عكس اقتراح SimpleWebAuthn بتسميته authenticator أو authenticator_device).

تعد أنواع البيانات لـ credential_id و public_key حاسمة. غالبًا ما تنشأ الأخطاء من أنواع البيانات غير الصحيحة أو الترميز أو التنسيق (خاصة الفرق بين Base64 و Base64URL هو سبب شائع للأخطاء)، مما قد يعطل عملية التسجيل (sign-up) أو المصادقة (login) بأكملها.

جميع أوامر SQL اللازمة لإعداد هذه الجداول موجودة في ملف init-db.sql. يضمن هذا البرنامج النصي تهيئة قاعدة بيانات سريعة وخالية من الأخطاء.

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

init-db.sql
CREATE TABLE users ( id VARCHAR(255) PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE ); CREATE TABLE credentials ( id INT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(255) NOT NULL, credential_id VARCHAR(255) NOT NULL, public_key TEXT NOT NULL, counter INT NOT NULL, transports VARCHAR(255), FOREIGN KEY (user_id) REFERENCES users (id) );

بعد أن أنشأنا هذا الملف، ننشئ ملف docker-compose.yml جديدًا على المستوى الجذر للمشروع:

docker-compose.yml
version: "3.1" services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: my-secret-pw MYSQL_DATABASE: webauthn_db ports: - "3306:3306" volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql

يبدأ هذا الملف قاعدة بيانات MySQL على المنفذ 3306 وينشئ هيكل قاعدة البيانات المحدد. من المهم ملاحظة أن الاسم وكلمة المرور لقاعدة البيانات المستخدمة هنا بسيطان لأغراض العرض التوضيحي. في بيئة الإنتاج، يجب عليك استخدام بيانات اعتماد أكثر تعقيدًا لتعزيز الأمان.

بعد ذلك، ننتقل إلى تشغيل حاوية Docker الخاصة بنا. في هذه المرحلة، يتضمن ملف docker-compose.yml الخاص بنا هذه الحاوية الواحدة فقط، لكننا سنضيف المزيد من المكونات لاحقًا. لبدء الحاوية، استخدم الأمر التالي:

docker compose up -d

بمجرد تشغيل الحاوية، نحتاج إلى التحقق مما إذا كانت قاعدة البيانات تعمل كما هو متوقع. افتح طرفية ونفذ الأمر التالي للتفاعل مع قاعدة بيانات MySQL:

docker exec -it <container ID> mysql -uroot -p

سيُطلب منك إدخال كلمة مرور الجذر، وهي my-secret-pw في مثالنا. بعد تسجيل الدخول، حدد قاعدة بيانات webauthn_db واعرض الجداول باستخدام هذه الأوامر:

use webauthn_db; show tables;

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

5. تطبيق مفاتيح المرور: خطوات تكامل الواجهة الخلفية#

الواجهة الخلفية هي جوهر أي تطبيق لمفاتيح المرور، حيث تعمل كمركز مركزي لمعالجة طلبات مصادقة المستخدم من الواجهة الأمامية. تتواصل مع مكتبة خادم WebAuthn للتعامل مع طلبات التسجيل (sign-up) والمصادقة (login)، وتتفاعل مع قاعدة بيانات MySQL لتخزين واسترداد بيانات اعتماد المستخدم. أدناه، سنرشدك خلال إعداد الواجهة الخلفية باستخدام Node.js (Express) مع TypeScript والذي سيكشف عن واجهة برمجة تطبيقات عامة (public API) للتعامل مع جميع الطلبات.

5.1 تهيئة خادم Node.js (Express)#

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

قم بتشغيل الأمر

npx create-express-typescript-application passkeys-tutorial

ينشئ هذا هيكل كود أساسي لتطبيق Node.js (Express) مكتوب بلغة TypeScript يمكننا استخدامه لمزيد من التعديلات.

يتطلب مشروعك العديد من الحزم الرئيسية التي نحتاج إلى تثبيتها بالإضافة إلى ذلك:

  • @simplewebauthn/server: مكتبة من جانب الخادم لتسهيل عمليات WebAuthn، مثل تسجيل المستخدم (sign-up) والمصادقة (login).
  • express-session: برنامج وسيط لـ Express.js لإدارة الجلسات، وتخزين بيانات الجلسة من جانب الخادم والتعامل مع ملفات تعريف الارتباط.
  • uuid: أداة لإنشاء معرفات فريدة عالميًا (UUIDs)، تُستخدم بشكل شائع لإنشاء مفاتيح أو معرفات فريدة في التطبيقات.
  • mysql2: عميل Node.js لـ MySQL، يوفر إمكانيات للاتصال وتنفيذ الاستعلامات على قواعد بيانات MySQL.

انتقل إلى الدليل الجديد وقم بتثبيتها بالأوامر التالية (نقوم أيضًا بتثبيت أنواع TypeScript المطلوبة):

cd passkeys-tutorial npm install @simplewebauthn/server mysql2 uuid express-session @types/express-session @types/uuid

للتأكد من تثبيت كل شيء بشكل صحيح، قم بتشغيل

npm run dev:nodemon

يجب أن يبدأ هذا خادم Node.js الخاص بك في وضع التطوير مع Nodemon، والذي يعيد تشغيل الخادم تلقائيًا عند أي تغييرات في الملفات.

نصيحة لاستكشاف الأخطاء وإصلاحها: إذا واجهت أخطاء، حاول تحديث ts-node إلى الإصدار 10.8.1 في ملف package.json ثم قم بتشغيل npm i لتثبيت التحديثات.

يحتوي ملف server.ts الخاص بك على الإعداد الأساسي والبرامج الوسيطة لتطبيق Express. لدمج وظائف مفتاح المرور، ستحتاج إلى إضافة:

  • المسارات (Routes): تحديد مسارات جديدة لتسجيل مفتاح المرور (sign-up) والمصادقة (login).
  • المتحكمات (Controllers): إنشاء متحكمات للتعامل مع منطق هذه المسارات.
  • البرامج الوسيطة (Middleware): دمج البرامج الوسيطة لمعالجة الطلبات والأخطاء.
  • الخدمات (Services): بناء خدمات لاسترداد وتخزين البيانات في قاعدة البيانات.
  • الدوال المساعدة (Utility Functions): تضمين دوال مساعدة لعمليات الكود الفعالة.

هذه التحسينات هي مفتاح تمكين مصادقة مفتاح المرور في الواجهة الخلفية لتطبيقك. سنقوم بإعدادها لاحقًا.

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

5.2 اتصال قاعدة بيانات MySQL#

بعد أن أنشأنا وبدأنا قاعدة البيانات في القسم 4، نحتاج الآن إلى التأكد من أن الواجهة الخلفية لدينا يمكنها الاتصال بقاعدة بيانات MySQL. لذلك، ننشئ ملف database.ts جديدًا في مجلد /src ونضيف المحتوى التالي:

database.ts
import mysql from "mysql2"; // Create a MySQL pool const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0, }); // Promisify for Node.js async/await. export const promisePool = pool.promise();

سيتم استخدام هذا الملف لاحقًا بواسطة خادمنا للوصول إلى قاعدة البيانات.

5.3 تكوين خادم التطبيق#

دعنا نلقي نظرة سريعة على ملف config.json الخاص بنا، حيث تم تعريف متغيرين بالفعل: المنفذ الذي نشغل عليه التطبيق والبيئة:

config.json
{ "PORT": 8080, "NODE_ENV": "development" }

يمكن أن يبقى package.json كما هو ويجب أن يبدو كالتالي:

package.json
{ "name": "passkeys-tutorial", "version": "0.0.1", "description": "passkeys-tutorial initialised with create-express-typescript-application.", "main": "src/index.ts", "scripts": { "build": "tsc", "start": "node ./build/src/index.js", "dev": "ts-node ./src/index.ts", "dev:nodemon": "nodemon -w src -e ts,json -x ts-node ./src/index.ts", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": ["express", "typescript"], "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/morgan": "^1.9.9", "@types/node": "^14.18.63", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "eslint": "^7.32.0", "nodemon": "^2.0.22", "ts-node": "^10.8.1", "typescript": "^4.9.5" }, "dependencies": { "@simplewebauthn/server": "^8.3.5", "@types/express-session": "^1.17.10", "@types/uuid": "^9.0.7", "cors": "^2.8.5", "env-cmd": "^10.1.0", "express": "^4.18.2", "express-session": "^1.17.3", "fs": "^0.0.1-security", "helmet": "^4.6.0", "morgan": "^1.10.0", "mysql2": "^3.6.5", "uuid": "^9.0.1" } }

index.ts يبدو كالتالي:

index.ts
import app from "./server"; import config from "../config.json"; // Start the application by listening to specific port const port = Number(process.env.PORT || config.PORT || 8080); app.listen(port, () => { console.info("Express application started on port: " + port); });

في server.ts، نحتاج إلى تكييف بعض الأشياء الأخرى. علاوة على ذلك، هناك حاجة إلى ذاكرة تخزين مؤقت مؤقتة من نوع ما (مثل redis أو memcache أو express-session) لتخزين التحديات المؤقتة التي يمكن للمستخدمين المصادقة عليها. قررنا استخدام express-session والإعلان عن وحدة express-session في الأعلى لجعل الأمور تعمل مع express-session. بالإضافة إلى ذلك، نقوم بتبسيط التوجيه وإزالة معالجة الأخطاء في الوقت الحالي (سيتم إضافتها إلى البرنامج الوسيط لاحقًا):

server.ts
import express, { Express } from "express"; import morgan from "morgan"; import helmet from "helmet"; import cors from "cors"; import config from "../config.json"; import { router as passkeyRoutes } from "./routes/routes"; import session from "express-session"; const app: Express = express(); declare module "express-session" { interface SessionData { currentChallenge?: string; loggedInUserId?: string; } } /************************************************************************************ * Basic Express Middlewares ***********************************************************************************/ app.set("json spaces", 4); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use( session({ // @ts-ignore secret: process.env.SESSION_SECRET, saveUninitialized: true, resave: false, cookie: { maxAge: 86400000, httpOnly: true, // Ensure to not expose session cookies to clientside scripts }, }), ); // Handle logs in console during development if (process.env.NODE_ENV === "development" || config.NODE_ENV === "development") { app.use(morgan("dev")); app.use(cors()); } // Handle security and origin in production if (process.env.NODE_ENV === "production" || config.NODE_ENV === "production") { app.use(helmet()); } /************************************************************************************ * Register all routes ***********************************************************************************/ app.use("/api/passkey", passkeyRoutes); app.use(express.static("src/public")); export default app;

5.4 خدمة بيانات الاعتماد وخدمة المستخدم#

لإدارة البيانات في جدولينا اللذين تم إنشاؤهما بفعالية، سنقوم بتطوير خدمتين متميزتين في دليل src/services جديد: authenticatorService.ts و userService.ts.

ستحتوي كل خدمة على أساليب CRUD (إنشاء، قراءة، تحديث، حذف)، مما يمكننا من التفاعل مع قاعدة البيانات بطريقة معيارية ومنظمة. ستسهل هذه الخدمات تخزين واسترداد وتحديث البيانات في جدولي المصادق والمستخدم. إليك كيف يجب أن يكون هيكل هذه الملفات المطلوبة:

userService.ts يبدو كالتالي:

userService.ts
import { promisePool } from "../database"; // Adjust the import path as necessary import { v4 as uuidv4 } from "uuid"; export const userService = { async getUserById(userId: string) { const [rows] = await promisePool.query("SELECT * FROM users WHERE id = ?", [ userId, ]); // @ts-ignore return rows[0]; }, async getUserByUsername(username: string) { try { const [rows] = await promisePool.query( "SELECT * FROM users WHERE username = ?", [username], ); // @ts-ignore return rows[0]; } catch (error) { return null; } }, async createUser(username: string) { const id = uuidv4(); await promisePool.query("INSERT INTO users (id, username) VALUES (?, ?)", [ id, username, ]); return { id, username }; }, };

credentialService.ts يبدو كالتالي:

credentialService.ts
import { promisePool } from "../database"; import type { AuthenticatorDevice } from "@simplewebauthn/typescript-types"; export const credentialService = { async saveNewCredential( userId: string, credentialId: string, publicKey: string, counter: number, transports: string, ) { try { await promisePool.query( "INSERT INTO credentials (user_id, credential_id, public_key, counter, transports) VALUES (?, ?, ?, ?, ?)", [userId, credentialId, publicKey, counter, transports], ); } catch (error) { console.error("Error saving new credential:", error); throw error; } }, async getCredentialByCredentialId( credentialId: string, ): Promise<AuthenticatorDevice | null> { try { const [rows] = await promisePool.query( "SELECT * FROM credentials WHERE credential_id = ? LIMIT 1", [credentialId], ); // @ts-ignore if (rows.length === 0) return null; // @ts-ignore const row = rows[0]; return { userID: row.user_id, credentialID: row.credential_id, credentialPublicKey: row.public_key, counter: row.counter, transports: row.transports ? row.transports.split(",") : [], } as AuthenticatorDevice; } catch (error) { console.error("Error retrieving credential:", error); throw error; } }, async updateCredentialCounter(credentialId: string, newCounter: number) { try { await promisePool.query( "UPDATE credentials SET counter = ? WHERE credential_id = ?", [newCounter, credentialId], ); } catch (error) { console.error("Error updating credential counter:", error); throw error; } }, };

5.5 البرنامج الوسيط (Middleware)#

لمعالجة الأخطاء مركزيًا وتسهيل تصحيح الأخطاء أيضًا، نضيف ملف errorHandler.ts:

errorHandler.ts
import { Request, Response, NextFunction } from "express"; import { CustomError } from "./customError"; interface ErrorWithStatus extends Error { statusCode?: number; } export const handleError = ( err: CustomError, req: Request, res: Response, next: NextFunction, ) => { const statusCode = err.statusCode || 500; const message = err.message || "Internal Server Error"; console.log(message); res.status(statusCode).send({ error: message }); };

إلى جانب ذلك، نضيف ملف customError.ts جديدًا لأننا نريد لاحقًا أن نكون قادرين على إنشاء أخطاء مخصصة لمساعدتنا في العثور على الأخطاء بشكل أسرع:

customError.ts
export class CustomError extends Error { statusCode: number; constructor(message: string, statusCode: number = 500) { super(message); this.statusCode = statusCode; Object.setPrototypeOf(this, CustomError.prototype); } }

5.6 الأدوات المساعدة (Utilities)#

في مجلد utils، ننشئ ملفين constants.ts و utils.ts.

يحتوي constant.ts على بعض معلومات خادم WebAuthn الأساسية، مثل اسم الطرف المعتمد (relying party)، ومعرف الطرف المعتمد (relying party ID) والأصل (origin):

constant.ts
export const rpName: string = "Passkeys Tutorial"; export const rpID: string = "localhost"; export const origin: string = `http://${rpID}:8080`;

يحتوي utils.ts على دالتين سنحتاجهما لاحقًا لترميز وفك ترميز البيانات:

utils.ts
export const uint8ArrayToBase64 = (uint8Array: Uint8Array): string => Buffer.from(uint8Array).toString("base64"); export const base64ToUint8Array = (base64: string): Uint8Array => new Uint8Array(Buffer.from(base64, "base64"));

5.7 متحكمات مفاتيح المرور مع SimpleWebAuthn#

الآن، نأتي إلى قلب واجهتنا الخلفية: المتحكمات. ننشئ متحكمين، أحدهما لإنشاء مفتاح مرور جديد (registration.ts) والآخر لتسجيل الدخول باستخدام مفتاح مرور (authentication.ts).

registration.ts يبدو كالتالي:

registration.ts
import { generateRegistrationOptions, verifyRegistrationResponse, } from "@simplewebauthn/server"; import { uint8ArrayToBase64 } from "../utils/utils"; import { rpName, rpID, origin } from "../utils/constants"; import { credentialService } from "../services/credentialService"; import { userService } from "../services/userService"; import { RegistrationResponseJSON } from "@simplewebauthn/typescript-types"; import { Request, Response, NextFunction } from "express"; import { CustomError } from "../middleware/customError"; export const handleRegisterStart = async ( req: Request, res: Response, next: NextFunction, ) => { const { username } = req.body; if (!username) { return next(new CustomError("Username empty", 400)); } try { let user = await userService.getUserByUsername(username); if (user) { return next(new CustomError("User already exists", 400)); } else { user = await userService.createUser(username); } const options = await generateRegistrationOptions({ rpName, rpID, userID: user.id, userName: user.username, timeout: 60000, attestationType: "direct", excludeCredentials: [], authenticatorSelection: { residentKey: "preferred", }, // Support for the two most common algorithms: ES256, and RS256 supportedAlgorithmIDs: [-7, -257], }); req.session.loggedInUserId = user.id; req.session.currentChallenge = options.challenge; res.send(options); } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } }; export const handleRegisterFinish = async ( req: Request, res: Response, next: NextFunction, ) => { const { body } = req; const { currentChallenge, loggedInUserId } = req.session; if (!loggedInUserId) { return next(new CustomError("User ID is missing", 400)); } if (!currentChallenge) { return next(new CustomError("Current challenge is missing", 400)); } try { const verification = await verifyRegistrationResponse({ response: body as RegistrationResponseJSON, expectedChallenge: currentChallenge, expectedOrigin: origin, expectedRPID: rpID, requireUserVerification: true, }); if (verification.verified && verification.registrationInfo) { const { credentialPublicKey, credentialID, counter } = verification.registrationInfo; await credentialService.saveNewCredential( loggedInUserId, uint8ArrayToBase64(credentialID), uint8ArrayToBase64(credentialPublicKey), counter, body.response.transports, ); res.send({ verified: true }); } else { next(new CustomError("Verification failed", 400)); } } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } finally { req.session.loggedInUserId = undefined; req.session.currentChallenge = undefined; } };

دعنا نراجع وظائف متحكماتنا، التي تتعامل مع نقطتي النهاية الرئيسيتين في عملية تسجيل WebAuthn (sign-up). هذا هو أيضًا المكان الذي يكمن فيه أحد أكبر الاختلافات عن المصادقة القائمة على كلمة المرور: لكل محاولة تسجيل (sign-up) أو مصادقة (login)، يلزم استدعاءان لواجهة برمجة التطبيقات الخلفية، والتي تتطلب محتوى واجهة أمامية محددًا بينهما. عادة ما تحتاج كلمات المرور إلى نقطة نهاية واحدة فقط.

1. نقطة النهاية handleRegisterStart:

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

يتم إعداد PublicKeyCredentialCreationOptions. يتم تعيين residentKey إلى preferred، و attestationType إلى direct، لجمع المزيد من البيانات من المصادق (authenticator) للتخزين المحتمل في قاعدة البيانات.

بشكل عام، تتكون PublicKeyCredentialCreationOptions من البيانات التالية:

dictionary PublicKeyCredentialCreationOptions { required PublicKeyCredentialRpEntity rp; required PublicKeyCredentialUserEntity user; required BufferSource challenge; required sequence<PublicKeyCredentialParameters> pubKeyCredParams; unsigned long timeout; sequence<PublicKeyCredentialDescriptor> excludeCredentials = []; AuthenticatorSelectionCriteria authenticatorSelection; DOMString attestation = "none"; AuthenticationExtensionsClientInputs extensions; };
  • rp: يمثل معلومات الطرف المعتمد (relying party) (موقع الويب أو الخدمة)، ويتضمن عادةً اسمه (rp.name) والنطاق (rp.id).
  • user: يحتوي على تفاصيل حساب المستخدم مثل user.name و user.id و user.displayName.
  • challenge: قيمة عشوائية آمنة تم إنشاؤها بواسطة خادم WebAuthn لمنع هجمات إعادة التشغيل (replay attacks) أثناء عملية التسجيل.
  • pubKeyCredParams: يحدد نوع بيانات اعتماد المفتاح العام التي سيتم إنشاؤها، بما في ذلك الخوارزمية التشفيرية المستخدمة.
  • timeout: اختياري، يحدد الوقت بالمللي ثانية الذي يتعين على المستخدم إكمال التفاعل فيه.
  • excludeCredentials: قائمة ببيانات الاعتماد التي سيتم استبعادها؛ تستخدم لمنع تسجيل مفتاح مرور لنفس الجهاز / المصادق (authenticator) عدة مرات.
  • authenticatorSelection: معايير لاختيار المصادق (authenticator)، مثل ما إذا كان يجب أن يدعم التحقق من المستخدم أو كيف يجب تشجيع المفاتيح المقيمة (resident keys).
  • attestation: يحدد تفضيل نقل الشهادة (attestation) المطلوب، مثل "none" أو "indirect" أو "direct".
  • extensions: اختياري، يسمح بامتدادات عميل إضافية.

يتم تخزين معرف المستخدم (User ID) والتحدي في كائن جلسة، مما يبسط العملية لأغراض هذا الدليل التعليمي. علاوة على ذلك، يتم مسح الجلسة بعد كل محاولة تسجيل (sign-up) أو مصادقة (login).

2. نقطة النهاية handleRegisterFinish:

تسترد نقطة النهاية هذه معرف المستخدم (user ID) والتحدي اللذين تم تعيينهما سابقًا. تتحقق من RegistrationResponse مع التحدي. إذا كانت صالحة، فإنها تخزن بيانات اعتماد جديدة للمستخدم. بمجرد تخزينها في قاعدة البيانات، يتم إزالة معرف المستخدم والتحدي من الجلسة.

نصيحة: عند تصحيح أخطاء تطبيقك، نوصي بشدة باستخدام متصفح Chrome وميزاته المدمجة لتحسين تجربة المطور لتطبيقات مفاتيح المرور، على سبيل المثال، المصادق الافتراضي لـ WebAuthn وسجل الجهاز (انظر نصائحنا للمطورين أدناه لمزيد من المعلومات).

بعد ذلك، ننتقل إلى authentication.ts، الذي له هيكل ووظائف مماثلة.

authentication.ts يبدو كالتالي:

authentication.ts
import { Request, Response, NextFunction } from "express"; import { generateAuthenticationOptions, verifyAuthenticationResponse, } from "@simplewebauthn/server"; import { uint8ArrayToBase64, base64ToUint8Array } from "../utils/utils"; import { rpID, origin } from "../utils/constants"; import { credentialService } from "../services/credentialService"; import { userService } from "../services/userService"; import { AuthenticatorDevice } from "@simplewebauthn/typescript-types"; import { isoBase64URL } from "@simplewebauthn/server/helpers"; import { VerifiedAuthenticationResponse, VerifyAuthenticationResponseOpts, } from "@simplewebauthn/server/esm"; import { CustomError } from "../middleware/customError"; export const handleLoginStart = async ( req: Request, res: Response, next: NextFunction, ) => { const { username } = req.body; try { const user = await userService.getUserByUsername(username); if (!user) { return next(new CustomError("User not found", 404)); } req.session.loggedInUserId = user.id; // allowCredentials is purposely for this demo left empty. This causes all existing local credentials // to be displayed for the service instead only the ones the username has registered. const options = await generateAuthenticationOptions({ timeout: 60000, allowCredentials: [], userVerification: "required", rpID, }); req.session.currentChallenge = options.challenge; res.send(options); } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } }; export const handleLoginFinish = async ( req: Request, res: Response, next: NextFunction, ) => { const { body } = req; const { currentChallenge, loggedInUserId } = req.session; if (!loggedInUserId) { return next(new CustomError("User ID is missing", 400)); } if (!currentChallenge) { return next(new CustomError("Current challenge is missing", 400)); } try { const credentialID = isoBase64URL.toBase64(body.rawId); const bodyCredIDBuffer = isoBase64URL.toBuffer(body.rawId); const dbCredential: AuthenticatorDevice | null = await credentialService.getCredentialByCredentialId(credentialID); if (!dbCredential) { return next(new CustomError("Credential not registered with this site", 404)); } // @ts-ignore const user = await userService.getUserById(dbCredential.userID); if (!user) { return next(new CustomError("User not found", 404)); } // @ts-ignore dbCredential.credentialID = base64ToUint8Array(dbCredential.credentialID); // @ts-ignore dbCredential.credentialPublicKey = base64ToUint8Array( dbCredential.credentialPublicKey, ); let verification: VerifiedAuthenticationResponse; const opts: VerifyAuthenticationResponseOpts = { response: body, expectedChallenge: currentChallenge, expectedOrigin: origin, expectedRPID: rpID, authenticator: dbCredential, }; verification = await verifyAuthenticationResponse(opts); const { verified, authenticationInfo } = verification; if (verified) { await credentialService.updateCredentialCounter( uint8ArrayToBase64(bodyCredIDBuffer), authenticationInfo.newCounter, ); res.send({ verified: true }); } else { next(new CustomError("Verification failed", 400)); } } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } finally { req.session.currentChallenge = undefined; req.session.loggedInUserId = undefined; } };

تتضمن عملية المصادقة (login) لدينا نقطتي نهاية:

1. نقطة النهاية handleLoginStart:

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

بالنسبة للمستخدمين الحاليين، تسترد معرف المستخدم من قاعدة البيانات، وتخزنه في الجلسة، وتنشئ خيارات PublicKeyCredentialRequestOptions. يتم ترك allowCredentials فارغًا لتجنب تقييد استخدام بيانات الاعتماد. لهذا السبب يمكن تحديد جميع مفاتيح المرور المتاحة لهذا الطرف المعتمد (relying party) في نافذة مفتاح المرور.

يتم أيضًا تخزين التحدي الذي تم إنشاؤه في الجلسة ويتم إرسال PublicKeyCredentialRequestOptions مرة أخرى إلى الواجهة الأمامية.

تتكون PublicKeyCredentialRequestOptions من البيانات التالية:

dictionary PublicKeyCredentialRequestOptions { required BufferSource challenge; unsigned long timeout; USVString rpId; sequence<PublicKeyCredentialDescriptor> allowCredentials = []; DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputs extensions; };
  • challenge: قيمة عشوائية آمنة من خادم WebAuthn تستخدم لمنع هجمات إعادة التشغيل أثناء عملية المصادقة.
  • timeout: اختياري، يحدد الوقت بالمللي ثانية الذي يتعين على المستخدم الاستجابة فيه لطلب المصادقة.
  • rpId: معرف الطرف المعتمد، وعادة ما يكون نطاق الخدمة.
  • allowCredentials: قائمة اختيارية من واصفات بيانات الاعتماد، تحدد بيانات الاعتماد التي يمكن استخدامها لهذه المصادقة (login).
  • userVerification: يحدد متطلبات التحقق من المستخدم، مثل "required" أو "preferred" أو "discouraged".
  • extensions: اختياري، يسمح بامتدادات عميل إضافية.

2. نقطة النهاية handleLoginFinish:

تسترد نقطة النهاية هذه currentChallenge و loggedInUserId من الجلسة.

تستعلم عن قاعدة البيانات للحصول على بيانات الاعتماد الصحيحة باستخدام معرف بيانات الاعتماد (credential ID) من الجسم. إذا تم العثور على بيانات الاعتماد، فهذا يعني أنه يمكن الآن مصادقة المستخدم المرتبط بمعرف بيانات الاعتماد هذا (تسجيل الدخول). بعد ذلك، يمكننا الاستعلام عن المستخدم من جدول المستخدمين عبر معرف المستخدم الذي نحصل عليه من بيانات الاعتماد والتحقق من authenticationResponse باستخدام التحدي وجسم الطلب. إذا كان كل شيء ناجحًا، نعرض رسالة نجاح تسجيل الدخول. إذا لم يتم العثور على بيانات اعتماد مطابقة، يتم إرسال خطأ.

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

علاوة على ذلك، يمكننا حذف مجلد src/app و src/constant مع جميع الملفات الموجودة فيهما.

ملاحظة: تم حذف إدارة الجلسات المناسبة وحماية المسارات، وهي أمور حاسمة في التطبيقات الواقعية، هنا للتبسيط في هذا الدليل التعليمي.

5.8 مسارات مفاتيح المرور#

أخيرًا وليس آخرًا، نحتاج إلى التأكد من أن متحكماتنا قابلة للوصول عن طريق إضافة المسارات المناسبة إلى routes.ts الموجود في دليل جديد src/routes:

routes.ts
import express from "express"; import { handleError } from "../middleware/errorHandler"; import { handleRegisterStart, handleRegisterFinish } from "../controllers/registration"; import { handleLoginStart, handleLoginFinish } from "../controllers/authentication"; const router = express.Router(); router.post("/registerStart", handleRegisterStart); router.post("/registerFinish", handleRegisterFinish); router.post("/loginStart", handleLoginStart); router.post("/loginFinish", handleLoginFinish); router.use(handleError); export { router };
Substack Icon

Subscribe to our Passkeys Substack for the latest news.

Subscribe

6. دمج مفاتيح المرور في الواجهة الأمامية#

يركز هذا الجزء من دليل مفاتيح المرور التعليمي على كيفية دعم مفاتيح المرور في الواجهة الأمامية لتطبيقك. لدينا واجهة أمامية أساسية جدًا تتكون من ثلاثة ملفات: index.html و styles.css و script.js. جميع الملفات الثلاثة موجودة في مجلد src/public جديد.

يحتوي ملف index.html على حقل إدخال لاسم المستخدم وزرين للتسجيل وتسجيل الدخول. علاوة على ذلك، نقوم باستيراد برنامج @simplewebauthn/browser النصي الذي يبسط التفاعل مع واجهة برمجة تطبيقات مصادقة الويب في المتصفح في ملف js/script.js.

index.html يبدو كالتالي:

index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Passkey Tutorial</title> <link rel="stylesheet" href="css/style.css" /> </head> <body> <div class="container"> <h1>Passkey Tutorial</h1> <div id="message"></div> <div class="input-group"> <input type="text" id="username" placeholder="Enter username" /> <button id="registerButton">Register</button> <button id="loginButton">Login</button> </div> </div> <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.es5.umd.min.js"></script> <script src="js/script.js"></script> </body> </html>

script.js يبدو كالتالي:

script.js
document.getElementById("registerButton").addEventListener("click", register); document.getElementById("loginButton").addEventListener("click", login); function showMessage(message, isError = false) { const messageElement = document.getElementById("message"); messageElement.textContent = message; messageElement.style.color = isError ? "red" : "green"; } async function register() { // Retrieve the username from the input field const username = document.getElementById("username").value; try { // Get registration options from your server. Here, we also receive the challenge. const response = await fetch("/api/passkey/registerStart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username }), }); console.log(response); // Check if the registration options are ok. if (!response.ok) { throw new Error( "User already exists or failed to get registration options from server", ); } // Convert the registration options to JSON. const options = await response.json(); console.log(options); // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello). // A new attestation is created. This also means a new public-private-key pair is created. const attestationResponse = await SimpleWebAuthnBrowser.startRegistration(options); // Send attestationResponse back to server for verification and storage. const verificationResponse = await fetch("/api/passkey/registerFinish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(attestationResponse), }); if (verificationResponse.ok) { showMessage("Registration successful"); } else { showMessage("Registration failed", true); } } catch (error) { showMessage("Error: " + error.message, true); } } async function login() { // Retrieve the username from the input field const username = document.getElementById("username").value; try { // Get login options from your server. Here, we also receive the challenge. const response = await fetch("/api/passkey/loginStart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username }), }); // Check if the login options are ok. if (!response.ok) { throw new Error("Failed to get login options from server"); } // Convert the login options to JSON. const options = await response.json(); console.log(options); // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello). // A new assertionResponse is created. This also means that the challenge has been signed. const assertionResponse = await SimpleWebAuthnBrowser.startAuthentication(options); // Send assertionResponse back to server for verification. const verificationResponse = await fetch("/api/passkey/loginFinish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(assertionResponse), }); if (verificationResponse.ok) { showMessage("Login successful"); } else { showMessage("Login failed", true); } } catch (error) { showMessage("Error: " + error.message, true); } }

في script.js، هناك ثلاث وظائف أساسية:

1. وظيفة showMessage:

هذه وظيفة مساعدة تستخدم بشكل أساسي لعرض رسائل الخطأ، مما يساعد في تصحيح الأخطاء.

2. وظيفة Register:

يتم تشغيلها عندما ينقر المستخدم على "Register". تستخرج اسم المستخدم من حقل الإدخال وترسله إلى نقطة النهاية passkeyRegisterStart. تتضمن الاستجابة PublicKeyCredentialCreationOptions، والتي يتم تحويلها إلى JSON وتمريرها إلى SimpleWebAuthnBrowser.startRegistration. يؤدي هذا الاستدعاء إلى تنشيط مصادق الجهاز (مثل Face ID أو Touch ID). عند المصادقة المحلية الناجحة، يتم إرسال التحدي الموقع مرة أخرى إلى نقطة النهاية passkeyRegisterFinish، لإكمال عملية إنشاء مفتاح المرور.

أثناء عملية التسجيل (sign-up)، يلعب كائن الشهادة (attestation) دورًا حاسمًا، لذلك دعونا نلقي نظرة فاحصة عليه.

يتكون كائن الشهادة (attestation) بشكل أساسي من ثلاثة مكونات: fmt و attStmt و authData. يشير عنصر fmt إلى تنسيق بيان الشهادة، بينما يمثل attStmt بيان الشهادة الفعلي نفسه. في السيناريوهات التي تعتبر فيها الشهادة غير ضرورية، سيتم تعيين fmt على "none"، مما يؤدي إلى attStmt فارغ.

التركيز على جزء authData داخل هذا الهيكل. هذا الجزء هو مفتاح استرداد العناصر الأساسية مثل معرف الطرف المعتمد، والأعلام، والعداد، وبيانات الاعتماد الموثقة على خادمنا. فيما يتعلق بالأعلام، فإن BS (حالة النسخ الاحتياطي) و BE (أهلية النسخ الاحتياطي) ذات أهمية خاصة والتي توفر مزيدًا من المعلومات إذا تمت مزامنة مفتاح المرور (على سبيل المثال عبر iCloud Keychain أو 1Password). إلى جانب ذلك، يوفر UV (التحقق من المستخدم) و UP (وجود المستخدم) مزيدًا من المعلومات المفيدة.

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

3. وظيفة Login:

يتم تنشيطها عندما ينقر المستخدم على "Login". على غرار وظيفة التسجيل، تستخرج اسم المستخدم وترسله إلى نقطة النهاية passkeyLoginStart. يتم تحويل الاستجابة، التي تحتوي على PublicKeyCredentialRequestOptions، إلى JSON واستخدامها مع SimpleWebAuthnBrowser.startAuthentication. يؤدي هذا إلى تشغيل المصادقة المحلية على الجهاز. ثم يتم إرسال التحدي الموقع مرة أخرى إلى نقطة النهاية passkeyLoginFinish. تشير الاستجابة الناجحة من نقطة النهاية هذه إلى أن المستخدم قد سجل الدخول إلى التطبيق بنجاح.

بالإضافة إلى ذلك، يوفر ملف CSS المصاحب تنسيقًا بسيطًا للتطبيق:

body { font-family: "Helvetica Neue", Arial, sans-serif; text-align: center; padding: 40px; background-color: #f3f4f6; color: #333; } .container { max-width: 400px; margin: auto; background: white; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border-radius: 8px; } h1 { color: #007bff; font-size: 24px; margin-bottom: 20px; } .input-group { margin-bottom: 20px; } input[type="text"] { padding: 10px; margin-bottom: 10px; border: 1px solid #ced4da; border-radius: 4px; width: calc(100% - 22px); } button { width: calc(50% - 20px); padding: 10px 0; margin: 5px; font-size: 16px; cursor: pointer; border: none; border-radius: 4px; background-color: #007bff; color: white; } button:hover { background-color: #0056b3; } #message { color: #dc3545; margin: 20px; }

7. تشغيل تطبيق مثال مفاتيح المرور#

لرؤية تطبيقك قيد التشغيل، قم بتجميع وتشغيل كود TypeScript الخاص بك باستخدام:

npm run dev

يجب أن يكون خادمك الآن قيد التشغيل على http://localhost:8080.

اعتبارات للإنتاج:

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

  • التدابير الأمنية: تنفيذ ممارسات أمنية قوية لحماية بيانات المستخدم.
  • معالجة الأخطاء: تأكد من أن تطبيقك يتعامل مع الأخطاء ويسجلها بأمان.
  • إدارة قاعدة البيانات: تحسين عمليات قاعدة البيانات من أجل قابلية التوسع والموثوقية.

8. تكامل DevOps لمفاتيح المرور#

لقد قمنا بالفعل بإعداد حاوية Docker لقاعدة بياناتنا. بعد ذلك، سنقوم بتوسيع إعداد Docker Compose الخاص بنا ليشمل الخادم مع كل من الواجهة الخلفية والواجهة الأمامية. يجب تحديث ملف docker-compose.yml الخاص بك وفقًا لذلك.

لتحويل تطبيقنا إلى حاوية، ننشئ Dockerfile جديدًا يقوم بتثبيت الحزم المطلوبة وبدء خادم التطوير:

Docker
# Use an official Node runtime as a parent image FROM node:20-alpine # Set the working directory in the container WORKDIR /usr/src/app # Copy package.json and package-lock.json COPY package*.json ./ # Install any needed packages RUN npm install # Bundle your app's source code inside the Docker image COPY . . # Make port 8080 available to the world outside this container EXPOSE 8080 # Define the command to run your app CMD ["npm", "run", "dev"]

ثم، نقوم أيضًا بتوسيع ملف docker-compose.yml لبدء هذه الحاوية:

docker-compose.yml
version: "3.1" services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: my-secret-pw MYSQL_DATABASE: webauthn_db ports: - "3306:3306" volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql app: build: . ports: - "8080:8080" environment: - DB_HOST=db - DB_USER=root - DB_PASSWORD=my-secret-pw - DB_NAME=webauthn_db - SESSION_SECRET=secret123 depends_on: - db

إذا قمت الآن بتشغيل docker compose up في الطرفية الخاصة بك والوصول إلى http://localhost:8080، يجب أن ترى الإصدار العامل من تطبيق الويب الخاص بمفاتيح المرور (هنا يعمل على Windows 11 23H2 + Chrome 119):

9. نصائح إضافية لمطوري مفاتيح المرور#

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

  • توافق ودعم الأجهزة / المنصات
  • تأهيل المستخدمين وتثقيفهم
  • التعامل مع الأجهزة المفقودة أو المتغيرة
  • المصادقة عبر المنصات
  • آليات احتياطية
  • تعقيد الترميز: غالبًا ما يكون الترميز هو الجزء الأصعب حيث يتعين عليك التعامل مع JSON و CBOR و uint8arrays والمخازن المؤقتة (buffers) والكتل (blobs) وقواعد البيانات المختلفة و base64 و base64url حيث يمكن أن تحدث الكثير من الأخطاء
  • إدارة مفاتيح المرور (على سبيل المثال، لإضافة أو حذف أو إعادة تسمية مفاتيح المرور)

علاوة على ذلك، لدينا النصائح التالية للمطورين عندما يتعلق الأمر بجزء التنفيذ:

استخدام مصحح أخطاء مفاتيح المرور (Passkeys Debugger)

يساعد Passkeys debugger في اختبار إعدادات خادم WebAuthn المختلفة واستجابات العميل. علاوة على ذلك، فإنه يوفر محللًا رائعًا لاستجابات المصادق.

تصحيح الأخطاء باستخدام ميزة سجل جهاز Chrome

استخدم سجل جهاز Chrome (يمكن الوصول إليه عبر chrome://device-log/) لمراقبة استدعاءات FIDO/WebAuthn. توفر هذه الميزة سجلات في الوقت الفعلي لعملية المصادقة (login)، مما يتيح لك رؤية البيانات التي يتم تبادلها واستكشاف أي مشكلات تنشأ وإصلاحها.

اختصار آخر مفيد جدًا للحصول على جميع مفاتيح المرور الخاصة بك في Chrome هو استخدام chrome://settings/passkeys.

استخدام مصادق WebAuthn الافتراضي في Chrome

لتجنب استخدام مطالبة Touch ID أو Face ID أو Windows Hello أثناء التطوير، يأتي Chrome مع مصادق WebAuthn افتراضي مفيد جدًا يحاكي مصادقًا حقيقيًا. نوصي بشدة باستخدامه لتسريع الأمور. ابحث عن مزيد من التفاصيل هنا.

الاختبار عبر منصات ومتصفحات مختلفة

تأكد من التوافق والوظائف عبر مختلف المتصفحات والمنصات. يتصرف WebAuthn بشكل مختلف على متصفحات مختلفة، لذا فإن الاختبار الشامل هو المفتاح.

الاختبار على أجهزة مختلفة

هنا من المفيد بشكل خاص العمل مع أدوات مثل ngrok، حيث يمكنك جعل تطبيقك المحلي قابلاً للوصول على أجهزة أخرى (محمولة).

تعيين التحقق من المستخدم إلى preferred

عند تحديد خصائص userVerification في PublicKeyCredentialRequestOptions، اختر تعيينها إلى preferred لأن هذا يمثل حلاً وسطًا جيدًا بين قابلية الاستخدام والأمان. هذا يعني أن فحوصات الأمان موجودة على الأجهزة المناسبة مع الحفاظ على سهولة الاستخدام على الأجهزة التي لا تحتوي على إمكانيات بيومترية.

10. الخلاصة: دليل مفاتيح المرور التعليمي#

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

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

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start for free

Share this article


LinkedInTwitterFacebook

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.

Related Articles