סקירה כללית
הנה סקירה כללית של השלבים העיקריים שנדרשים לרישום מפתח גישה:
- מגדירים אפשרויות ליצירת מפתח גישה. שולחים אותם ללקוח כדי להעביר אותם לקריאה ליצירת מפתח הגישה: קריאת WebAuthn API
navigator.credentials.create
באינטרנט ו-credentialManager.createCredential
ב-Android. אחרי שהמשתמש מאשר את יצירת מפתח הגישה, קריאת יצירת מפתח הגישה נפתרת ומחזירה אישורPublicKeyCredential
. - מאמתים את פרטי הכניסה ומאחסנים אותם בשרת.
בקטעים הבאים מפורט כל שלב.
יצירת אפשרויות ליצירת פרטי כניסה
השלב הראשון שצריך לבצע בשרת הוא ליצור אובייקט PublicKeyCredentialCreationOptions
.
כדי לעשות זאת, צריך להסתמך על ספריית FIDO בצד השרת. בדרך כלל מוצעת פונקציית כלי עזר שיכולה ליצור את האפשרויות האלה בשבילכם. לדוגמה, SimpleWebAuthn מציע generateRegistrationOptions
.
PublicKeyCredentialCreationOptions
צריך לכלול את כל מה שנדרש ליצירת מפתח גישה: מידע על המשתמש, על ה-RP והגדרה של מאפייני אמצעי האימות שיוצרים. אחרי שמגדירים את כל הפרמטרים האלה, מעבירים אותם לפי הצורך לפונקציה בספרייה בצד השרת של FIDO שאחראית ליצירת האובייקט PublicKeyCredentialCreationOptions
.
חלק מהשדות של PublicKeyCredentialCreationOptions
יכולים להיות קבועים. אחרים צריכים להיות מוגדרים באופן דינמי בשרת:
-
rpId
: כדי לאכלס את מזהה הצד הנסמך בשרת, משתמשים בפונקציות או במשתנים בצד השרת שמספקים את שם המארח של אפליקציית האינטרנט, כמוexample.com
. -
user.name
ו-user.displayName
: כדי לאכלס את השדות האלה, צריך להשתמש בפרטי הסשן של המשתמש המחובר (או בפרטי חשבון המשתמש החדש, אם המשתמש יוצר מפתח גישה במהלך ההרשמה).user.name
היא בדרך כלל כתובת אימייל, והיא ייחודית ל-RP.user.displayName
הוא שם ידידותי למשתמש. שימו לב שלא כל הפלטפורמות ישתמשו ב-displayName
. -
user.id
: מחרוזת ייחודית אקראית שנוצרת כשיוצרים את החשבון. הוא צריך להיות קבוע, בניגוד לשם משתמש שאפשר לערוך. מזהה המשתמש מציין חשבון, אבל אסור שהוא יכיל פרטים אישיים מזהים (PII). סביר להניח שכבר יש לכם מזהה משתמש במערכת, אבל אם צריך, אפשר ליצור מזהה משתמש במיוחד למפתחות גישה כדי שלא יכלול פרטים אישיים מזהים. -
excludeCredentials
: רשימה של מזהים של פרטי כניסה קיימים כדי למנוע שכפול של מפתח גישה מספק מפתחות הגישה. כדי לאכלס את השדה הזה, צריך לחפש במסד הנתונים את פרטי הכניסה הקיימים של המשתמש. פרטים נוספים זמינים במאמר בנושא מניעת יצירה של מפתח גישה חדש אם כבר קיים מפתח גישה. -
challenge
: לצורך רישום פרטי הכניסה, האתגר לא רלוונטי אלא אם משתמשים באימות, טכניקה מתקדמת יותר לאימות הזהות של ספק מפתחות הגישה והנתונים שהוא מפיק. עם זאת, גם אם לא משתמשים באימות, השדה 'אתגר' הוא עדיין שדה חובה. הוראות ליצירת אתגר מאובטח לאימות זמינות במאמר בנושא אימות מפתח גישה בצד השרת.
קידוד ופענוח

PublicKeyCredentialCreationOptions
נשלחה על ידי השרת. challenge
, user.id
ו-excludeCredentials.credentials
צריכים להיות מקודדים בצד השרת ל-base64URL
, כדי שניתן יהיה להעביר את PublicKeyCredentialCreationOptions
באמצעות HTTPS.PublicKeyCredentialCreationOptions
כוללים שדות שהם ArrayBuffer
, ולכן הם לא נתמכים על ידי JSON.stringify()
. המשמעות היא שבשלב הזה, כדי להעביר PublicKeyCredentialCreationOptions
באמצעות HTTPS, צריך לקודד באופן ידני חלק מהשדות בשרת באמצעות base64URL
ואז לפענח אותם בלקוח.
- בשרת, הקידוד והפענוח מתבצעים בדרך כלל על ידי ספריית FIDO בצד השרת.
- בצד הלקוח, נכון לעכשיו צריך לבצע את הקידוד והפענוח באופן ידני. בעתיד יהיה קל יותר: תהיה זמינה שיטה להמרת אפשרויות בפורמט JSON ל-
PublicKeyCredentialCreationOptions
. בודקים את הסטטוס של ההטמעה ב-Chrome.
קוד לדוגמה: יצירת אפשרויות ליצירת פרטי כניסה
בדוגמאות שלנו אנחנו משתמשים בספריית SimpleWebAuthn. בשלב הזה, אנחנו מעבירים את היצירה של אפשרויות האישורים של המפתח הציבורי לפונקציה generateRegistrationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } 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: isoBase64URL.toBuffer(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: user.id,
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
requireResidentKey: true
},
});
// 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 Keychain ומפתח גישה שסונכרן דרך מנהל הסיסמאות של Google.
דוגמה לסכימה שאפשר להשתמש בה כדי לאחסן פרטי כניסה:
- טבלת Users:
-
user_id
: מזהה המשתמש הראשי. מזהה אקראי, ייחודי וקבוע של המשתמש. משתמשים בו כמפתח ראשי לטבלה Users. -
username
. שם משתמש שהוגדר על ידי המשתמש, שאפשר לערוך אותו. -
passkey_user_id
: מזהה המשתמש הספציפי למפתח הגישה שלא כולל פרטים אישיים מזהים, שמיוצג על ידיuser.id
באפשרויות הרישום. כשמשתמש ינסה לאמת את עצמו בהמשך, אמצעי האימות יספק אתpasskey_user_id
בתגובת האימות שלו ב-userHandle
. מומלץ לא להגדיר אתpasskey_user_id
כמפתח ראשי. מפתחות ראשיים נוטים להפוך לפרטים אישיים בפועל במערכות, כי נעשה בהם שימוש נרחב.
-
- טבלה של פרטי מפתח ציבורי:
-
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,
credentialPublicKey,
credentialID,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '000000-0000-0000-0000-00000000' ?
req.useragent?.platform : aaguids[aaguid].name;
const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
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
contains two pieces of information: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
או ברשימה המלאה של האימותים במפרט.