אימות באמצעות מפתח גישה

במדריך הזה מוסבר איך להטמיע אימות של משתמשים חוזרים באמצעות פרטי כניסה שניתן לגלות, גם באמצעות טריגרים מפורשים של לחצנים וגם באמצעות הצעות למילוי אוטומטי בדפדפן (ממשק משתמש מותנה).

בצד השרת

יצירת אפשרויות

יוצרים נקודת קצה שמפיקה פרמטרים של בקשות 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 של הסשן ביקש זאת, על ידי העברת requireUserVerification: false לספריית האימות בצד השרת."preferred" אם מוצגת בקשה "required", צריך להזין את המידע הביומטרי או את קוד האימות.
  3. שגיאה 404 בחיבור לשרת: אם מזהה פרטי הכניסה שהלקוח החזיר לא נמצא במסד הנתונים, מחזירים שגיאת HTTP 404 מפורשת כדי שהלקוח יוכל להפעיל את Signal API.

לוגיקה בצד הלקוח

הערת טופס ב-HTML

כדי להשתמש בממשק המשתמש המותנה באופן מקורי, מוסיפים הערות לשדות של שם המשתמש והסיסמה. אסימוני השלמה אוטומטית משלבים את הפרמטרים של מפרט 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>

תהליך מפורש של לחיצה על כפתור

הפעלת אימות באמצעות מפתח גישה כשמשתמש לוחץ על הלחצן "כניסה באמצעות מפתח גישה". לפני שמוצגת ההנחיה להשתמש במפתח הגישה, צריך לבטל את כל הקריאות למילוי אוטומטי של טפסים (Conditional Get) שמתבצעות.

תהליך מותנה של בחירת רשת (מילוי אוטומטי של טופס)

כדי להציע אימות באמצעות מפתח גישה באופן מקורי כשמשתמשים מתמקדים בשדות הכניסה, מפעילים הצעות למילוי אוטומטי של טפסים בטעינת הדף:

  1. זיהוי תכונות: קוראים ל-PublicKeyCredential.getClientCapabilities() בטעינת הדף ומדלגים על הכניסה באמצעות מפתח גישה אם conditionalGet לא זמין.
  2. אפשרויות פענוח: פענוח של אובייקט JSON של פרטי הכניסה שאוחזרו באמצעות PublicKeyCredential.parseRequestOptionsFromJSON().
  3. הפעלת Get מותנה: קוראים ל-navigator.credentials.get() עם mediation: "conditional" ומעבירים אות AbortController. הפעולה הזו רושמת את המילוי האוטומטי באופן שקט בלי להציג תיבת דו-שיח של מפתח גישה.
  4. הפרדת חריגים של Try/Catch: עוטפים את הקריאה ל-navigator.credentials.get בבלוק try/catch:
    • ‫NotAllowedError: המשתמש ביטל את בקשת ההתחברות באמצעות מפתח הגישה או שהזמן הקצוב לתפוגה של הבקשה חלף.
    • ‫AbortError: בקשת האימות בוטלה באופן פרוגרמטי.
  5. ‫Call Signal API: עוטפים את הקריאה fetch() לאימות השרת בבלוק try/catch:
    • הצגת הודעת שגיאה כדי שהמשתמש יבין מה השתבש.
    • צריך להפעיל את 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';