סקירה כללית
ריכזנו כאן סקירה כללית של השלבים העיקריים בתהליך האימות באמצעות מפתחות גישה:
- צריך להגדיר את האתגר ואפשרויות נוספות שנדרשות לאימות באמצעות מפתח גישה. צריך לשלוח אותם ללקוח כדי שתהיה אפשרות להעביר אותם לשיחת האימות של מפתח הגישה (
navigator.credentials.get
באינטרנט). אחרי שהמשתמש יאשר את האימות של מפתח הגישה, הקריאה לאימות של מפתח הגישה תטופל ותחזיר פרטי כניסה (PublicKeyCredential
). פרטי הכניסה מכילים טענת אימות.
- מאמתים את טענת האימות.
- אם טענת הנכוֹנוּת (assertion) של האימות תקפה, מאמתים את המשתמש.
הקטעים הבאים מתארים את הפרטים הספציפיים של כל שלב.
יצירת האתגר
בפועל, אתגר הוא מערך של בייטים אקראיים, שמיוצגים כאובייקט ArrayBuffer
.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
כדי להבטיח שהאתגר מגשים את מטרתו, צריך:
- ודאו שלא משתמשים באותו אתגר יותר מפעם אחת. יצירת אתגר חדש בכל ניסיון כניסה. למחוק את האתגר אחרי כל ניסיון כניסה, גם אם הוא הצליח וגם אם נכשל. מחקו את האתגר גם לאחר פרק זמן מסוים. אין לקבל את אותו האתגר בתשובה יותר מפעם אחת.
- מוודאים שהאתגר מאובטח מבחינה קריפטוגרפית. האתגר צריך להיות משימה בלתי אפשרית. כדי ליצור אתגר מאובטח מבחינה קריפטוגרפית בצד השרת, מומלץ להשתמש בספרייה בצד השרת של FIDO שסומכים עליה. אם במקום זאת תיצרו אתגרים משלכם, תוכלו להשתמש בפונקציונליות הקריפטוגרפית המובנית שזמינה בסטאק התוכנות שלכם, או לחפש ספריות שמיועדות לתרחישים קריפטוגרפיים לדוגמה. לדוגמה: iso-crypto ב-Node.js או secrets ב-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-dot-devsite-v2-prod.appspot.com/identity/passkeys/developer-guides/server-authentication/images/auth-options.png?authuser=1&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-dot-devsite-v2-prod.appspot.com/identity/passkeys/developer-guides/server-authentication/images/publicKeyCredential.png?authuser=1&hl=he)
navigator.credentials.get
מחזירה PublicKeyCredential
.הערך response
הוא AuthenticatorAssertionResponse
. היא מייצגת את התגובה של ספק מפתחות הגישה להנחיות של הלקוח כדי ליצור את מה שצריך כדי לנסות לבצע אימות באמצעות מפתח גישה ב-RP. התוצאה מכילה:
response.authenticatorData
ו-response.clientDataJSON
, למשל בשלב הרישום של מפתח הגישה.response.signature
שמכיל חתימה על הערכים האלה.
שולחים את האובייקט PublicKeyCredential
לשרת.
בשרת, מבצעים את הפעולות הבאות:
![סכימה של מסד נתונים](https://developers-dot-devsite-v2-prod.appspot.com/identity/passkeys/developer-guides/server-authentication/images/database-schema-passkeys.png?authuser=1&hl=he)
- לאסוף את המידע הדרוש לאימות טענת הנכוֹנוּת (assertion) ולאימות המשתמש:
- מקבלים את האתגר הצפוי ששמרתם בסשן כשיצרתם את אפשרויות האימות.
- מקבלים את המקור ואת מזהה הגורם המוגבל (RP) הצפויים.
- מוצאים במסד הנתונים מי המשתמש. במקרה של פרטי כניסה גלויים, לא יודעים מי המשתמש ששולח בקשת אימות. כדי לבדוק זאת, יש שתי אפשרויות:
- אפשרות 1: משתמשים ב-
response.userHandle
באובייקטPublicKeyCredential
. בטבלה משתמשים, מחפשים אתpasskey_user_id
שתואם ל-userHandle
. - אפשרות 2: משתמשים בפרטי הכניסה
id
שנמצאים באובייקטPublicKeyCredential
. בטבלה פרטי כניסה למפתח ציבורי, מחפשים את פרטי הכניסהid
שתואמים לפרטי הכניסהid
שנמצאים באובייקטPublicKeyCredential
. לאחר מכן, מחפשים את המשתמש המתאים באמצעות המפתח הזרpasskey_user_id
בטבלה משתמשים.
- אפשרות 1: משתמשים ב-
- מוצאים במסד הנתונים את פרטי פרטי הכניסה של המפתח הציבורי שתואמים לטענת האימות שקיבלתם. כדי לעשות זאת, בטבלה פרטי כניסה למפתח ציבורי מחפשים את פרטי הכניסה
id
שתואמים לפרטי הכניסהid
שקיימים באובייקטPublicKeyCredential
.
מאמתים את טענת האימות. צריך להעביר את שלב האימות הזה לספרייה בצד השרת של 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, אפשר לעיין במאמר אימות המקור.
- יש לבדוק שהמכשיר הצליח לבצע את האתגר שנתת לו.
- יש לוודא שבמהלך האימות, המשתמש עמד בדרישות שהגדרת כגורם מוגבל. אם יש צורך באימות משתמש, חשוב לוודא שהסימון
uv
(מאומת על ידי משתמשים) ב-authenticatorData
הואtrue
. יש לבדוק שהסימון שלup
(יש משתמש קיים) ב-authenticatorData
הואtrue
, כי תמיד נדרשת נוכחות של משתמשים במפתחות גישה. - מאמתים את החתימה. כדי לאמת את החתימה, צריך:
- החתימה, שהיא האתגר החתום:
response.signature
- המפתח הציבורי, שצריך לאמת את החתימה איתו.
- הנתונים החתומים המקוריים. אלו הנתונים שצריך לאמת את החתימה שלהם.
- אלגוריתם הקריפטוגרפיה ששימש ליצירת החתימה.
- החתימה, שהיא האתגר החתום:
כדי לקבל מידע נוסף על השלבים האלה, אפשר לעיין בקוד המקור של verifyAuthenticationResponse
של SimpleWebAuthn או לעיין ברשימת האימות המלאה במפרט.