عندما ينقر مستخدم غير مصادَق عليه على رابط "تسجيل الدخول" أو يبدأ عملية الدفع أو يحفظ عنصرًا في المفضّلة أو يصل إلى حاجز الدفع لمقالة، يتم توجيهه إلى صفحة تسجيل دخول مخصّصة، ما يؤدي إلى فقدان السياق وزيادة معدّل مغادرة المسار للمستخدمين. يتيح وضع واجهة المستخدم الفورية (uiMode: 'immediate' في navigator.credentials.get()) تسجيل الدخول السريع في السياق من خلال عرض محدّد حساب موحّد في المتصفّح فورًا عند توفّر مفاتيح مرور محلية (publicKey) أو كلمات مرور محفوظة (password: true) على الجهاز.
على عكس طلبات WebAuthn العادية التي تظهر في نافذة منبثقة وتعرض دائمًا واجهة مستخدم المتصفّح وتعود إلى رموز الاستجابة السريعة على أجهزة متعددة أو مفاتيح الأمان الخارجية، يتوقف وضع "واجهة المستخدم الفورية" بسرعة مع ظهور الخطأ NotAllowedError إذا لم تكن هناك بيانات اعتماد مؤهَّلة متاحة على الجهاز. يتيح ذلك لتطبيقك مصادقة المستخدمين المكرّري الزيارة في مكانهم بنقرة واحدة، مع نقل المستخدمين الذين ليس لديهم بيانات اعتماد محلية بسلاسة إلى صفحة تسجيل الدخول العادية أو صفحة إتمام الدفع بلا تسجيل دخول أو نموذج تسجيل الدخول المضمّن.
أنماط تجربة المستخدم المقترَحة والأنماط المضادة
تفعيل وضع واجهة المستخدم الفورية داخل معالج أحداث يبدأه المستخدم (يتطلّب تفعيل المستخدم المؤقت) في لحظات تكون فيها نية المستخدم واضحة قبل الانتقال من الصفحة الحالية إذا نجحت المصادقة الفورية، يجب إكمال الإجراء الذي ينوي المستخدم تنفيذه على الفور (سواء من خلال تعديل الصفحة الحالية في مكانها أو الانتقال مباشرةً إلى الوجهة التي تمت المصادقة عليها). إذا تعذّر ذلك وظهرت رسالة الخطأ NotAllowedError، اعرِض مسار تسجيل الدخول أو التسجيل مع مسار لإكمال الإجراء الذي ينوي المستخدم تنفيذه عند الانتهاء.
حالات الاستخدام المحتملة:
- اعتراض رابط أو زر تسجيل الدخول في العنوان: يمكنك اعتراض نقرة على رابط "تسجيل الدخول" العام (
event.preventDefault()) لمصادقة المستخدمين المكرري الزيارة على الفور، أو مواصلة الانتقال إلى صفحة تسجيل الدخول المخصّصة علىNotAllowedError. - تسجيل الدخول الديناميكي قبل الدفع: يمكنك مصادقة المتسوّقين المتكررين بنقرة واحدة عندما ينقرون على "المتابعة إلى الدفع" ليتم توجيههم مباشرةً إلى خطوة الشحن والدفع، أو الانتقال إلى شاشة تسجيل الدخول العادية أو شاشة إتمام الدفع بلا تسجيل دخول في حال حدوث خطأ.
- تسجيل الدخول المضمّن للإجراءات المصغّرة (الإضافة إلى المفضّلة، الإعجاب، الإضافة إلى الإشارات المرجعية، الحفظ): يمكنك المصادقة في المكان نفسه عندما ينقر مستخدم سجّل خروجه على زر إجراء مصغّر وإكمال الإجراء على الفور (على سبيل المثال، تبديل
aria-pressed="true")، أو فتح نافذة تسجيل دخول احتياطية أو صفحة تسجيل دخول باستخدام مَعلمة عنوان URL للإرجاع. - حظر مقيّد بعدد القراءات ضمن المقال: يمكنك مصادقة القرّاء في المكان نفسه عندما ينقرون على "مواصلة القراءة باستخدام حسابك" في بانر حاجز الدفع، أو توسيع نموذج تسجيل الدخول أو التسجيل المضمّن في حال تعذّر ذلك.
الأنماط المضادة التي يجب تجنّبها:
- لا تستخدِم وضع واجهة المستخدم الفورية في صفحة تسجيل دخول مخصّصة: لا تربط أبدًا
uiMode: 'immediate'بالزر الأساسي "تسجيل الدخول" أو "تسجيل الدخول باستخدام مفتاح مرور" في صفحة تسجيل الدخول للوجهة النهائية. يصل المستخدمون إلى صفحة تسجيل الدخول المخصّصة تحديدًا عندما لا تكون لديهم بيانات اعتماد محلية على الجهاز الحالي، أو عندما ينوون استخدام مفتاح مرور على أجهزة متعددة (مسح رمز استجابة سريعة ضوئيًا باستخدام هاتف) أو مفتاح أمان خارجي. بما أنّuiMode: 'immediate'يمنع ظهور خيارات رمز الاستجابة السريعة ومفتاح الأمان على جميع الأجهزة ويعرض على الفورNotAllowedErrorفي حال عدم توفّر بيانات اعتماد محلية، فإنّ استخدامه في صفحة تسجيل دخول مخصّصة يضع المستخدم في مأزق حيث لا يؤدي النقر على الزر إلى أي نتيجة. في صفحة تسجيل الدخول المخصّصة، استخدِم واجهة المستخدم المشروطة (mediation: 'conditional'معautocomplete="username webauthn") في حقول إدخال النماذج المقترنة بزرnavigator.credentials.get({ publicKey })عادي في نافذة مشروطة (راجِع المصادقة باستخدام مفتاح المرور). - لا تستخدِم وضع واجهة المستخدم الفورية لإعادة المصادقة الإضافية: لا تستخدِم أبدًا
uiMode: 'immediate'لإعادة مصادقة مستخدم سجّل الدخول قبل العمليات الحسّاسة (مثل تغيير كلمة مرور أو تعديل عنوان شحن أو تعديل طرق الدفع المحفوظة أو إضافة مفتاح مرور جديد). يجب أن تقتصر إعادة المصادقة على بيانات اعتماد المستخدم الذي سجّل الدخول من خلال قائمةallowCredentialsغير فارغة، والتي يرفضها وضع واجهة المستخدم الفورية معNotAllowedError، وسيؤدي حذفallowCredentialsإلى السماح للمستخدم باختيار حساب مختلف عن الحساب الذي سجّل الدخول إليه حاليًا عن غير قصد. لإعادة المصادقة الإضافية، اتّبِع الخطوات الواردة في مقالة إعادة المصادقة باستخدام مفتاح المرور.
قيود الخصوصية والأمان
تفرض المتصفّحات قواعد صارمة للخصوصية والأمان عند طلب uiMode: 'immediate':
- يجب تفعيل المستخدم المؤقت: يجب استدعاء
navigator.credentials.get({ uiMode: 'immediate' })داخل معالج أحداث يبدأه المستخدم (مثل مستمعclickعلى عنصر<button>أو<a>) لمنع فحص بيانات الاعتماد الصامت. لا تستدعيها تلقائيًا عند تحميل الصفحة (DOMContentLoaded). لا يؤدي طلب واجهة برمجة التطبيقات إلى استهلاك التفعيل المؤقت. - عدم تمرير قائمة
allowCredentialsغير فارغة: يجب عدم تعبئةpublicKey.allowCredentialsعند تحديدuiMode: 'immediate'. ترفض المتصفّحات على الفور أي طلب لوضع واجهة المستخدم الفورية يتضمّن قائمةallowCredentialsغير فارغة من خلال عرض الخطأNotAllowedError، وبالتالي لا يمكن للأطراف المعتمِدة البحث عن معرّفات بيانات اعتماد معيّنة لاستنتاج الزيارات السابقة أو تتبُّع المستخدمين على مستوى الجلسات. - عدم تمرير السمة
AbortSignal(signal): يجب عدم ضبط السمةsignalفيnavigator.credentials.get()عند تحديدuiMode: 'immediate'، لأنّ المتصفّحات لا تسمح بإغلاق مربّع حوار تسجيل الدخول الفوري آليًا. إذا كانت صفحتك تُشغّل أيضًا طلب واجهة مستخدم شرطية (mediation: 'conditional') في الخلفية، يمكنك استدعاءabort()في واجهة المستخدم الشرطية هذهAbortControllerقبل استدعاء وضع واجهة المستخدم الفورية، وحذفsignalمن الطلب الفوري نفسه. - جلسات التصفّح المتخفي والتصفّح بخصوصية تامة: يتم دائمًا رفض الطلبات المقدَّمة في نوافذ التصفّح المتخفي أو التصفّح بخصوصية تامة باستخدام
NotAllowedErrorلكي تظل الجلسات الخاصة غير قابلة للتمييز عن الجلسات التي لا تتضمّن بيانات اعتماد محلية.
الإعداد من جهة الخادم
نقطة نهاية إنشاء الخيارات
يجب عرض نقطة نهاية تنشئ خيارات طلب WebAuthn قابلة للاكتشاف للزوّار غير المصادَق عليهم:
- إنشاء تحدٍ مرتبط بالجلسة: أنشِئ مخزنًا مؤقتًا عشوائيًا للتحدي المشفّر، وخزِّنه في جلسة الخادم، ورمِّزه باستخدام Base64URL.
- الاحتفاظ بخيار التحقّق من المستخدم: اضبط
userVerification: 'preferred'وخزِّنexpectedUserVerification: 'preferred'في الجلسة. - حذف
allowCredentials: لا تُدرِج قائمةallowCredentialsغير فارغة في خيارات وضع واجهة المستخدم الفورية، لأنّ المتصفّحات ترفض القوائم المسموح بها غير الفارغة التي تحتوي علىNotAllowedError.
// Server-side options generation example (/api/login/options)
router.post('/api/login/options', (req, res) => {
const options = {
challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
rpId: 'example.com',
userVerification: 'preferred',
};
req.session.challenge = options.challenge;
req.session.expectedUserVerification = 'preferred';
return res.json(options);
});
نقاط نهاية مزدوجة للتحقّق من بيانات الاعتماد
بما أنّ navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) يمكن أن يحلّ المشكلة باستخدام مفتاح مرور (PublicKeyCredential) أو كلمة مرور محفوظة (PasswordCredential)، يجب توفير معالِجات التحقّق لكلا نوعَي بيانات الاعتماد:
- التحقّق من تأكيد WebAuthn (
/api/login/verify): يتم التحقّق من التأكيد مقابل طلب الجلسة والمفتاح العام المخزَّن. إذا لم يتم العثور على معرّف بيانات الاعتماد في قاعدة البيانات، يجب عرض حالة HTTP404ليتمكّن العميل من إرسال إشعار إلى مدير كلمات المرور من خلالPublicKeyCredential.signalUnknownCredential(). - التحقّق من كلمة المرور (
/api/login/password): يتم التحقّق منusername(cred.id) وpassword(cred.password) من خلال قاعدة بيانات المستخدمين وإنشاء جلسة مصادقة.
التنفيذ من جهة العميل
طلب بيانات اعتماد موحّد
- طلب كل من مفاتيح المرور وكلمات المرور: يمكنك فك ترميز خيارات الخادم باستخدام
PublicKeyCredential.parseRequestOptionsFromJSON()واستدعاءnavigator.credentials.get()داخل معالج نقرات يبدأه المستخدم باستخدامpassword: trueوpublicKeyوuiMode: 'immediate'(بدون تمريرsignal). - التحقّق من الفرع من قِبل
cred.type:- إذا كانت القيمة
cred.type === 'public-key'، يمكنك تسلسل التأكيد باستخدامcred.toJSON()وإرساله إلى نقطة نهاية التحقّق من WebAuthn. عندما يستجيب الخادم برمز الحالة HTTP404، يمكنك استدعاءPublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }). - إذا كانت القيمة
cred.type === 'password'، أرسِل طلب POST{ username: cred.id, password: cred.password }إلى نقطة نهاية التحقّق من كلمة المرور.
- إذا كانت القيمة
- التعامل مع
NotAllowedErrorفي حال عدم توفّر بيانات اعتماد محلية: يمكنك رصدNotAllowedError(يتم عرضه فورًا في حال عدم توفّر مفاتيح مرور محلية أو كلمات مرور محفوظة على الجهاز، أو عندما يغلق المستخدم أداة الاختيار) والانتقال إلى مسار تسجيل الدخول العادي (مثل فتح مربّع حوار تسجيل الدخول المضمّن أو الانتقال إلى/signinباستخدام عنوان URL للعودة).
let isAuthenticating = false;
async function authenticateWithImmediateUi() {
const optionsResponse = await fetch('/api/login/options', { method: 'POST' });
if (!optionsResponse.ok) {
throw new Error('Failed to fetch authentication options');
}
const optionsJSON = await optionsResponse.json();
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON);
// MANDATORY: Request both password and publicKey with uiMode: 'immediate' and NO signal parameter
const cred = await navigator.credentials.get({
password: true,
publicKey,
uiMode: 'immediate',
});
if (!cred) {
throw new DOMException('No credential returned', 'NotAllowedError');
}
if (cred.type === 'public-key') {
const encoded = cred.toJSON();
const verifyResponse = await fetch('/api/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(encoded),
});
if (verifyResponse.status === 404) {
await PublicKeyCredential.signalUnknownCredential({
rpId: publicKey.rpId || window.location.hostname,
credentialId: encoded.id, // Base64URL-encoded credential ID string
});
throw new Error('Passkey credential is no longer recognized by the server');
}
if (!verifyResponse.ok) {
throw new Error('Passkey verification failed');
}
return await verifyResponse.json();
}
if (cred.type === 'password') {
const verifyResponse = await fetch('/api/login/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: cred.id,
password: cred.password,
}),
});
if (!verifyResponse.ok) {
throw new Error('Password verification failed');
}
return await verifyResponse.json();
}
throw new Error(`Unsupported credential type: ${cred.type}`);
}
// Example: Intercepting an inline micro-action (Favorite button) or Header Sign-in click
const favoriteButton = document.getElementById('favorite-btn');
favoriteButton.addEventListener('click', async (event) => {
event.preventDefault();
if (isAuthenticating) return;
// If already signed in, toggle the micro-action state immediately
if (document.body.dataset.signedIn === 'true') {
const nextPressed = favoriteButton.getAttribute('aria-pressed') !== 'true';
favoriteButton.setAttribute('aria-pressed', String(nextPressed));
return;
}
isAuthenticating = true;
try {
const user = await authenticateWithImmediateUi();
// Complete the user's intended action immediately
document.body.dataset.signedIn = 'true';
favoriteButton.setAttribute('aria-pressed', 'true');
updateHeaderUserState(user);
} catch (err) {
if (err.name === 'NotAllowedError') {
// No local credentials available or user dismissed dialog:
// transition smoothly to fallback sign-in UI or redirect with return URL
showFallbackSignInUi({ returnAction: 'favorite' });
return;
}
console.error('Authentication error:', err);
} finally {
isAuthenticating = false;
}
});
استراتيجيات احتياطية
العودة الفورية إلى وضع واجهة المستخدم
- تجربة التحسين التدريجي: يمكنك التعامل مع
uiMode: 'immediate'كتحسين تدريجي. لا تستنتج التوافق من اسم المتصفّح أو إصداره، لأنّ إشارة التوافق الوحيدة الموثوقة هي فحص وقت التشغيلcapabilities.immediateGetمن خلالPublicKeyCredential.getClientCapabilities(). إلزامي: يجب دائمًا التحقّق منcapabilities.immediateGetقبل استدعاءnavigator.credentials.get({ uiMode: 'immediate' })، لأنّ المتصفّحات التي لا تتعرّف علىuiMode: 'immediate'تتجاهل مفتاح القاموس غير المعروف وتفتح مربّع حوار يحظر مفتاح المرور ورمز الاستجابة السريعة. عندما تكون قيمةcapabilities.immediateGetخاطئة أو عندما يرفضnavigator.credentials.get()استخدامNotAllowedError، يمكنك الرجوع إلى تجربة تسجيل الدخول العادية في تطبيقك (مثل فتح نافذة تسجيل دخول مضمّنة أو الانتقال إلى صفحة تسجيل الدخول المخصّصة باستخدام مَعلمة عنوان URL للإرجاع)، لكي يرى المستخدمون في المتصفّحات غير المتوافقة مسار تسجيل الدخول الحالي بدون تغيير:
async function isImmediateUiSupported() {
if (!window.PublicKeyCredential || !PublicKeyCredential.getClientCapabilities) {
return false;
}
try {
const capabilities = await PublicKeyCredential.getClientCapabilities();
return Boolean(capabilities?.immediateGet);
} catch {
return false;
}
}
async function authenticateWithImmediateUi() {
// MANDATORY: Check immediateGet support so unsupported browsers do not open a modal QR dialog
const supported = await isImmediateUiSupported();
if (!supported) {
// Throw NotAllowedError so the caller's single catch path treats "unsupported" like "no local credentials"
throw new DOMException('Immediate UI mode is not supported', 'NotAllowedError');
}
// Proceed with fetching options and calling navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' })
}
الاحتياط لرصد إمكانات العميل
يجب دائمًا تثبيت 'webauthn-polyfills' واستيرادها في وحدة العميل لكي تكون PublicKeyCredential.getClientCapabilities() متاحة في أي وقت تتوفّر فيه PublicKeyCredential:
import 'webauthn-polyfills';
الاحتفاظ بنسخة احتياطية من بيانات اعتماد كلمات المرور
يؤدي تمرير password: true مع publicKey في navigator.credentials.get() إلى السماح للمتصفّحات المتوافقة بتضمين كلمات المرور المحفوظة من مدير كلمات المرور في المتصفّح في "محدّد الحساب" الموحّد الفوري.
- تجربة الاحتفاظ بنسخة احتياطية: تتجاهل المتصفحات التي لا تتيح استخدام
PasswordCredentialالخيارpassword: trueأو تعود إلى نموذج تسجيل الدخول العادي من خلال التحقّق من ميزةcapabilities.immediateGet، حيث يوفّر<input autocomplete="username webauthn">و<input type="password" autocomplete="current-password">ميزة الملء التلقائي لكلمات المرور الأصلية.
سهولة استخدام JSON كبديل في حال تعذُّر التسلسل
يجب دائمًا تثبيت 'webauthn-polyfills' واستيرادها في حزمة العميل لكي تعمل PublicKeyCredential.parseRequestOptionsFromJSON() وPublicKeyCredential.prototype.toJSON() بشكل متّسق على جميع المتصفّحات التي تتيح استخدام PublicKeyCredential:
import 'webauthn-polyfills';
الاحتياطي لمزامنة واجهة برمجة التطبيقات Signal
واجهة برمجة التطبيقات WebAuthn Signal API (webauthn-signals) هي تحسين تدريجي يزيل مفاتيح المرور القديمة من مدير كلمات المرور الخاص بالمستخدم عندما يستجيب الخادم برمز HTTP 404.
- التجربة البديلة: حظر المكالمات من خلال
if (PublicKeyCredential.signalUnknownCredential)في حال عدم توفّرها، يمكنك تخطّي طلب الإشارة بسلاسة وعرض رسالة واضحة توجّه المستخدم إلى تسجيل الدخول باستخدام طريقة بديلة.