نظرة عامة
في ما يلي نظرة عامة عالية المستوى على الخطوات الرئيسية اللازمة لتسجيل مفاتيح المرور:
- حدِّد خيارات إنشاء مفتاح مرور. أرسِلهما إلى العميل لتتمكّن من توجيهه إلى الطلب الخاص بإنشاء مفتاح المرور: طلب WebAuthn API
navigator.credentials.create
على الويب وcredentialManager.createCredential
على Android. بعد تأكيد المستخدم إنشاء مفتاح المرور، يتم حل طلب إنشاء مفتاح المرور ويتم عرض بيانات اعتمادPublicKeyCredential
. - تحقَّق من بيانات الاعتماد وخزِّنها على الخادم.
تتعمق الأقسام التالية في تفاصيل كل خطوة.
إنشاء خيارات إنشاء بيانات الاعتماد
الخطوة الأولى التي يجب اتخاذها على الخادم هي إنشاء عنصر PublicKeyCredentialCreationOptions
.
ولإجراء ذلك، يمكنك الاعتماد على مكتبة FIDO التابعة للخادم. ستقدم عادةً دالة فائدة يمكنها إنشاء هذه الخيارات لك. يوفر SimpleWebAuthn، على سبيل المثال، generateRegistrationOptions
.
يجب أن يتضمّن PublicKeyCredentialCreationOptions
كل ما يلزم لإنشاء مفتاح المرور، مثل معلومات عن المستخدم، وعن الجهة المحظورة، وإعدادات لخصائص بيانات الاعتماد التي تُنشئها. بعد تعريف كل هذه الإعدادات، يمكنك ضبطها حسب الحاجة إلى الدالة في مكتبة FIDO من جهة الخادم المسؤولة عن إنشاء الكائن PublicKeyCredentialCreationOptions
.
جزء من PublicKeyCredentialCreationOptions
يمكن أن تكون الحقول ثوابت. يجب تحديد القيم الأخرى ديناميكيًا على الخادم:
rpId
: لتعبئة رقم تعريف الجهة المحظورة على الخادم، استخدِم الدوال أو المتغيّرات من جهة الخادم التي تمنحك اسم مضيف تطبيق الويب، مثلexample.com
.user.name
وuser.displayName
: لتعبئة هذه الحقول، استخدِم معلومات جلسة المستخدم الذي سجّل الدخول (أو معلومات حساب المستخدم الجديد، إذا كان المستخدم ينشئ مفتاح مرور عند الاشتراك). يكون عادةًuser.name
عنوان بريد إلكتروني وفريدًا للجهة المحظورة. "user.displayName
" هو اسم سهل الاستخدام. ملاحظة: لن تستخدم كل الأنظمة الأساسيةdisplayName
.user.id
: سلسلة عشوائية وفريدة يتم إنشاؤها عند إنشاء الحساب ويجب أن يكون الاسم دائمًا، على عكس اسم المستخدم الذي قد يكون قابلاً للتعديل. ويحدِّد رقم تعريف المستخدم حسابًا، ولكن يجب ألا يحتوي على أي معلومات تحدِّد الهوية الشخصية. لديك على الأرجح رقم تعريف مستخدم في نظامك، ولكن إذا لزم الأمر، يمكنك إنشاء رقم تعريف مخصّص لمفاتيح المرور لإبقائه خاليًا من أي معلومات تحديد الهوية الشخصية.excludeCredentials
: قائمة ببيانات الاعتماد الحالية أرقام التعريف لمنع تكرار مفتاح مرور من موفِّر مفتاح المرور. لتعبئة هذا الحقل، ابحث في بيانات الاعتماد الحالية لهذا المستخدم في قاعدة البيانات. راجِع التفاصيل في قسم منع إنشاء مفتاح مرور جديد في حال توفُّره حاليًا.challenge
: بالنسبة إلى تسجيل بيانات الاعتماد، لن يكون التحدي مرتبطًا إلا إذا كنت تستخدم أسلوب المصادقة، وهو أسلوب أكثر تقدمًا للتحقق من هوية موفِّر مفتاح المرور والبيانات التي يصدرها. ومع ذلك، حتى إذا لم تكن تستخدم المصادقة، لا يزال التحدي حقلاً مطلوبًا. في هذه الحالة، يمكنك ضبط هذا التحدي على0
واحد لتبسيط الأمر. تتوفّر تعليمات إنشاء اختبار آمن للمصادقة في المصادقة باستخدام مفتاح المرور من جهة الخادم.
الترميز وفك التشفير
تتضمّن PublicKeyCredentialCreationOptions
حقولاً بقيمة ArrayBuffer
، لذا فهي غير متاحة في JSON.stringify()
. ويعني هذا أنّه في الوقت الحالي، ولإرسال PublicKeyCredentialCreationOptions
عبر HTTPS، يجب ترميز بعض الحقول يدويًا على الخادم باستخدام base64URL
ثم فك ترميزها على البرنامج.
- على الخادم، تتم عادةً معالجة عمليات التشفير وفك التشفير من خلال مكتبة FIDO في جهة الخادم.
- على جهاز العميل، يجب إجراء الترميز وفكّ الترميز يدويًا في الوقت الحالي. سيصبح الأمر أكثر سهولة في المستقبل: ستتوفّر طريقة لتحويل الخيارات بتنسيق JSON إلى
PublicKeyCredentialCreationOptions
. يمكنك الاطّلاع على حالة الاستخدام في Chrome.
مثال على الرمز: إنشاء خيارات إنشاء بيانات الاعتماد
نحن نستخدم مكتبة SimpleWebAuthn في أمثلتنا. ننقل هنا خيارات إنشاء بيانات اعتماد المفتاح العام إلى وظيفة generateRegistrationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
const { user } = res.locals;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// `excludeCredentials` prevents users from re-registering existing
// credentials for a given passkey provider
const excludeCredentials = [];
const credentials = Credentials.findByUserId(user.id);
if (credentials.length > 0) {
for (const cred of credentials) {
excludeCredentials.push({
id: isoBase64URL.toBuffer(cred.id),
type: 'public-key',
transports: cred.transports,
});
}
}
// Generate registration options for WebAuthn create
const options = generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.HOSTNAME,
userID: user.id,
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
requireResidentKey: true
},
});
// Keep the challenge in the session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).send({ error: e.message });
}
});
تخزين المفتاح العام
عندما يتم حلّ المشكلة "navigator.credentials.create
" بنجاح على الجهاز العميل، هذا يعني أنّه تم إنشاء مفتاح مرور بنجاح. يتم إرجاع عنصر PublicKeyCredential
.
يحتوي العنصر PublicKeyCredential
على العنصر AuthenticatorAttestationResponse
، ما يمثّل استجابة موفِّر مفتاح المرور لتعليمات العميل بشأن إنشاء مفتاح مرور. ويحتوي على معلومات عن بيانات الاعتماد الجديدة التي تحتاج إليها بصفتك جهة محظورة لمصادقة المستخدم لاحقًا. يمكنك الاطّلاع على مزيد من المعلومات حول "AuthenticatorAttestationResponse
" في الملحق: AuthenticatorAttestationResponse
.
أرسِل كائن PublicKeyCredential
إلى الخادم. بعد استلامه، أثبِت ملكيته.
يمكنك تسليم خطوة إثبات الملكية هذه إلى مكتبة FIDO من جهة الخادم. وستقدم عادةً دالة فائدة لهذا الغرض. يوفر SimpleWebAuthn، على سبيل المثال، verifyRegistrationResponse
. يمكنك الاطّلاع على مزيد من التفاصيل في الملحق: التحقّق من صحة الردّ الذي قدّمه التسجيل.
بعد إتمام عملية التحقق بنجاح، يمكنك تخزين معلومات بيانات الاعتماد في قاعدة البيانات حتى يتمكن المستخدم لاحقًا من المصادقة باستخدام مفتاح المرور المرتبط ببيانات الاعتماد هذه.
يمكنك استخدام جدول مخصَّص لبيانات اعتماد المفاتيح العامة المرتبطة بمفاتيح المرور. يمكن للمستخدم امتلاك كلمة مرور واحدة فقط، ولكن يمكنه امتلاك عدة مفاتيح مرور، على سبيل المثال، مفتاح مرور تمت مزامنته عبر Apple iCloud Keychain وآخر عبر "مدير كلمات المرور في Google".
إليك مثال على مخطط يمكنك استخدامه لتخزين معلومات بيانات الاعتماد:
- جدول المستخدمون:
user_id
: رقم تعريف المستخدِم الأساسي رقم تعريف عشوائي وفريد ودائم للمستخدم. يمكنك استخدام هذا المفتاح كمفتاح أساسي لجدول المستخدمون.username
اسم مستخدم محدّد من قِبل المستخدم ويمكن تعديلهpasskey_user_id
: رقم تعريف المستخدم بدون معلومات تحديد الهوية الشخصية الخاص بمفتاح المرور، والمتمثل فيuser.id
في خيارات التسجيل. وعندما يحاول المستخدم إجراء المصادقة لاحقًا، سيجعل برنامج المصادقةpasskey_user_id
متاحًا في استجابة المصادقة فيuserHandle
. ننصحك بعدم ضبطpasskey_user_id
كمفتاح أساسي. غالبًا ما تصبح المفاتيح الأساسية معلومات تحديد هوية شخصية فعلية في الأنظمة، لأنها تُستخدم على نطاق واسع.
- جدول بيانات اعتماد المفتاح العام:
id
: معرّف بيانات الاعتماد يمكنك استخدام هذا المفتاح كمفتاح أساسي لجدول بيانات اعتماد المفتاح العام.public_key
: المفتاح العام لبيانات الاعتماد.passkey_user_id
: استخدِم هذا المفتاح كمفتاح خارجي لإنشاء رابط مع جدول المستخدمون.backed_up
: يتم الاحتفاظ بنسخة احتياطية من مفتاح المرور إذا تمت مزامنته من خلال موفِّر مفتاح المرور. يكون تخزين حالة النسخة الاحتياطية مفيدًا إذا كنت تريد تجاهل كلمات المرور في المستقبل للمستخدمين الذين يملكون مفاتيح مرورbacked_up
. يمكنك التحقّق مما إذا تم الاحتفاظ بنسخة احتياطية من مفتاح المرور من خلال فحص العلامات فيauthenticatorData
، أو باستخدام ميزة مكتبة FIDO من جهة الخادم المتوفّرة عادةً لمنحك وصولاً سهلاً إلى هذه المعلومات. يمكن أن يساعد تخزين متطلبات أهلية الاحتفاظ بنسخة احتياطية من البيانات في الردّ على استفسارات المستخدمين المحتملة.name
: اختيار اسم معروض لبيانات الاعتماد يتيح للمستخدمين تقديم أسماء مخصّصة لبيانات الاعتماد.transports
: مصفوفة من وسائل النقل. يُعد تخزين عمليات النقل مفيدًا لتجربة المستخدم للمصادقة. عندما تكون وسائل النقل متاحة، يمكن أن يعمل المتصفّح على النحو المطلوب، ويعرض واجهة مستخدم تتطابق مع وسيلة النقل التي يستخدمها موفِّر مفتاح المرور للتواصل مع العملاء، لا سيما في حالات استخدام إعادة المصادقة التي لا يكون فيهاallowCredentials
فارغًا.
ويمكن أن يفيد تخزين المعلومات الأخرى لأغراض تجربة المستخدم، بما في ذلك عناصر مثل موفِّر مفتاح المرور ووقت إنشاء بيانات الاعتماد ووقت آخر استخدام. يمكنك الاطّلاع على مزيد من المعلومات في مقالة تصميم واجهة مستخدم مفاتيح المرور.
مثال على الرمز: تخزين بيانات الاعتماد
نحن نستخدم مكتبة SimpleWebAuthn في أمثلتنا.
في هذه الحالة، يتم تسليم عملية التحقّق من ردّ التسجيل إلى وظيفة verifyRegistrationResponse
.
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
const response = req.body;
// This sample code is for registering a passkey for an existing,
// signed-in user
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Verify the credential
const { verified, registrationInfo } = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
requireUserVerification: false,
});
if (!verified) {
throw new Error('Verification failed.');
}
const { credentialPublicKey, credentialID } = registrationInfo;
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
publicKey: base64PublicKey,
// Optional: set the platform as a default name for the credential
// (example: "Pixel 7")
name: req.useragent.platform,
transports: response.response.transports,
passkey_user_id: user.passkey_user_id,
backed_up: registrationInfo.credentialBackedUp
});
// Kill the challenge for this session
delete req.session.challenge;
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).send({ error: e.message });
}
});
الملحق: AuthenticatorAttestationResponse
يحتوي AuthenticatorAttestationResponse
على عنصرَين مهمَّين:
response.clientDataJSON
هو إصدار JSON من بيانات العميل، وهي على الويب بيانات كما تظهر في المتصفح. ويحتوي على مصدر الجهة المحظورة والتحدي وandroidPackageName
إذا كان العميل أحد تطبيقات Android. بصفتك جهة محظورة، تمنحك القراءةclientDataJSON
إمكانية الوصول إلى المعلومات التي اطّلع عليها المتصفّح عند طلبcreate
.response.attestationObject
تحتوي على معلومتَين:attestationStatement
وهو غير ملائم ما لم يتم استخدام المصادقة.authenticatorData
هي البيانات على النحو الذي يراه مزوّد مفاتيح المرور. بصفتك جهة محظورة، تمنحك القراءةauthenticatorData
إمكانية الوصول إلى البيانات التي يطّلع عليها موفِّر مفتاح المرور ويتم عرضها في وقت طلبcreate
.
authenticatorData
تحتوي على معلومات أساسية حول بيانات اعتماد المفتاح العام المرتبطة بمفتاح المرور الذي تم إنشاؤه حديثًا:
- بيانات اعتماد المفتاح العام نفسها ومعرّف بيانات اعتماد فريد له.
- تشير هذه السمة إلى رقم تعريف الجهة المحظورة المرتبط ببيانات الاعتماد.
- علامات تصف حالة المستخدم عند إنشاء مفتاح المرور: ما إذا كان المستخدم موجودًا بالفعل، وما إذا كان قد تم إثبات هوية المستخدم بنجاح (راجِع
userVerification
) - AAGUID، وهو يحدّد موفِّر مفتاح المرور. يمكن أن يكون عرض موفِّر مفتاح المرور مفيدًا للمستخدمين، خاصةً إذا كان لديهم مفتاح مرور مسجَّل لخدمتك على عدة موفِّري مفاتيح مرور.
على الرغم من دمج authenticatorData
في attestationObject
، يجب إدخال المعلومات التي تحتوي عليها لتنفيذ مفتاح المرور، سواء كنت تستخدم المصادقة أم لا. يكون authenticatorData
مشفَّرًا، ويحتوي على حقول تم ترميزها بتنسيق ثنائي. ستعالج المكتبة من جانب الخادم عادةً التحليل وفك الترميز. إذا كنت لا تستخدم مكتبة من جهة الخادم، يمكنك الاستفادة من "getAuthenticatorData()
" من جهة العميل لتوفير بعض عمليات التحليل وفك الترميز الخاصة بالعمل من جهة الخادم.
الملحق: التحقّق من ردّ التسجيل
في البداية، تشمل عملية التحقّق من الردّ على التسجيل عمليات التحقّق التالية:
- تأكَّد من أنّ رقم تعريف الجهة المحظورة يتطابق مع موقعك الإلكتروني.
- تأكَّد من أنّ مصدر الطلب هو مصدر متوقَّع لموقعك الإلكتروني (عنوان URL للموقع الإلكتروني الرئيسي أو تطبيق Android).
- إذا كنت تطلب التحقق من المستخدم، تأكد من أن علامة التحقق من المستخدم
authenticatorData.uv
هيtrue
. تحقَّق من أنّ علامة تواجد المستخدمauthenticatorData.up
هيtrue
، لأنّ مفاتيح المرور مطلوبة دائمًا. - التحقق من أن العميل كان قادرًا على تقديم التحدي الذي قدمته له. إذا كنت لا تستخدم المصادقة، لن تكون عملية الفحص هذه مهمة. ومع ذلك، يُعد تنفيذ هذا الفحص من أفضل الممارسات: فهو يضمن أن التعليمة البرمجية جاهزة إذا قررت استخدام المصادقة في المستقبل.
- تأكَّد من أنه لم يتم تسجيل رقم تعريف بيانات الاعتماد لأي مستخدم حتى الآن.
- تأكَّد من أنّ الخوارزمية التي يستخدمها موفِّر مفاتيح المرور لإنشاء بيانات الاعتماد هي خوارزمية أدرجتها (في كل حقل
alg
من حقولpublicKeyCredentialCreationOptions.pubKeyCredParams
، والتي يتم تحديدها عادةً في المكتبة من جهة الخادم ولا تكون مرئية لك). وهذا يضمن أنه لا يمكن للمستخدمين التسجيل إلا باستخدام الخوارزميات التي اخترت السماح بها.
للتعرّف على مزيد من المعلومات، يمكنك الاطّلاع على رمز مصدر verifyRegistrationResponse
في SimpleWebAuthn أو الاطّلاع على القائمة الكاملة لعمليات إثبات الملكية في المواصفات.