במדריך הזה מוסבר איך להטמיע אימות של משתמשים חוזרים באמצעות פרטי כניסה שניתן לגלות, גם באמצעות טריגרים מפורשים של לחצנים וגם באמצעות הצעות למילוי אוטומטי בדפדפן (ממשק משתמש מותנה).
בצד השרת
יצירת אפשרויות
יוצרים נקודת קצה שמפיקה פרמטרים של בקשות WebAuthn באמצעות ספרייה שנבדקה לפי התקנים.
- שימוש במזהה RP מוגדר מראש: משתמשים במזהה RP תקין ומוגדר מראש כמחרוזת קבועה.
- יצירת אתגר: יוצרים מאגר אקראי מאובטח מבחינה קריפטוגרפית עם אנטרופיה גבוהה, מאחסנים אותו באופן מאובטח בסשן של המשתמש ומקודדים אותו כ-Base64URL.
- מיפוי פרטי כניסה שניתן לגלות: מציינים מערך ריק
[]עבורallowCredentials. הבקשה הזו מיועדת לפרטי כניסה שניתן לגלות, כלומר המשתמש לא צריך להזין קודם את שם המשתמש שלו. ספק מפתח הגישה יציג את החשבונות הזמינים. - רמת אימות המשתמש: מגדירים את
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";
נקודת קצה לאימות
מאמתים באופן מאובטח את ההצהרה שהוחזרה על ידי הלקוח כדי לאמת את המשתמש:
- אימות אתגר הסשן: אכיפת התאמה קפדנית של האתגר בין תגובת הלקוח לבין האתגר הצפוי שמאוחסן בסשן.
- אכיפת העדפות של UV:
- אפשר להשתמש במאמתים ללא אימות משתמש (למשל, נעילות מסך של מאמתים מושבתות) אם
expectedUserVerificationשל הסשן ביקש זאת, על ידי העברתrequireUserVerification: falseלספריית האימות בצד השרת."preferred"אם מוצגת בקשה"required", צריך להזין את המידע הביומטרי או את קוד האימות.
- אפשר להשתמש במאמתים ללא אימות משתמש (למשל, נעילות מסך של מאמתים מושבתות) אם
- שגיאה 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) שמתבצעות.
תהליך מותנה של בחירת רשת (מילוי אוטומטי של טופס)
כדי להציע אימות באמצעות מפתח גישה באופן מקורי כשמשתמשים מתמקדים בשדות הכניסה, מפעילים הצעות למילוי אוטומטי של טפסים בטעינת הדף:
- זיהוי תכונות: קוראים ל-
PublicKeyCredential.getClientCapabilities()בטעינת הדף ומדלגים על הכניסה באמצעות מפתח גישה אםconditionalGetלא זמין. - אפשרויות פענוח: פענוח של אובייקט JSON של פרטי הכניסה שאוחזרו באמצעות
PublicKeyCredential.parseRequestOptionsFromJSON(). - הפעלת Get מותנה: קוראים ל-
navigator.credentials.get()עםmediation: "conditional"ומעבירים אותAbortController. הפעולה הזו רושמת את המילוי האוטומטי באופן שקט בלי להציג תיבת דו-שיח של מפתח גישה. - הפרדת חריגים של Try/Catch: עוטפים את הקריאה ל-
navigator.credentials.getבבלוק try/catch:-
NotAllowedError: המשתמש ביטל את בקשת ההתחברות באמצעות מפתח הגישה או שהזמן הקצוב לתפוגה של הבקשה חלף. -
AbortError: בקשת האימות בוטלה באופן פרוגרמטי.
-
- Call Signal API: עוטפים את הקריאה
fetch()לאימות השרת בבלוק try/catch:- הצגת הודעת שגיאה כדי שהמשתמש יבין מה השתבש.
- צריך להפעיל את
signalUnknownCredential()רק כשהשרת מגיב באופן מפורש עם סטטוס HTTP404(פרטי הכניסה לא נמצאו) והמשתמש לא מאומת. - הפרמטר
credentialIdשמועבר אלsignalUnknownCredential()חייב להיות מחרוזת מזהה פרטי הכניסה בקידוד Base64URL (למשל,encoded.id), ולא אובייקט ArrayBuffer הגולמיcredential.rawId.
- קידוד התגובה: מקודדים את פרטי הכניסה
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';