במדריך הזה מוסבר איך לאפשר למשתמשים לרשום מפתח גישה לחשבון שלהם, כדי לספק חלופה מאובטחת מאוד לכניסה ללא סיסמה שעמידה בפני פישינג.
דרישות לגבי מסד הנתונים
כדי לתמוך ברישום של מפתחות גישה, טבלת פרטי הכניסה של מסד הנתונים צריכה לכלול את השדות הבאים:
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. הסתמכו על ספרייה שנבדקה בהתאם לתקנים של הקטגוריה במקום להשתמש בקריפטוגרפיה ידנית.
- שימוש במזהה RP מוגדר מראש: משתמשים במזהה RP תקין ומוגדר מראש כמחרוזת קבועה.
- יצירת אתגר מאובטח: יוצרים מאגר אקראי מאובטח מבחינה קריפטוגרפית עם אנטרופיה גבוהה בשרת, מאחסנים אותו באופן מאובטח בסשן של המשתמש ומקודדים אותו כ-Base64URL למסירת אפשרויות.
- הימנעות ממפתחות גישה כפולים: ממפים את מזהי פרטי הכניסה הקיימים של המשתמש שנרשמו מראש למערך האפשרויות של
excludeCredentials. כך המאמת לא רושם פרטי כניסה כפולים באותו חשבון של ספק מפתחות הגישה. - אכיפת פרטי כניסה שניתן לגלות: מגדירים את
requireResidentKey: trueו-residentKey: "required"באפשרויות שלauthenticatorSelectionכדי לבקש פרטי כניסה שניתן לגלות, שנדרשים לכניסות שניתן לגלות. - הגדרת אימות משתמש: מציינים
userVerification: "preferred"אוuserVerification: "required". במקרים רבים של תאימות (למשל, בתחום הפיננסים או הבריאות) נדרש'required'כדי לאכוף אימות משתמשים בזמן היצירה. - הגדרת היקף הקובץ המצורף:
- תהליך קידום: כשמציעים ליצור מפתח גישה מיד אחרי כניסה רגילה באמצעות סיסמה או קידום אחרי הרשמה, מגדירים את
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" }),
},
};
אימות
- אימות האתגר: מאמתים באופן מאובטח את האתגר מול האתגר הצפוי שקשור לסשן.
- אימות נוכחות המשתמש:
- כדי לאשר שהמשתמש נוכח פיזית בזמן היצירה, צריך לוודא שהדגל "נוכחות משתמש" (UP) שמוחזר בנתוני המאמת המנותחים הוא
true.
- כדי לאשר שהמשתמש נוכח פיזית בזמן היצירה, צריך לוודא שהדגל "נוכחות משתמש" (UP) שמוחזר בנתוני המאמת המנותחים הוא
- הקלה באימות של "מועדף":
- כשאפשרויות היצירה שצוינו הן
userVerification: "preferred", חובה להגדיר את הקריאה לאימות בצד השרת עםrequireUserVerification: false. אחרת, מאמתים שנרשמים בלי אימות משתמש (למשל, נעילות מסך מושבתות) יגרמו לכשלים מזויפים באימות השרת.
- כשאפשרויות היצירה שצוינו הן
לוגיקה בצד הלקוח
- הגבלת ממשק המשתמש בטעינת הדף:
- בזמן טעינת הדף, צריך להפעיל את
PublicKeyCredential.getClientCapabilities()ולהשבית את הלחצן "יצירת מפתח גישה" אםconditionalGetאוpasskeyPlatformAuthenticatorלא זמינים.
- בזמן טעינת הדף, צריך להפעיל את
- הפעלת היצירה והסדרה: מפענחים את אפשרויות השרת באמצעות
PublicKeyCredential.parseCreationOptionsFromJSON()ומעבירים את ההגדרה שנוצרה אלnavigator.credentials.create().- מפעילים את
credential.toJSON()כדי לקודד אתAuthenticatorAttestationResponseלאובייקט תקין שניתן לסדר ב-JSON, לפני שמאחזרים את נקודת הקצה לאימות.
- מפעילים את
- טיפול בחריגים של WebAuthn:
-
InvalidStateError: כבר קיים מפתח גישה תואם (התאמה לפיexcludeCredentials). NotAllowedError: המשתמש ביטל את תיבת הדו-שיח של אימות מפתח הגישה או שהזמן הקצוב לתפוגה שלה חלף.-
AbortError: הפעולה בוטלה. SecurityError: שגיאות שקשורות למקורות מאובטחים (HTTPS) או לחוסר התאמה במזהה ה-RP (בעיות בהגדרות).
-
- הפרדת 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';