סקירה כללית
סקירה כללית ומקיפה של השלבים העיקריים לאימות באמצעות מפתחות גישה:
- מגדירים את האתגר ואפשרויות נוספות שנדרשות כדי לבצע אימות באמצעות מפתח גישה. צריך לשלוח אותם ללקוח כדי שתהיה לך אפשרות להעביר אותם לשיחת אימות באמצעות מפתחות גישה (
navigator.credentials.get
באינטרנט). אחרי שהמשתמש יאשר את האימות באמצעות מפתח גישה, הקריאה לאימות באמצעות מפתח גישה תסתיים ותחזיר פרטי כניסה (PublicKeyCredential
). פרטי הכניסה מכילים טענת נכוֹנוּת (assertion) לגבי אימות.
- מאמתים את טענת הנכונות (assertion) של האימות.
- אם טענת הנכוֹנוּת (assertion) של האימות חוקית, מאמתים את המשתמש.
בקטעים הבאים מתוארים הפרטים הספציפיים של כל שלב.
יוצרים את האתגר
בפועל, אתגר הוא מערך של בייטים אקראיים, שמיוצגים כאובייקט ArrayBuffer
.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
כדי להבטיח שהאתגר יענה על המטרה שלו, צריך:
- מוודאים שלא משתמשים באותו אתגר יותר מפעם אחת. יצירת אתגר חדש בכל ניסיון כניסה. מחיקת האתגר לאחר כל ניסיון כניסה, בין אם הניסיון הצליח ובין אם נכשל. גם מוחקים את האתגר לאחר פרק זמן מסוים. אף פעם אל תאשרו את אותו אתגר בתשובה יותר מפעם אחת.
- מוודאים שהאתגר מאובטח מבחינה קריפטוגרפית. אתגר צריך להיות בלתי אפשרי לניחוש. כדי ליצור אתגר מאובטח מבחינה קריפטוגרפית בצד השרת, מומלץ להסתמך על ספרייה בצד השרת מסוג FIDO שיש בה אמון. אם במקום זאת תיצרו אתגרים משלכם, תוכלו להשתמש בפונקציונליות הקריפטוגרפית המובנית בסטאק התוכנות שלכם, או לחפש ספריות שמיועדות לתרחישים קריפטוגרפיים לדוגמה. לדוגמה: iso-crypto ב-Node.js או סודות ב-Python. לפי המפרט, האתגר חייב להיות באורך של 16 בייטים לפחות כדי להיחשב מאובטח.
אחרי שיוצרים אתגר, שומרים אותו בסשן של המשתמש כדי לאמת אותו מאוחר יותר.
יצירת אפשרויות לבקשת פרטי כניסה
יצירת אפשרויות לבקשת פרטי כניסה כאובייקט publicKeyCredentialRequestOptions
.
כדי לעשות זאת, צריך להיעזר בספריית FIDO בצד השרת. בדרך כלל הוא מציע פונקציית שירות שיכולה ליצור את האפשרויות האלה עבורכם. מוצרי SimpleWebAuthn, לדוגמה, generateAuthenticationOptions
.
האפליקציה publicKeyCredentialRequestOptions
צריכה להכיל את כל המידע שנדרש לאימות של מפתח גישה. מעבירים את המידע הזה לפונקציה בספרייה בצד השרת מסוג FIDO שאחראית ליצירת האובייקט publicKeyCredentialRequestOptions
.
חלק מ-publicKeyCredentialRequestOptions
יכולים להיות קבועים. אחרות צריכות להיות מוגדרות באופן דינמי בשרת:
rpId
: מזהה הגורם המוגבל (RP) שאליו אתם מצפים שפרטי הכניסה ישויכו, לדוגמהexample.com
. האימות יצליח רק אם מזהה הגורם המוגבל (RP) שמציינים כאן תואם למזהה הגורם המוגבל (RP) שמשויך לפרטי הכניסה. כדי לאכלס את מזהה הגורם המוגבל (RP), צריך להשתמש באותו ערך כמו מזהה הגורם המוגבל (RP) שהוגדר ב-publicKeyCredentialCreationOptions
במהלך רישום פרטי הכניסה.challenge
: קטע נתונים שספק מפתח הגישה יחתום עליו כדי להוכיח שהמשתמש מחזיק במפתח הגישה בזמן בקשת האימות. בודקים את הפרטים בקטע יצירת האתגר.allowCredentials
: מערך של פרטי כניסה קבילים לאימות הזה. צריך להעביר מערך ריק כדי לאפשר למשתמש לבחור מפתח גישה זמין מהרשימה המוצגת בדפדפן. פרטים נוספים זמינים במאמרים אחזור אתגר משרת RP וסקירה מפורטת של פרטי כניסה שניתנים לגילוי.userVerification
: מציין אם אימות המשתמש באמצעות נעילת המסך של המכשיר הוא 'חובה' או 'מועדף' או 'תחושת אי נעימות'. קוראים את המאמר אחזור אתגר משרת RP.timeout
: משך הזמן (באלפיות שנייה) שיכול לעבור למשתמש כדי להשלים את האימות. הבקשה צריכה להיות נדיבה במידה סבירה וקצרה יותר ממשך החיים שלchallenge
. ערך ברירת המחדל המומלץ הוא 5 דקות, אבל אפשר להגדיל אותו – עד 10 דקות, והוא עדיין במסגרת הטווח המומלץ. זמן קצוב לתפוגה הוא הגיוני אם מצפים מהמשתמשים להשתמש בתהליך העבודה ההיברידי, שלרוב נמשך זמן רב יותר. אם הזמן הקצוב לפעולה פג, יועברNotAllowedError
.
אחרי שיוצרים את publicKeyCredentialRequestOptions
, צריך לשלוח אותו ללקוח.
![PublicKeyCredentialCreationOptions נשלח על ידי השרת](https://developers.google.cn/identity/passkeys/developer-guides/server-authentication/images/auth-options.png?authuser=7&hl=he)
challenge
מתבצע בצד הלקוח.קוד לדוגמה: יצירת אפשרויות לבקשת פרטי כניסה
בדוגמאות שלנו אנחנו משתמשים בספרייתSimpleWebAuthn. כאן נעביר את האפשרות ליצור אפשרויות לבקשת פרטי כניסה לפונקציה generateAuthenticationOptions
שלה.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
router.post('/signinRequest', csrfCheck, async (req, res) => {
// Ensure you nest 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 {
// Use the generateAuthenticationOptions function from SimpleWebAuthn
const options = await generateAuthenticationOptions({
rpID: process.env.HOSTNAME,
allowCredentials: [],
});
// Save the challenge in the user session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).json({ error: e.message });
}
});
אימות וכניסה של המשתמש
כשהמערכת מזהה את קוד ה-navigator.credentials.get
בהצלחה אצל הלקוח, היא מחזירה אובייקט PublicKeyCredential
.
![אובייקט PublicKeyCredential נשלח על ידי השרת](https://developers.google.cn/identity/passkeys/developer-guides/server-authentication/images/publicKeyCredential.png?authuser=7&hl=he)
navigator.credentials.get
מחזירה PublicKeyCredential
.response
הוא AuthenticatorAssertionResponse
. היא מייצגת את התשובה של הספק של מפתח הגישה להוראה של הלקוח ליצור את מה שנדרש כדי לנסות לבצע אימות באמצעות מפתח גישה ב-RP. היא מכילה:
response.authenticatorData
וגםresponse.clientDataJSON
, למשל בשלב רישום מפתח הגישה.response.signature
שמכיל חתימה מעל הערכים האלה.
שולחים את האובייקט PublicKeyCredential
לשרת.
בשרת, מבצעים את הפעולות הבאות:
![סכימה של מסד נתונים](https://developers.google.cn/identity/passkeys/developer-guides/server-authentication/images/database-schema-passkeys.png?authuser=7&hl=he)
- אוספים את המידע שנדרש כדי לאמת את טענת הנכוֹנוּת (assertion) ולאמת את המשתמש:
- כשיצרתם את אפשרויות האימות, תקבלו את האתגר הצפוי ששמרתם בסשן.
- מקבלים את המקור ואת מזהה הגורם המוגבל (RP).
- מאתרים במסד הנתונים מי המשתמש. במקרה של פרטי כניסה גלויים, אתם לא יודעים מי המשתמש ששולח בקשת אימות. יש שתי דרכים לבדוק זאת:
- אפשרות 1: משתמשים ב-
response.userHandle
באובייקטPublicKeyCredential
. בטבלה משתמשים, מחפשים את הערךpasskey_user_id
שתואם ל-userHandle
. - אפשרות 2: משתמשים בפרטי הכניסה
id
שקיימים באובייקטPublicKeyCredential
. בטבלה פרטי כניסה של מפתח ציבורי, צריך לחפש את פרטי הכניסהid
שתואמים לפרטי הכניסהid
שמופיעים באובייקטPublicKeyCredential
. לאחר מכן, מחפשים בטבלת המשתמשים את המשתמש המתאים באמצעות המפתח הזרpasskey_user_id
.
- אפשרות 1: משתמשים ב-
- מוצאים במסד הנתונים את פרטי פרטי הכניסה של המפתח הציבורי שתואמים לטענת האימות שקיבלתם. כדי לעשות זאת, בטבלה Public key credentials, מחפשים את פרטי הכניסה
id
שתואמים לפרטי הכניסהid
שקיימים באובייקטPublicKeyCredential
.
מאמתים את טענת הנכוֹנוּת (assertion) של האימות. מעבירים את שלב האימות הזה לספרייה בצד השרת של ה-FIDO, שבדרך כלל תציע פונקציית שירות למטרה הזו. מוצרי SimpleWebAuthn, לדוגמה,
verifyAuthenticationResponse
. מידע נוסף על הבעיה זמין בנספח: אימות תגובת האימות.מוחקים את האתגר גם אם האימות הצליח וגם אם לא, כדי למנוע התקפות שליחה מחדש.
נכנסים לחשבון המשתמש. אם האימות הצליח, מעדכנים את פרטי הסשן כדי לסמן את המשתמש שמחובר לחשבון. כדאי גם להחזיר אובייקט
user
ללקוח, כדי שהחזית תוכל להשתמש במידע שמשויך למשתמש שנכנס לאחרונה.
קוד לדוגמה: אימות וכניסה לחשבון המשתמש
בדוגמאות שלנו אנחנו משתמשים בספרייתSimpleWebAuthn. כאן נעביר את האימות של תגובת האימות לפונקציית verifyAuthenticationResponse
שלה.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/signinResponse', csrfCheck, async (req, res) => {
const response = req.body;
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
// 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 {
// Find the credential stored to the database by the credential ID
const cred = Credentials.findById(response.id);
if (!cred) {
throw new Error('Credential not found.');
}
// Find the user - Here alternatively we could look up the user directly
// in the Users table via userHandle
const user = Users.findByPasskeyUserId(cred.passkey_user_id);
if (!user) {
throw new Error('User not found.');
}
// Base64URL decode some values
const authenticator = {
credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
credentialID: isoBase64URL.toBuffer(cred.id),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
authenticator,
requireUserVerification: false,
});
if (!verified) {
throw new Error('User verification failed.');
}
// Kill the challenge for this session.
delete req.session.challenge;
req.session.username = user.username;
req.session['signed-in'] = 'yes';
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).json({ error: e.message });
}
});
נספח: אימות של תגובת האימות
כדי לאמת את תגובת האימות:
- מוודאים שמזהה הגורם המוגבל (RP) תואם לאתר שלכם.
- מוודאים שמקור הבקשה תואם למקור הכניסה של האתר. לאפליקציות ל-Android, בודקים את אימות המקור.
- בודקים אם המכשיר הצליח לספק את האתגר שנתתם לו.
- עליכם לוודא שבמהלך האימות, המשתמש עומד בדרישות שהגדרתם כגורם מוגבל (RP). אם צריך לאמת את המשתמש, צריך לוודא שהסימון
uv
(אומת על ידי המשתמש) ב-authenticatorData
הואtrue
. בודקים שהסימוןup
(נוכחות המשתמש) ב-authenticatorData
הואtrue
, כי נוכחות המשתמש נדרשת תמיד למפתחות גישה. - מאמתים את החתימה. כדי לאמת את החתימה, צריך:
- החתימה, שהיא האתגר החתום:
response.signature
- המפתח הציבורי, שאיתו רוצים לאמת את החתימה.
- הנתונים החתומים המקוריים אלו הנתונים שצריך לאמת את החתימה שלהם.
- אלגוריתם ההצפנה שבו נעשה שימוש ליצירת החתימה.
- החתימה, שהיא האתגר החתום:
לקבלת מידע נוסף על השלבים האלה, אפשר לעיין בקוד המקור של verifyAuthenticationResponse
ב-SimpleWebAuthn או לעיין ברשימת האימותים המלאה במפרט.