نظرة عامة
في ما يلي نظرة عامة عالية المستوى على الخطوات الرئيسية المتضمنة في مصادقة مفتاح المرور:
- حدِّد التحدي والخيارات الأخرى اللازمة للمصادقة باستخدام مفتاح مرور. أرسِلها إلى العميل لتتمكّن من إرسالها إلى المكالمة الخاصة بمصادقة مفتاح المرور ("
navigator.credentials.get
" على الويب). بعد أن يؤكّد المستخدم مصادقة مفتاح المرور، يتم حل المكالمة الخاصة بمصادقة مفتاح المرور ويتم عرض بيانات اعتماد (PublicKeyCredential
). تحتوي بيانات الاعتماد على تأكيد مصادقة.
- تحقق من تأكيد المصادقة.
- إذا كان تأكيد المصادقة صالحًا، يمكنك المصادقة على المستخدم.
تتعمق الأقسام التالية في تفاصيل كل خطوة.
أنشِئ التحدي
من الناحية العملية، يشكّل التحدي مصفوفة من وحدات البايت العشوائية، ويتم تمثيلها على شكل كائن ArrayBuffer
.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
ولضمان تحقيق التحدّي للغرض المحدّد له، يجب:
- تأكَّد من عدم استخدام اختبار التحقُّق نفسه أكثر من مرة. إنشاء تحدٍ جديد في كل محاولة تسجيل دخول تجاهل التحدي بعد كل محاولة تسجيل دخول، سواء نجحت أو فشلت. أيضًا تجاهل التحدي بعد مدة معينة. عدم قبول التحدي نفسه في الرد أكثر من مرة.
- تأكَّد من أنّ التحدي آمن من خلال التشفير. يجب أن يكون من المستحيل عمليًا تخمين التحدي. لإنشاء اختبار تحقق آمن من جهة الخادم من خلال التشفير، من الأفضل الاعتماد على مكتبة FIDO من جهة الخادم تثق بها. وإذا أنشأت تحدياتك الخاصة بدلاً من ذلك، يمكنك استخدام وظيفة التشفير المضمَّنة المتوفّرة في حزمة التكنولوجيا، أو البحث عن المكتبات المصمّمة لحالات الاستخدام المشفَّرة. من الأمثلة على ذلك iso-crypto في Node.js، أو الأسرار في بايثون. وفقًا للمواصفات، يجب ألا يقل حجم التحدي عن 16 بايت حتى يعتبر آمنًا.
وبعد إنشاء تحدٍّ، يمكنك حفظه في جلسة المستخدم لإثبات صحته لاحقًا.
خيارات إنشاء طلب بيانات الاعتماد
إنشاء خيارات طلب بيانات الاعتماد كعنصر publicKeyCredentialRequestOptions
.
ولإجراء ذلك، يمكنك الاعتماد على مكتبة FIDO التابعة للخادم. ستقدم عادةً دالة فائدة يمكنها إنشاء هذه الخيارات لك. يوفر SimpleWebAuthn، على سبيل المثال، generateAuthenticationOptions
.
يجب أن يحتوي publicKeyCredentialRequestOptions
على جميع المعلومات اللازمة لمصادقة مفتاح المرور. مرِّر هذه المعلومات إلى الدالة في مكتبة FIDO من جهة الخادم المسؤولة عن إنشاء كائن publicKeyCredentialRequestOptions
.
جزء من publicKeyCredentialRequestOptions
يمكن أن تكون الحقول ثوابت. يجب تحديد القيم الأخرى ديناميكيًا على الخادم:
rpId
: رقم تعريف الجهة المحظورة الذي تتوقّع أن يتم ربط بيانات الاعتماد به، على سبيل المثالexample.com
. لن تنجح المصادقة إلا إذا كان رقم تعريف الجهة المحظورة الذي تقدّمه هنا يتطابق مع رقم تعريف الجهة المحظورة المرتبط ببيانات الاعتماد. لتعبئة رقم تعريف الجهة المحظورة، استخدِم القيمة نفسها المستخدَمة في رقم تعريف الجهة المحظورة الذي ضبطته فيpublicKeyCredentialCreationOptions
أثناء تسجيل بيانات الاعتماد.challenge
: جزء من البيانات التي سيوقّعها موفِّر مفتاح المرور لإثبات أنّ المستخدم يحتفظ بمفتاح المرور في وقت طلب المصادقة. راجِع التفاصيل في قسم إنشاء التحدّي.allowCredentials
: مصفوفة من بيانات الاعتماد المقبولة لهذه المصادقة. مرِّر مصفوفة فارغة للسماح للمستخدم باختيار مفتاح مرور متاح من قائمة يعرضها المتصفّح. لمعرفة التفاصيل، يمكنك الاطّلاع على مقالة جلب اختبار تأكيد من خادم الجهة المحظورة ومراجعة تفصيلية لبيانات الاعتماد القابلة للاكتشاف.userVerification
: تشير إلى ما إذا كانت عملية إثبات هوية المستخدم باستخدام قفل شاشة الجهاز "مطلوبة" أو "مفضّلة". أو "محبط". راجِع المقالة جلب تحدٍّ من خادم الجهة المحظورة.timeout
: المدة (بالمللي ثانية) التي يمكن أن يستغرقها المستخدم لإكمال المصادقة. ويجب أن يكون مقدارًا كبيرًا بشكل معقول وأن يكون أقصر من عمرchallenge
. القيمة التلقائية المقترَحة هي 5 دقائق، ولكن يمكنك زيادتها، وهي تصل إلى 10 دقائق، ولا تزال ضمن النطاق المُقترَح. المهلات الطويلة منطقية إذا كنت تتوقع أن يستخدم المستخدمون سير العمل المختلَط، وهو ما يستغرق عادةً وقتًا أطول قليلاً. إذا انتهت العملية، سيتم طرحNotAllowedError
.
بعد إنشاء "publicKeyCredentialRequestOptions
"، أرسِله إلى العميل.
مثال على الرمز: إنشاء خيارات طلب بيانات الاعتماد
نحن نستخدم مكتبة SimpleWebAuthn في أمثلتنا. ننقل هنا إنشاء خيارات طلب بيانات الاعتماد إلى الوظيفة generateAuthenticationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
router.post('/signinRequest', csrfCheck, async (req, res) => {
// Ensure you nest 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 {
// Use the generateAuthenticationOptions function from SimpleWebAuthn
const options = await generateAuthenticationOptions({
rpID: process.env.HOSTNAME,
allowCredentials: [],
});
// Save the challenge in the user session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).json({ error: e.message });
}
});
التحقق من المستخدم وتسجيل الدخول إليه
عندما يتم حلّ المشكلة navigator.credentials.get
بنجاح على الجهاز، يتم عرض عنصر PublicKeyCredential
.
إنّ العمود response
هو AuthenticatorAssertionResponse
. وهو يمثّل استجابة موفِّر مفتاح المرور لتعليمات العميل لإنشاء ما هو مطلوب لمحاولة المصادقة باستخدام مفتاح مرور على الجهة المحظورة. ويشتمل على ما يلي:
response.authenticatorData
وresponse.clientDataJSON
، مثلاً في خطوة تسجيل مفتاح المرور.response.signature
الذي يحتوي على توقيع على هذه القيم.
أرسِل العنصر PublicKeyCredential
إلى الخادم.
على الخادم، قم بما يلي:
- اجمع المعلومات اللازمة لإثبات صحة التأكيد ومصادقة المستخدم:
- احصل على الاختبار المتوقع الذي خزّنته في الجلسة عند إنشاء خيارات المصادقة.
- الحصول على المصدر ورقم تعريف الجهة المحظورة المتوقعَين
- ابحث في قاعدة البيانات عن هوية المستخدم. في حالة بيانات الاعتماد القابلة للاكتشاف، لن تعرف من هو المستخدم الذي يقدم طلب المصادقة. لمعرفة ذلك، لديك خياران:
- الخيار 1: استخدام
response.userHandle
في كائنPublicKeyCredential
في جدول المستخدمون، ابحث عنpasskey_user_id
الذي يطابقuserHandle
. - الخيار 2: استخدام بيانات الاعتماد
id
المتوفّرة في الكائنPublicKeyCredential
في جدول بيانات اعتماد المفتاح العام، ابحث عن بيانات الاعتمادid
التي تتطابق مع بيانات الاعتمادid
المتوفّرة في كائنPublicKeyCredential
. بعد ذلك، ابحث عن المستخدم المقابل باستخدام المفتاح الخارجيpasskey_user_id
لجدول المستخدمون.
- الخيار 1: استخدام
- ابحث في قاعدة البيانات عن معلومات بيانات اعتماد المفتاح العام التي تتطابق مع تأكيد المصادقة الذي تلقّيته. لإجراء ذلك، في جدول بيانات اعتماد المفتاح العام، ابحث عن بيانات الاعتماد
id
التي تتطابق مع بيانات الاعتمادid
المتوفّرة في العنصرPublicKeyCredential
.
تحقّق من تأكيد المصادقة. يمكنك تسليم خطوة إثبات الملكية هذه إلى مكتبة FIDO على خادم الخادم، والتي ستقدّم عادةً وظيفة أداة مساعدة لهذا الغرض. يوفر SimpleWebAuthn، على سبيل المثال،
verifyAuthenticationResponse
. تعرَّف على ما يحدث خلف الكواليس في الملحق: التحقّق من استجابة المصادقة.لمنع إعادة تشغيل هجمات إعادة التشغيل، احذف اختبار التحقّق ممّا إذا كانت عملية إثبات الملكية ناجحة أم لا.
سجِّل دخول المستخدم. إذا نجحت عملية التحقّق، يمكنك تعديل معلومات الجلسة لوضع علامة على المستخدم بأنّه سجّل دخوله. قد تحتاج أيضًا إلى إرجاع كائن
user
إلى العميل، وبالتالي يمكن للواجهة الأمامية استخدام المعلومات المرتبطة بالمستخدم الذي سجّل دخوله حديثًا.
مثال على الرمز: التحقّق من المستخدم وتسجيل الدخول إليه
نحن نستخدم مكتبة SimpleWebAuthn في أمثلتنا. وفي هذه الحالة، ننقل التحقّق من استجابة المصادقة إلى وظيفة verifyAuthenticationResponse
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/signinResponse', csrfCheck, async (req, res) => {
const response = req.body;
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
// 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 {
// Find the credential stored to the database by the credential ID
const cred = Credentials.findById(response.id);
if (!cred) {
throw new Error('Credential not found.');
}
// Find the user - Here alternatively we could look up the user directly
// in the Users table via userHandle
const user = Users.findByPasskeyUserId(cred.passkey_user_id);
if (!user) {
throw new Error('User not found.');
}
// Base64URL decode some values
const authenticator = {
credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
credentialID: isoBase64URL.toBuffer(cred.id),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
authenticator,
requireUserVerification: false,
});
if (!verified) {
throw new Error('User verification failed.');
}
// Kill the challenge for this session.
delete req.session.challenge;
req.session.username = user.username;
req.session['signed-in'] = 'yes';
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).json({ error: e.message });
}
});
الملحق: التحقّق من استجابة المصادقة
يتضمن التحقق من استجابة المصادقة عمليات التحقق التالية:
- تأكَّد من أنّ رقم تعريف الجهة المحظورة يتطابق مع موقعك الإلكتروني.
- تأكَّد من أنّ مصدر الطلب يتطابق مع مصدر تسجيل الدخول إلى موقعك الإلكتروني. بالنسبة إلى تطبيقات Android، راجِع المقالة التحقّق من المصدر.
- تحقّق من أنّ الجهاز يمكنه تقديم التحدي الذي منحته إياه.
- تأكّد من أنّ المستخدم قد اتّبع المتطلبات التي تفرضها كجهة محظورة أثناء المصادقة. إذا كنت بحاجة إلى التحقق من المستخدم، تأكَّد من أنّ علامة
uv
(التي تحقّق منها المستخدم) فيauthenticatorData
هيtrue
. تأكَّد من أنّ علامةup
(مشاركة المستخدم الحالية) فيauthenticatorData
هيtrue
، لأنّ مفاتيح المرور مطلوبة دائمًا. - التأكّد من صحة التوقيع: لإثبات صحة التوقيع، تحتاج إلى ما يلي:
- التوقيع، وهو التحدّي الموقَّع:
response.signature
- المفتاح العام لإثبات صحة التوقيع
- البيانات الأصلية الموقعة. هذه هي البيانات التي يجب التحقق من توقيعها.
- خوارزمية التشفير التي تم استخدامها لإنشاء التوقيع.
- التوقيع، وهو التحدّي الموقَّع:
لمعرفة المزيد من المعلومات عن هذه الخطوات، يمكنك الاطّلاع على رمز مصدر verifyAuthenticationResponse
في SimpleWebAuthn أو الاطّلاع على القائمة الكاملة لعمليات إثبات الملكية في المواصفات.