نظرة عامة
في ما يلي نظرة عامة عالية المستوى على الخطوات الرئيسية المتضمنة في مصادقة مفاتيح المرور:
- حدِّد اختبار التحقّق والخيارات الأخرى اللازمة للمصادقة باستخدام مفتاح مرور. عليك إرسالها إلى العميل لتتمكّن من تمريرها إلى مكالمة مصادقة مفتاح المرور (
navigator.credentials.get
على الويب). بعد تأكيد المستخدم لمصادقة مفتاح المرور، يتم التعامل بشكل نهائي مع طلب مصادقة مفتاح المرور ويعرض بيانات اعتماد (PublicKeyCredential
). تحتوي بيانات الاعتماد على تأكيد المصادقة.
- تحقق من تأكيد المصادقة.
- إذا كان تأكيد المصادقة صالحًا، يمكنك مصادقة المستخدم.
تتناول الأقسام التالية تفاصيل كل خطوة.
إنشاء التحدي
من الناحية العملية، يمثل التحدي مصفوفة من وحدات البايت العشوائية، ويتم تمثيلها ككائن ArrayBuffer
.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
ولضمان تحقيق التحدي الغرض منه، يجب:
- ضمان عدم استخدام التحدي نفسه أكثر من مرة: إنشاء اختبار تحقُّق جديد في كل محاولة تسجيل دخول تجاهل التحدي بعد كل محاولة تسجيل دخول، سواء نجحت في ذلك أم لم تنجح تجاهل التحدي بعد مدة معينة أيضًا. عدم قبول التحدي نفسه في رد أكثر من مرة
- التأكد من أن التحدي آمن من خلال التشفير. من المفترض أن يكون من المستحيل عمليًا تخمين التحدي. لإنشاء اختبار تحدي آمن من جهة الخادم، من الأفضل الاعتماد على مكتبة FIDO من جهة الخادم التي تثق بها. وإذا ابتكرت تحدياتك الخاصة، يمكنك استخدام وظائف التشفير المضمّنة المتاحة في حزمة التكنولوجيا أو البحث عن المكتبات المصمّمة لحالات استخدام التشفير. تتضمن الأمثلة iso-crypto في Node.js أو secrets في بايثون. وفقًا للمواصفات، يجب أن يبلغ طول اختبار التحقق 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 أو الاطّلاع على تفاصيل القائمة الكاملة لعمليات إثبات الملكية في المواصفات.