اصالت‌سنجی گذرکلید

این راهنما جزئیات نحوه پیاده‌سازی اصالت‌سنجی کاربر بازگشتی را بااستفاده از اطلاعات کاربری قابل‌کشف، هم ازطریق راه‌اندازهای دکمه صریح و هم ازطریق پیشنهادهای تکمیل خودکار مرورگر یکپارچه (رابط کاربری شرطی)، شرح می‌دهد.

سمت سرور

تولید کردن گزینه‌ها

نقطه پایانی‌ای ایجاد کنید که پارامترهای درخواست WebAuthn را بااستفاده از کتابخانه تأییدشده طبق استانداردها تولید کند.

  1. استفاده از شناسه RP ازپیش‌تعریف‌شده: از شناسه RP مناسب ازپیش‌تعریف‌شده به‌عنوان رشته ثابت استفاده کنید.
  2. تولید چالش: یک میان‌گیر تصادفی با آنتروپی بالا و ازنظر رمزنگاری ایمن تولید کنید، آن را به‌طور ایمن در جلسه کاربر ذخیره کنید، و آن را به‌عنوان Base64URL کدبندی کنید.
  3. نگاشت «اعتبارنامه‌های قابل‌کشف»: آرایه خالی [] را برای allowCredentials مشخص کنید. این درخواست اعتبارنامه‌های قابل‌کشف را می‌طلبد، به این معنی که کاربر نیازی به وارد کردن نام کاربری خود در ابتدا ندارد؛ ارائه‌دهنده گذرکلید حساب‌های موجود را ارائه خواهد داد.
  4. سطح درستی‌سنجی کاربر: userVerification: "preferred" را تنظیم کنید (یا "required" را اگر صریحاً توسط خط‌مشی‌های رعایت مقررات شرکتی الزامی شده است).
    • سطح محدودیت userVerification درخواست‌شده باید در سابقه جلسه سرور در نقطه پایانی گزینه‌ها حفظ شود، نه اینکه ازطریق رشته‌های پُرسمان از کارخواه برگردانده شود. این کار به نقطه پایانی درستی‌سنجی اجازه می‌دهد تا محدودیت‌های تطابق دقیق را به‌طور ایمن و بدون خطر دستکاری مشتری اعمال کند.
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

نقطه پایان درستی‌سنجی

برای اصالت‌سنجی کاربر، ادعای برگردانده‌شده توسط کارخواه را به‌طور ایمن درستی‌سنجی کنید:

  1. اعتبارسنجی چالش جلسه: مطابقت دقیق چالش بین پاسخ کارخواه و چالش موردانتظار ذخیره‌شده در جلسه را اعمال کنید.
  2. اجرای اولویت‌های UV:
    • اگر جلسه expectedUserVerification درخواست شده است"preferred"، با ارسال requireUserVerification: false به کتابخانه درستی‌سنجی سمت سرور، به اصالت‌سنج‌های بدون UV (مثلاً قفل‌های صفحه اصالت‌سنج غیرفعال‌شده) اجازه دهید. درصورت درخواست "required"، وارد کردن پین/ زیست‌سنجشی را به‌طور جدی اعمال کنید.
  3. خطای سرور Clean 404: اگر شناسه اطلاعات کاربری برگردانده‌شده توسط کارخواه در پایگاه داده پیدا نشد، خطای صریح HTTP 404 را برگردانید تا کارخواه بتواند Signal API را راه‌اندازی کند.

منطق سمت کارخواه

HTML Form Annotation

ورودی‌های نام کاربری و گذرواژه را حاشیه‌نویسی کنید تا به‌طور بومی از «میانای کاربری شرطی» بهره ببرید. نشان‌های تکمیل خودکار پارامترهای مشخصات webauthn را ترکیب می‌کنند و فوکوس خودکار باعث می‌شود وقتی ورودی فوکوس می‌شود، بالاپر تکمیل خودکار مرورگر فوراً فعال شود.

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

جریان دکمه صریح

وقتی کاربر روی دکمه «ورود به سیستم با گذرکلید» کلیک می‌کند، اصالت‌سنجی با گذرکلید را راه‌اندازی کنید. قبل‌از فراخوانی درخواست گذرکلید، هرگونه تماس تکمیل خودکار فرم (دریافت شرطی) درحال انجام را لغو کنید.

جریان میانجی‌گری مشروط (تکمیل خودکار فرم)

پیشنهادهای تکمیل خودکار فرم را هنگام بار شدن صفحه فعال کنید تا وقتی کاربران روی فیلدهای ورود به سیستم تمرکز می‌کنند، اصالت‌سنجی با گذرکلید به‌صورت بومی ارائه شود:

  1. تشخیص ویژگی: PublicKeyCredential.getClientCapabilities() را هنگام بار شدن صفحه فراخوانی کنید و اگر conditionalGet دردسترس نیست، از ورود به سیستم با گذرکلید صرف‌نظر کنید.
  2. گزینه‌های رمزگشایی: رمزگشایی شیء JSON اطلاعات کاربری واکشی‌شده با PublicKeyCredential.parseRequestOptionsFromJSON().
  3. فراخوانی Conditional Get: navigator.credentials.get() را با mediation: "conditional" فراخوانی کنید و سیگنال AbortController را ارسال کنید. این کار تکمیل خودکار را بدون نمایش کادر گفتگوی گذرکلید ثبت می‌کند.
  4. Try/Catch Exception Segregation: Wrap navigator.credentials.get call in try/catch block:
    • NotAllowedError: کاربر پیام‌واره ورود به سیستم با گذرکلید را لغو کرد یا زمان آن به‌پایان رسید.
    • AbortError: درخواست اصالت‌سنجی به‌صورت برنامه‌نویسی‌شده لغو شد.
  5. Call Signal API: Wrap server verification fetch() call in a try/catch block:
    • پیام خطایی به کاربر نشان دهید تا متوجه شود مشکل چیست.
    • signalUnknownCredential() را فقط زمانی فراخوانی کنید که سرور صریحاً با وضعیت HTTP 404 (اعتبارنامه پیدا نشد) پاسخ دهد و کاربر احراز هویت نشده باشد.
    • پارامتر credentialId که به signalUnknownCredential() ارسال شده است باید دقیقاً رشته شناسه اطلاعات کاربری کدبندی‌شده با Base64URL باشد (مثلاً encoded.id)، نه شیء ArrayBuffer خام credential.rawId.
  6. رمزگذاری پاسخ: اعتبارنامه AuthenticatorAssertionResponse را با .toJSON() رمزگذاری کنید و سپس آن را برای درستی‌سنجی به سرور ارسال کنید.
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

راهبردهای جایگزین

جایگزین تشخیص ویژگی گذرکلید

همیشه «webauthn-polyfills» را نصب کنید و آن را در بافت وارد کنید. درنظر بگیرید که تا زمانی که از PublicKeyCredential پشتیبانی می‌شود، از PublicKeyCredential.getClientCapabilities نیز پشتیبانی می‌شود.

import 'webauthn-polyfills';

جایگزین همگام‌سازی Signal API

WebAuthn Signal API‏ (webauthn-signals) یک بهینه‌سازی پیش‌رونده است که برای همگام نگه داشتن مدیران گذرواژه با وضعیت اعتبارنامه سرور استفاده می‌شود.

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

Easy JSON Serialization Fallback

همیشه «webauthn-polyfills» را نصب کنید و آن را در بافت وارد کنید. درنظر بگیرید که تا زمانی که PublicKeyCredential پشتیبانی می‌شود، PublicKeyCredential.parseRequestOptionsFromJSON و PublicKeyCredential.prototype.toJSON نیز پشتیبانی می‌شوند.

import 'webauthn-polyfills';