סקירה כללית
הנה סקירה כללית של השלבים העיקריים ברישום מפתח גישה:

- מגדירים אפשרויות ליצירת מפתח גישה. שולחים אותם ללקוח, כדי שתוכלו להעביר אותם לקריאה ליצירת מפתח הגישה: הקריאה ל-WebAuthn API
navigator.credentials.createבאינטרנט, ו-credentialManager.createCredentialב-Android. אחרי שהמשתמש מאשר את יצירת מפתח הגישה, הקריאה ליצירת מפתח הגישה נפתרת ומוחזרים פרטי כניסהPublicKeyCredential. - מאמתים את פרטי הכניסה ושומרים אותם בשרת.
בקטעים הבאים מפורטים השלבים השונים.
יצירת אפשרויות ליצירת פרטי כניסה
השלב הראשון שצריך לבצע בשרת הוא ליצור אובייקט PublicKeyCredentialCreationOptions.
כדי לעשות את זה, צריך להסתמך על הספרייה בצד השרת של FIDO. בדרך כלל היא תציע פונקציה בסיסית שיכולה ליצור את האפשרויות האלה בשבילכם. לדוגמה, SimpleWebAuthn מציעה את generateRegistrationOptions.
האובייקט PublicKeyCredentialCreationOptions צריך לכלול את כל מה שנדרש ליצירת מפתח גישה: מידע על המשתמש, על הגורם המסתמך והגדרה של מאפייני פרטי הכניסה שאתם יוצרים. אחרי שמגדירים את כל הפרמטרים האלה, מעבירים אותם לפי הצורך לפונקציה בספרייה בצד השרת של FIDO שאחראית ליצירת האובייקט PublicKeyCredentialCreationOptions.
חלק מהשדות של PublicKeyCredentialCreationOptions יכולים להיות קבועים. אחרים צריכים להיות מוגדרים באופן דינמי בשרת:
-
rpId: כדי לאכלס את מזהה ה-RP בשרת, משתמשים בפונקציות או במשתנים בצד השרת שמספקים את שם המארח של אפליקציית האינטרנט, כמוexample.com. -
user.nameו-user.displayName: כדי לאכלס את השדות האלה, משתמשים בפרטי הסשן של המשתמש המחובר (או בפרטי חשבון המשתמש החדש, אם המשתמש יוצר מפתח גישה בזמן ההרשמה). user.nameהיא בדרך כלל כתובת אימייל, והיא ייחודית ל-RP. user.displayNameהוא שם ידידותי למשתמש. חשוב לזכור שלא כל הפלטפורמות ישתמשו ב-displayName. -
user.id: מחרוזת ייחודית ואקראית שנוצרת כשיוצרים את החשבון. הוא צריך להיות קבוע, בניגוד לשם משתמש שאפשר לערוך. מזהה המשתמש מזהה חשבון, אבל לא אמור להכיל פרטים אישיים מזהים (PII). סביר להניח שכבר יש לכם מזהה משתמש במערכת, אבל אם צריך, צרו מזהה במיוחד למפתחות גישה כדי שלא יהיו בו פרטים אישיים מזהים (PII). -
excludeCredentials: רשימה של מזהים של פרטי כניסה קיימים כדי למנוע שכפול של מפתח גישה מספק מפתחות הגישה. כדי לאכלס את השדה הזה, צריך לחפש במסד הנתונים את פרטי הכניסה הקיימים של המשתמש הזה. פרטים נוספים זמינים במאמר מניעת יצירה של מפתח גישה חדש אם כבר קיים מפתח גישה. -
challenge: ברישום פרטי כניסה, האתגר לא רלוונטי אלא אם משתמשים באימות, טכניקה מתקדמת יותר לאימות הזהות של ספק מפתחות הגישה והנתונים שהוא פולט. עם זאת, גם אם לא משתמשים באימות, האתגר עדיין נדרש. הוראות ליצירת אתגר מאובטח לאימות זמינות במאמר אימות מפתח גישה בצד השרת.
קידוד ופענוח
PublicKeyCredentialCreationOptions נשלח על ידי השרת. שדות בינאריים כמו challenge ו-user.id מקודדים בצד השרת ל-base64URL כדי שאפשר יהיה להעביר את האפשרויות כ-JSON דרך HTTPS.האובייקט הסטנדרטי PublicKeyCredentialCreationOptions של WebAuthn כולל שדות מסוג ArrayBuffer, שלא ניתן להעביר אותם ישירות באמצעות JSON.stringify(). כדי לשלוח את האפשרויות האלה דרך HTTPS, צריך לקודד שדות בינאריים כמחרוזות base64URL:
- בשרת, ספריית צד השרת של FIDO (כמו SimpleWebAuthn) יוצרת באופן אוטומטי אובייקט אפשרויות שניתן לסדר ב-JSON (למשל
PublicKeyCredentialCreationOptionsJSON) עם שדות בקידוד Base64URL. - בצד הלקוח, דפדפנים מודרניים תומכים ב-
PublicKeyCredential.parseCreationOptionsFromJSON()כדי להמיר את אפשרויות ה-JSON ישירות ל-PublicKeyCredentialCreationOptions. כשמשתמשים בספריות בצד הלקוח כמו@simplewebauthn/browser, פונקציות כמוstartRegistration({ optionsJSON: options })מטפלות בהמרה הזו באופן אוטומטי.
קוד לדוגמה: יצירת אפשרויות ליצירת פרטי כניסה
בדוגמאות שלנו אנחנו משתמשים בספריית SimpleWebAuthn. כאן אנחנו מעבירים את היצירה של אפשרויות פרטי הכניסה של המפתח הציבורי לפונקציה generateRegistrationOptions.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers';
router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
const { user } = res.locals;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// `excludeCredentials` prevents users from re-registering existing
// credentials for a given passkey provider
const excludeCredentials = [];
const credentials = Credentials.findByUserId(user.id);
if (credentials.length > 0) {
for (const cred of credentials) {
excludeCredentials.push({
id: cred.id,
type: 'public-key',
transports: cred.transports,
});
}
}
// Generate registration options for WebAuthn create
const options = await generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.HOSTNAME,
userID: isoUint8Array.fromUTF8String(user.id),
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
residentKey: 'required',
userVerification: 'preferred',
},
});
// Keep the challenge in the session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).send({ error: e.message });
}
});
שמירת המפתח הציבורי
navigator.credentials.create מחזירה אובייקט PublicKeyCredential.כשהפונקציה navigator.credentials.create פותרת את הבעיה בצד הלקוח, זה אומר שמפתח הגישה נוצר בהצלחה. מוחזר אובייקט PublicKeyCredential.
האובייקט PublicKeyCredential מכיל אובייקט AuthenticatorAttestationResponse, שמייצג את התגובה של ספק מפתח הגישה להוראה של הלקוח ליצור מפתח גישה. הוא מכיל מידע על פרטי הכניסה החדשים שאתם צריכים כ-RP כדי לאמת את המשתמש בהמשך. מידע נוסף על AuthenticatorAttestationResponse זמין בנספח: AuthenticatorAttestationResponse.
שולחים את האובייקט PublicKeyCredential לשרת. אחרי שמקבלים את הקוד, צריך לאמת אותו.
מעבירים את שלב האימות הזה לספרייה בצד השרת של FIDO. בדרך כלל, תהיה פונקציה בסיסית למטרה הזו. לדוגמה, SimpleWebAuthn מציעה את verifyRegistrationResponse. בנספח: אימות תגובת הרישום מוסבר מה קורה מאחורי הקלעים.
אחרי שהאימות מסתיים בהצלחה, שומרים את פרטי הכניסה במסד הנתונים כדי שהמשתמש יוכל לבצע אימות מאוחר יותר באמצעות מפתח הגישה שמשויך לפרטי הכניסה האלה.
משתמשים בטבלה ייעודית לפרטי כניסה של מפתחות ציבוריים שמשויכים למפתחות גישה. למשתמש יכולה להיות רק סיסמה אחת, אבל יכולים להיות לו כמה מפתחות גישה – לדוגמה, מפתח גישה שמסונכרן דרך צרור המפתחות של Apple iCloud ומפתח גישה שמסונכרן דרך מנהל הסיסמאות של Google.
הנה דוגמה לסכימה שבה אפשר להשתמש כדי לאחסן פרטי כניסה:

- הטבלה Users:
-
user_id: מזהה המשתמש הראשי. מזהה אקראי, ייחודי וקבוע של המשתמש. אפשר להשתמש בזה כמפתח ראשי בטבלה Users. -
username. שם משתמש שהוגדר על ידי המשתמש, ואולי אפשר לערוך אותו. -
passkey_user_id: מזהה המשתמש הספציפי למפתח הגישה, ללא פרטים אישיים מזהים, שמיוצג על ידיuser.idבאפשרויות הרישום. כשהמשתמש ינסה לבצע אימות בהמשך, המאמת יהפוך אתpasskey_user_idלזמין בתגובת האימות שלו ב-userHandle. מומלץ לא להגדיר אתpasskey_user_idכמפתח ראשי. מפתחות ראשיים נוטים להפוך לפרטים אישיים מזהים (PII) בפועל במערכות, כי משתמשים בהם הרבה.
-
- הטבלה "פרטי כניסה של מפתח ציבורי":
-
id: מזהה פרטי הכניסה. אפשר להשתמש בו כמפתח ראשי בטבלה Public key credentials. -
public_key: המפתח הציבורי של פרטי הכניסה. -
passkey_user_id: משמש כמפתח זר ליצירת קישור לטבלה Users. -
backed_up: מפתח גישה מגובה אם הוא מסונכרן על ידי ספק מפתח הגישה. אחסון מצב הגיבוי שימושי אם אתם שוקלים להפסיק להשתמש בסיסמאות בעתיד למשתמשים שיש להם מפתחות גישה שלbacked_up. כדי לבדוק אם מפתח הגישה מגובה, אפשר לבחון את הדגל BE ב-authenticatorData, או להשתמש בתכונה של ספרייה בצד השרת של FIDO שבדרך כלל זמינה כדי לתת לכם גישה קלה למידע הזה. שמירת הזכאות לגיבוי יכולה לעזור לכם לטפל בפניות אפשריות של משתמשים. -
name: אופציונלי, שם תצוגה לפרטי הכניסה כדי לאפשר למשתמשים לתת לפרטי הכניסה שמות בהתאמה אישית. -
transports: מערך של אמצעי תעבורה. אחסון של אמצעי התחבורה שימושי לחוויית המשתמש באימות. כשהטרנספורטים זמינים, הדפדפן יכול לפעול בהתאם ולהציג ממשק משתמש שתואם לטרנספורט שבו ספק מפתח הגישה משתמש כדי לתקשר עם לקוחות – במיוחד במקרים של אימות מחדש שבהםallowCredentialsלא ריק.
-
כדי לשפר את חוויית המשתמש, כדאי לשמור מידע נוסף, כמו ספק מפתח הגישה, מועד יצירת פרטי הכניסה והמועד האחרון שבו השתמשו בהם. מידע נוסף זמין במאמר עיצוב ממשק המשתמש של מפתחות הגישה.
קוד לדוגמה: אחסון פרטי הכניסה
בדוגמאות שלנו אנחנו משתמשים בספריית SimpleWebAuthn.
כאן אנחנו מעבירים את אימות התגובה לרישום לפונקציה verifyRegistrationResponse.
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
const response = req.body;
// This sample code is for registering a passkey for an existing,
// signed-in user
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Verify the credential
const { verified, registrationInfo } = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
requireUserVerification: false,
});
if (!verified) {
throw new Error('Verification failed.');
}
const {
aaguid,
credential,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '00000000-0000-0000-0000-000000000000' ?
req.useragent?.platform : aaguids[aaguid]?.name;
const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: credential.id,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
transports: credential.transports || response.response.transports,
backed_up: credentialBackedUp,
registered_at: new Date().getTime()
});
// Kill the challenge for this session
delete req.session.challenge;
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).send({ error: e.message });
}
});
נספח: AuthenticatorAttestationResponse
AuthenticatorAttestationResponse מכיל שני אובייקטים חשובים:
-
response.clientDataJSONהיא גרסת JSON של נתוני הלקוח, שבדפדפן הם הנתונים כפי שהם מוצגים בדפדפן. הוא מכיל את מקור ה-RP, את האתגר ואתandroidPackageNameאם הלקוח הוא אפליקציה ל-Android. כ-RP, קריאתclientDataJSONנותנת לך גישה למידע שהדפדפן ראה בזמן בקשת ה-create. response.attestationObjectמכיל שני פריטי מידע:attestationStatement, שלא רלוונטי אלא אם משתמשים באימות.-
authenticatorDataהם נתונים כפי שהם נראים לספק מפתח הגישה. כ-RP, קריאתauthenticatorDataנותנת לך גישה לנתונים שספק מפתחות הגישה רואה ומוחזרים בזמן הבקשה שלcreate.
authenticatorData מכיל מידע חיוני על פרטי הכניסה של המפתח הציבורי שמשויך למפתח הגישה החדש שנוצר:
- פרטי הכניסה של המפתח הציבורי עצמו, ומזהה ייחודי של פרטי הכניסה.
- מזהה הגורם המוגבל (RP) שמשויך לפרטי הכניסה.
- דגלים שמתארים את סטטוס המשתמש כשהמפתח הוקש נוצר: אם המשתמש באמת היה נוכח, ואם המשתמש אומת בהצלחה (ראו הסבר מעמיק על userVerification).
- AAGUID הוא מזהה של ספק מפתח הגישה, כמו מנהל הסיסמאות של Google. על סמך ה-AAGUID, אתם יכולים לזהות את ספק מפתח הגישה ולהציג את השם בדף ניהול מפתחות הגישה. (ראו זיהוי ספק מפתח הגישה באמצעות AAGUID)
למרות ש-authenticatorData מוטמע בתוך attestationObject, המידע שהוא מכיל נדרש להטמעה של מפתח הגישה, גם אם משתמשים באימות וגם אם לא. authenticatorData מקודד ומכיל שדות שמקודדים בפורמט בינארי. בדרך כלל, הספרייה בצד השרת תטפל בניתוח ובפענוח. אם אתם לא משתמשים בספרייה בצד השרת, כדאי להשתמש ב-getAuthenticatorData() בצד הלקוח כדי לחסוך לעצמכם עבודה של ניתוח ופענוח בצד השרת.
נספח: אימות התגובה לרישום
מאחורי הקלעים, אימות התגובה לרישום כולל את הבדיקות הבאות:
- מוודאים שמזהה ה-RP תואם לאתר.
- מוודאים שמקור הבקשה הוא מקור שצפוי באתר (כתובת ה-URL של האתר הראשי, אפליקציה ל-Android).
- אם אתם דורשים אימות משתמש, ודאו שהדגל של אימות המשתמש
authenticatorData.uvהואtrue. - דגל נוכחות המשתמש
authenticatorData.upאמור להיות בדרך כללtrue, אבל אם פרטי הכניסה נוצרו באופן מותנה, הוא אמור להיותfalse. - בודקים שהלקוח הצליח לספק את האתגר שנתתם לו. אם אתם לא משתמשים באימות, הבדיקה הזו לא חשובה. עם זאת, מומלץ להטמיע את הבדיקה הזו כדי לוודא שהקוד מוכן למקרה שתחליטו להשתמש באימות בעתיד.
- מוודאים שמזהה פרטי הכניסה עדיין לא רשום לאף משתמש.
- מוודאים שהאלגוריתם שספק מפתח הגישה משתמש בו כדי ליצור את פרטי הכניסה הוא אלגוריתם שציינתם (בכל שדה
algשלpublicKeyCredentialCreationOptions.pubKeyCredParams, שבדרך כלל מוגדר בספרייה בצד השרת ולא גלוי לכם). כך אפשר לוודא שהמשתמשים יכולים להירשם רק באמצעות אלגוריתמים שבחרתם לאשר.
כדי לקבל מידע נוסף, אפשר לעיין בקוד המקור של SimpleWebAuthn עבור verifyRegistrationResponse או לעיין ברשימה המלאה של האימותים במפרט.