ورود سریع به سیستم با حالت واسط کاربر فوری

هدایت کردن کاربر احراز هویت‌نشده به صفحه ورود به سیستم اختصاصی وقتی روی پیوند «ورود به سیستم» کلیک می‌کند، تسویه‌حساب را شروع می‌کند، موردی را در موارد دلخواه ذخیره می‌کند، یا به دیوار پرداخت مقاله می‌رسد باعث ازدست رفتن بافت و افزایش ریزش کاربر می‌شود. حالت میانای کاربری فوری (uiMode: 'immediate' در navigator.credentials.get()) با ارائه فوری انتخابگر حساب مرورگر یکپارچه وقتی گذرکلیدهای محلی (publicKey) یا گذرواژه‌های ذخیره‌شده (password: true) در دستگاه وجود دارد، ورودهای سریع به سیستم بافتی را فعال می‌کند.

برخلاف پیام‌واره‌های WebAuthn استاندارد—که همیشه واسط کاربر مرورگر را نمایش می‌دهند و به رمزینه‌های پاسخ‌سریع بین‌دستگاهی یا کلیدهای امنیتی خارجی برمی‌گردند—حالت «واسط کاربر فوری» اگر هیچ اطلاعات کاربری واجدشرایطی به‌صورت محلی در دستگاه دردسترس نباشد، به‌سرعت با NotAllowedError ناموفق می‌شود. این ویژگی به برنامه شما امکان می‌دهد کاربران بازگشتی را در همان‌جا با یک تک‌ضرب اصالت‌سنجی کند و درعین‌حال کاربران فاقد اطلاعات کاربری محلی را به‌طور یکپارچه به صفحه ورود به سیستم استاندارد، تسویه‌حساب به‌عنوان مهمان، یا فرم ورود به سیستم درون‌خطی شما منتقل کند.

حالت «واسط کاربر فوری» را در داخل یک گرداننده رویداد آغازشده توسط کاربر (که نیاز به فعال‌سازی موقت کاربر دارد) در لحظاتی که قصد کاربر واضح است، درست قبل‌از خروج از صفحه فعلی، راه‌اندازی کنید. اگر اصالت‌سنجی فوری موفقیت‌آمیز بود، قصد کاربر را فوراً تکمیل کنید (چه با به‌روزرسانی صفحه فعلی در محل یا رفتن مستقیم به مقصد اصالت‌سنجی‌شده). اگر با NotAllowedError ناموفق بود، جریان ورود به سیستم یا ثبت‌نام را با مسیری برای تکمیل هدف کاربر در تکمیل نشان دهید.

موارد استفاده بالقوه:

  • رهگیری دکمه یا پیوند ورود به سیستم در سرصفحه: رهگیری کلیک روی پیوند سراسری «ورود به سیستم» (event.preventDefault()) برای اصالت‌سنجی فوری کاربران بازگشتی، یا ادامه پیمایش به صفحه ورود به سیستم اختصاصی در NotAllowedError.
  • ورود به سیستم پویا قبل‌از تسویه‌حساب: خریداران بازگشتی را با یک تک‌ضرب هنگام کلیک روی «ادامه به تسویه‌حساب» اصالت‌سنجی کنید تا مستقیماً به مرحله ارسال و پرداخت بروند، یا درصورت عدم موفقیت، به صفحه ورود به سیستم استاندارد یا تسویه‌حساب به‌عنوان مهمان منتقل شوند.
  • ورود به سیستم درون‌خطی برای کنش‌های کوچک (افزودن به موارد دلخواه، پسندیدن، نشانک‌گذاری، ذخیره کردن): وقتی کاربر خارج‌شده از سیستم روی دکمه کنش کوچک کلیک می‌کند، اصالت‌سنجی را در همان‌جا انجام دهید و بلافاصله کنش را تکمیل کنید (برای مثال، تغییر وضعیت aria-pressed="true")، یا یک مدال ورود به سیستم جایگزین یا صفحه ورود به سیستم با پارامتر نشانی وب بازگشت باز کنید.
  • دیوار نسبتاً پولی درون مقاله: وقتی خوانندگان روی برنمای دیوار پولی «ادامه خواندن با حساب شما» کلیک می‌کنند، اصالت آن‌ها را در همان‌جا تأیید می‌کند یا درصورت عدم موفقیت، فرم ورود به سیستم یا ثبت‌نام درون‌خطی را گسترش می‌دهد.

الگوهای منفی که باید از آن‌ها اجتناب کرد:

  • از حالت «رابط کاربری فوری» در صفحه ورود به سیستم اختصاصی استفاده نکنید: هرگز 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) فراخوانی نکنید. فراخوانی کردن API، فعال‌سازی گذرا را مصرف نمی‌کند.
  • فهرست allowCredentials غیرخالی را ارسال نکنید: وقتی uiMode: 'immediate' مشخص شده است، publicKey.allowCredentials را پر نکنید. مرورگرها بلافاصله هرگونه درخواست حالت «رابط کاربری فوری» را که فهرست allowCredentials غیرخالی داشته باشد با ایجاد NotAllowedError رد می‌کنند تا طرف‌های متکی نتوانند شناسه‌های اطلاعات اعتباری خاص را برای استنباط بازدیدهای قبلی یا ردیابی کاربران در جلسه‌ها کاوش کنند.
  • AbortSignal (signal) را ارسال نکنید: وقتی uiMode: 'immediate' مشخص شده است، ویژگی signal را در navigator.credentials.get() تنظیم نکنید، زیرا مرورگرها رد کردن برنامه‌ای کادر گفتگوی ورود فوری را مجاز نمی‌دانند. اگر صفحه‌تان درخواست «واسط کاربر شرطی» (mediation: 'conditional') پس‌زمینه‌ای را نیز اجرا می‌کند، قبل‌از فراخوانی حالت «واسط کاربر فوری»، abort() را در آن «واسط کاربر شرطی» AbortController فراخوانی کنید و signal را از خود درخواست فوری حذف کنید.
  • جلسه‌های مرور خصوصی و ناشناس: درخواست‌های ایجادشده در پنجره‌های مرور خصوصی یا ناشناس همیشه با NotAllowedError رد می‌شوند تا جلسه‌های خصوصی از جلسه‌های بدون اطلاعات کاربری محلی قابل‌تشخیص نباشند.

راه‌اندازی سمت سرور

نقطه پایان تولید گزینه‌ها

نقطه پایانی‌ای را افشا کنید که گزینه‌های درخواست WebAuthn قابل‌شناسایی را برای بازدیدکنندگان احراز هویت‌نشده تولید می‌کند:

  1. تولید چالش محدود به جلسه: یک میان‌گیر چالش تصادفی رمزنگاری‌شده ایجاد کنید، آن را در جلسه سرور ذخیره کنید، و آن را به‌عنوان Base64URL کدبندی کنید.
  2. Persist user verification preference: Set userVerification: 'preferred' and store expectedUserVerification: 'preferred' in the session.
  3. حذف 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) حل شود، برای هر دو نوع اعتبارنامه، گرداننده درستی‌سنجی ارائه دهید:

  1. درستی‌سنجی ادعای WebAuthn‏ (/api/login/verify): ادعا را دربرابر چالش جلسه و کلید عمومی ذخیره‌شده درستی‌سنجی کنید. اگر شناسه اطلاعات کاربری در پایگاه داده پیدا نشد، وضعیت HTTP 404 را برگردانید تا کارخواه بتواند ازطریق PublicKeyCredential.signalUnknownCredential() به مدیر گذرواژه اطلاع دهد.
  2. درستی‌سنجی گذرواژه (/api/login/password): username (cred.id) و password (cred.password) را درمقابل پایگاه داده کاربر درستی‌سنجی کنید و جلسه اصالت‌سنجی‌شده‌ای برقرار کنید.

پیاده‌سازی سمت کارخواه

درخواست اطلاعات اعتباری یکپارچه

  1. درخواست گذرکلید و گذرواژه: گزینه‌های سرور را بااستفاده از PublicKeyCredential.parseRequestOptionsFromJSON() رمزگشایی کنید و navigator.credentials.get() را در گرداننده کلیک آغازشده توسط کاربر با password: true، publicKey، و uiMode: 'immediate' (بدون ارسال signal) فراخوانی کنید.
  2. درستی‌سنجی شعبه توسط cred.type:
    • اگر cred.type === 'public-key'، ادعا را با cred.toJSON() سریال‌سازی کنید و آن را به نقطه پایان درستی‌سنجی «اصالت‌سنجی وب» خود ارسال کنید. وقتی سرور با HTTP 404 پاسخ می‌دهد، با PublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }) تماس بگیرید.
    • اگر cred.type === 'password'،‏ { username: cred.id, password: cred.password } را به نقطه پایانی درستی‌سنجی گذرواژه خود ارسال کنید.
  3. مدیریت NotAllowedError وقتی اعتبارنامه محلی وجود ندارد: NotAllowedError را بگیرید (بلافاصله وقتی گذرکلید محلی یا گذرواژه ذخیره‌شده‌ای در دستگاه وجود ندارد یا وقتی کاربر انتخاب‌کننده را رد می‌کند، ایجاد می‌شود) و به جریان ورود به سیستم استاندارد خود بروید (مانند باز کردن کادر گفتگوی ورود به سیستم درون‌خطی یا پیمایش به /signin با نشانی وب بازگشت).
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() است. اجباری: همیشه قبل‌از فراخوانی navigator.credentials.get({ uiMode: 'immediate' })، capabilities.immediateGet را بررسی کنید—مرورگرهایی که uiMode: 'immediate' را تشخیص نمی‌دهند، کلید واژه‌نامه ناشناخته را نادیده می‌گیرند و یک کادر گفتگوی رمزینه پاسخ‌سریع و گذرکلید مسدودکننده را باز می‌کنند. وقتی capabilities.immediateGet نادرست است، یا وقتی navigator.credentials.get() با NotAllowedError رد می‌شود، به تجربه ورود به سیستم استاندارد برنامه‌تان برگردید (مانند باز کردن یک مدال ورود به سیستم درون‌خطی یا پیمایش به صفحه ورود به سیستم اختصاصی با پارامتر نشانی وب بازگشت)، تا کاربران در مرورگرهای پشتیبانی‌نشده جریان ورود به سیستم موجود شما را بدون تغییر ببینند:
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 پشتیبانی شد، PublicKeyCredential.getClientCapabilities() دردسترس باشد:

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 API

‫WebAuthn Signal API‏ (webauthn-signals) یک ارتقای پیش‌رو است که وقتی سرور با HTTP 404 پاسخ می‌دهد، گذرکلیدهای قدیمی را از مدیر گذرواژه کاربر حذف می‌کند.

  • تجربه جایگزین: تماس‌های دروازه پشت if (PublicKeyCredential.signalUnknownCredential). وقتی پشتیبانی نمی‌شود، تماس سیگنال را به‌طور مناسب رد کنید و پیام واضحی نمایش دهید که کاربر را برای ورود به سیستم با روشی جایگزین راهنمایی کند.