רישום מפתח גישה

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

דרישות לגבי מסד הנתונים

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

export interface StoredPasskeyCredential {
  id: string; // Base64URL-encoded credential ID (unique lookup key)
  passkeyUserId: string; // Associated application user ID
  credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
  credentialType: "public-key";
  credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
  credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
  aaguid: string; // Authenticator Attestation GUID
  providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
  name: string; // Provider name derived from AAGUID registry
  transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
  lastUsedAt?: number; // Optional epoch timestamp of last sign-in
  registeredAt: number; // Registration epoch timestamp
  counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}

בצד השרת

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

יוצרים נקודת קצה שמייצרת פרמטרים ליצירת WebAuthn. הסתמכו על ספרייה שנבדקה בהתאם לתקנים של הקטגוריה במקום להשתמש בקריפטוגרפיה ידנית.

  1. שימוש במזהה RP מוגדר מראש: משתמשים במזהה RP תקין ומוגדר מראש כמחרוזת קבועה.
  2. יצירת אתגר מאובטח: יוצרים מאגר אקראי מאובטח מבחינה קריפטוגרפית עם אנטרופיה גבוהה בשרת, מאחסנים אותו באופן מאובטח בסשן של המשתמש ומקודדים אותו כ-Base64URL למסירת אפשרויות.
  3. הימנעות ממפתחות גישה כפולים: ממפים את מזהי פרטי הכניסה הקיימים של המשתמש שנרשמו מראש למערך האפשרויות של excludeCredentials. כך המאמת לא רושם פרטי כניסה כפולים באותו חשבון של ספק מפתחות הגישה.
  4. אכיפת פרטי כניסה שניתן לגלות: מגדירים את requireResidentKey: true ו-residentKey: "required" באפשרויות של authenticatorSelection כדי לבקש פרטי כניסה שניתן לגלות, שנדרשים לכניסות שניתן לגלות.
  5. הגדרת אימות משתמש: מציינים userVerification: "preferred" או userVerification: "required". במקרים רבים של תאימות (למשל, בתחום הפיננסים או הבריאות) נדרש 'required' כדי לאכוף אימות משתמשים בזמן היצירה.
  6. הגדרת היקף הקובץ המצורף:
    • תהליך קידום: כשמציעים ליצור מפתח גישה מיד אחרי כניסה רגילה באמצעות סיסמה או קידום אחרי הרשמה, מגדירים את authenticatorAttachment: "platform" כדי לאכוף את מאמת הפלטפורמה ולעקוף את ההנחיות של מפתח האבטחה החיצוני.
    • תהליך הניהול: כשמפעילים את הפונקציה מלוח הגדרות או אבטחה ייעודי, שבו יש תמיכה במפתחות אבטחה חיצוניים בנוסף למאמת של הפלטפורמה, משמיטים את המאפיין authenticatorAttachment לגמרי.
    • טיפ: אפשר לאשר דגל בקשה של promotion: boolean כדי לטפל בשני התהליכים באופן מותנה באמצעות נקודת קצה אחת.
// Options generation example
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
  rp: { id: "example.com", name: "Secure Application" },
  user: {
    id: userBase64UrlId, // Unique base64url string identifying the account
    name: "user@example.com",
    displayName: "Jane Doe",
  },
  pubKeyCredParams: [
    {
      type: "public-key",
      alg: -7,
    },
    {
      type: "public-key",
      alg: -257,
    },
  ],
  excludeCredentials: userExistingCredentials.map((cred) => ({
    type: "public-key",
    id: cred.id,
    transports: cred.transports,
  })),
  authenticatorSelection: {
    residentKey: "required",
    requireResidentKey: true,
    userVerification: "preferred",
    ...(isPromotionFlow && { authenticatorAttachment: "platform" }),
  },
};

אימות

  1. אימות האתגר: מאמתים באופן מאובטח את האתגר מול האתגר הצפוי שקשור לסשן.
  2. אימות נוכחות המשתמש:
    • כדי לאשר שהמשתמש נוכח פיזית בזמן היצירה, צריך לוודא שהדגל "נוכחות משתמש" (UP) שמוחזר בנתוני המאמת המנותחים הוא true.
  3. הקלה באימות של "מועדף":
    • כשאפשרויות היצירה שצוינו הן userVerification: "preferred", חובה להגדיר את הקריאה לאימות בצד השרת עם requireUserVerification: false. אחרת, מאמתים שנרשמים בלי אימות משתמש (למשל, נעילות מסך מושבתות) יגרמו לכשלים מזויפים באימות השרת.

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

  1. הגבלת ממשק המשתמש בטעינת הדף:
    • בזמן טעינת הדף, צריך להפעיל את PublicKeyCredential.getClientCapabilities() ולהשבית את הלחצן "יצירת מפתח גישה" אם conditionalGet או passkeyPlatformAuthenticator לא זמינים.
  2. הפעלת היצירה והסדרה: מפענחים את אפשרויות השרת באמצעות PublicKeyCredential.parseCreationOptionsFromJSON() ומעבירים את ההגדרה שנוצרה אל navigator.credentials.create().
    • מפעילים את credential.toJSON() כדי לקודד את AuthenticatorAttestationResponse לאובייקט תקין שניתן לסדר ב-JSON, לפני שמאחזרים את נקודת הקצה לאימות.
  3. טיפול בחריגים של WebAuthn:
    • ‫InvalidStateError: כבר קיים מפתח גישה תואם (התאמה לפי excludeCredentials).
    • NotAllowedError: המשתמש ביטל את תיבת הדו-שיח של אימות מפתח הגישה או שהזמן הקצוב לתפוגה שלה חלף.
    • ‫AbortError: הפעולה בוטלה.
    • SecurityError: שגיאות שקשורות למקורות מאובטחים (HTTPS) או לחוסר התאמה במזהה ה-RP (בעיות בהגדרות).
  4. הפרדת Try/Catch ל-Signal API:
    • עוטפים את הקריאה לאימות השרת fetch() בבלוק try/catch. צריך להפעיל את signalUnknownCredential() כשהאחזור של אימות השרת נכשל (כל סטטוס response.ok === false או שגיאות רשת).
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";

async function registerPasskey(isPromotion = false) {
  // Verify passkey capability and conditional UI are available
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (
    !capabilities.passkeyPlatformAuthenticator ||
    !capabilities.conditionalGet
  ) {
    // Hide "Create passkey" buttons and fall back to password flows instead
    showStandardPasswordFallbackUI();
    return;
  }

  const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
  const publicKey =
    PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // passkey prompt execution
    credential = await navigator.credentials.create({ publicKey });
  } catch (err) {
    if (err.name === "InvalidStateError") {
      console.log("A passkey already exists for this account.");
      alert("A passkey already exists for this account.");
    } else if (err.name === "SecurityError") {
      console.error("Configuration RP ID or Secure Context error.");
      alert("Configuration RP ID or Secure Context error.");
    } else if (err.name === "NotAllowedError") {
      console.log("User cancelled the passkey dialog.");
    } else if (err.name === "AbortError") {
      console.log("The creation operation has been aborted.");
    }
    return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
  }

  // Server Verification phase (Segregated Try/Catch)
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // Server verification failed to verify/authenticate the credential (orphaned)
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Server verification network failure:", serverErr);
    await publickeycredential.signalunknowncredential({
      rpId, // RP ID must match the one defined on the server
      credentialid: encodedresponse.id, // base64url-encoded credential id
    });
  }
}

שיטות חלופיות

חלופה לסנכרון של Signal API

‫WebAuthn Signal API‏ (webauthn-signals) הוא אופטימיזציה מתקדמת שמשמשת לסנכרון של מנהלי הסיסמאות עם מצב פרטי הכניסה בשרת.

  • חוויית חלופה: אם הדפדפן לא תומך ב-PublicKeyCredential.signalUnknownCredential, חובה לעקוף את הקריאה בצורה בטוחה באמצעות שער לזיהוי תכונות (if (PublicKeyCredential.signalUnknownCredential)), והאימות בצד השרת פשוט מתעד את הכישלון בלי להפעיל עדכונים של המנהל.

Easy JSON Serialization Fallback

תמיד צריך להתקין את webauthn-polyfills ולייבא אותו בהקשר. כל עוד יש תמיכה ב-PublicKeyCredential, יש תמיכה גם ב-PublicKeyCredential.parseCreationOptionsFromJSON וב-PublicKeyCredential.prototype.toJSON.

import 'webauthn-polyfills';