אבטחת האתר באמצעות אימות דו-שלבי באמצעות מפתח אבטחה (WebAuthn)

1. מה תפתחו

עליך להתחיל עם אפליקציית אינטרנט בסיסית שתומכת בהתחברות מבוססת סיסמה.

לאחר מכן מוסיפים תמיכה לאימות דו-שלבי באמצעות מפתח אבטחה, על בסיס WebAuthn. כדי לעשות זאת, עליך ליישם את התנאים הבאים:

  • דרך שבה המשתמש יכול לרשום פרטי כניסה ב-WebAuthn.
  • תהליך אימות דו-שלבי שבו המשתמש מבקש את הגורם השני שלו - פרטי כניסה של WebAuthn – אם הוא נרשם.
  • ממשק לניהול פרטי כניסה: רשימת פרטי כניסה שמאפשרת למשתמשים לשנות את השם ולמחוק את פרטי הכניסה.

16ce77744061c5f7.png

אתם מוזמנים לעיין באפליקציית האינטרנט המוגמרת ולנסות אותה.

2. מידע על WebAuthn

WebAuthn בסיסי

למה WebAuthn?

פישינג היא בעיית אבטחה עצומה באינטרנט: רוב הפרצות באבטחת החשבון ממנפות סיסמאות חלשות או גנובות שנעשה בהן שימוש חוזר באתרים שונים. התגובה המשותפת של התעשייה הזו לבעיה הייתה אימות רב-גורמי, אבל ההטמעות מקוטעות ורבות מהן עדיין לא מטפלות באופן הולם בפישינג.

Web Authentication API או WebAuthn הוא פרוטוקול סטנדרטי ועמיד בפני פישינג, שיכול לשמש כל אפליקציית אינטרנט.

איך זה עובד?

מקור: webauthn.guide

WebAuthn מאפשר לשרתים לרשום ולאמת משתמשים באמצעות הצפנת מפתח ציבורי במקום סיסמה. אתרים יכולים ליצור פרטי כניסה המבוססים על זוג מפתחות ציבוריים.

  • המפתח הפרטי מאוחסן באופן מאובטח במכשיר של המשתמש.
  • המפתח הציבורי ומזהה פרטי הכניסה שנוצרו באופן אקראי נשלחים לשרת לאחסון.

המפתח הציבורי משמש את השרת להוכחת זהות המשתמש. הוא לא סודי כי אין בו שימוש ללא המפתח הפרטי המתאים.

יתרונות

ל-WebAuthn יש שני יתרונות עיקריים:

  • אין סוד משותף: השרת לא מאחסן סוד. פעולה זו הופכת את מסדי הנתונים לאטרקטיביים יותר להאקרים, מפני שהמפתחות הציבוריים אינם שימושיים עבורם.
  • פרטי כניסה עם היקף שנקבע: לא ניתן להשתמש בפרטי הכניסה שרשומים עבור site.example ב-evil-site.example. פעולה זו מונעת פישינג באינטרנט באמצעות WebAuthn.

תרחישים לדוגמה

תרחיש לדוגמה אחד עבור WebAuthn הוא אימות דו-שלבי עם מפתח אבטחה. הדבר עשוי להיות רלוונטי במיוחד לאפליקציות אינטרנט ארגוניות.

תמיכה בדפדפנים

הוא נכתב על ידי W3C ו-FIDO, בהשתתפות Google, Mozilla, Microsoft, Yubico ואחרים.

מילון מונחים

  • מאמת: ישות תוכנה או חומרה שיכולה לרשום משתמש ומאוחר יותר להצהיר בעלות על פרטי הכניסה הרשומים. יש שני סוגים של מאמתי חשבונות:
  • מאמת נדידה: רכיב אימות (authenticator) שניתן להשתמש בו עם כל מכשיר שממנו המשתמש מנסה להיכנס. דוגמה: מפתח אבטחה בחיבור USB, סמארטפון.
  • מאמת פלטפורמה: מאמת חשבונות מובנה במכשיר של המשתמש. דוגמה: Touch ID של Apple.
  • פרטי כניסה: זוג המפתחות הציבוריים
  • הצד המוסמך: האתר (השרת של) האתר שמנסה לאמת את המשתמש
  • שרת FIDO: השרת המשמש לאימות. FIDO היא משפחה של פרוטוקולים שפותחו על ידי ברית ה-FIDO; אחד מהפרוטוקולים האלה הוא WebAuthn.

בסדנה הזו, נשתמש במאמת חשבונות בנדידה.

3. לפני שמתחילים

מה תצטרך להכין

כדי להשלים את קוד Lab הזה, תצטרכו:

  • הבנה בסיסית של WebAuthn.
  • ידע בסיסי ב-JavaScript וב-HTML.
  • דפדפן עדכני שתומך ב-WebAuthn.
  • מפתח אבטחה תואם ל-U2F.

אפשר להשתמש באחד מהמפתחות הבאים כמפתח אבטחה:

  • טלפון Android עם Android>=7 (Nougat) שמפעיל את Chrome. במקרה כזה, יהיה צורך גם במחשב Windows , macOS או Chrome OS עם ה-Bluetooth פועל.
  • מפתח USB, כמו YubiKey.

6539dc7ffec2538c.png

מקור: https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

מה תלמדו

תוכלו ללמוד 👋

  • איך לרשום מפתח אבטחה ולהשתמש בו כגורם שני באימות WebAuthn.
  • איך להפוך את התהליך הזה לידידותי למשתמש.

לא תלמדו ❌

  • איך בונים שרת FIDO – השרת המשמש לאימות. הפעולה הזו נכונה כי לרוב, כאפליקציית אינטרנט או מפתחי אתרים, אתם מסתמכים על הטמעות קיימות של שרתי FIDO. חשוב לוודא תמיד את הפונקציונליות והאיכות של ההטמעות של השרת שאתם מסתמכים עליהן. במעבדה זו, שרת FIDO משתמש ב-SimpleWebAuthn. לאפשרויות נוספות, עיינו בדף הרשמי של ברית הברית (FiDO Alliance). מידע על ספריות קוד פתוח זמין בכתובת webauthn.io או AwesomeWebAuthn.

כתב ויתור

המשתמש צריך להזין סיסמה כדי להיכנס. עם זאת, כדי לפשט את ה-codelab הזה, הסיסמה לא נשמרת ולא מסומנת. באפליקציה אמיתית, יש לבדוק שהיא נכונה בצד השרת.

בבדיקות הקוד האלה יושמו בדיקות אבטחה בסיסיות כמו בדיקות CSRF, אימות סשן וסניטציה של קלט. עם זאת, אמצעי אבטחה רבים לא מהווים, למשל, הגבלה של סיסמאות על מנת למנוע מתקפות של Brute Force. זה לא חשוב כאן כי הסיסמאות לא נשמרות, אבל אין להשתמש בקוד הזה כפי שהוא בסביבת הייצור.

4. הגדרת מאמת החשבונות

אם אתם משתמשים בטלפון Android כמאמת

  • מוודאים ש-Chrome מעודכן גם במחשב וגם בטלפון.
  • גם במחשב וגם בטלפון, פותחים את Chrome ונכנסים לאותו פרופיל שבו רוצים להשתמש בסדנת Google.
  • צריך להפעיל את הסנכרון של הפרופיל הזה במחשב ובטלפון. לשם כך, צריך להשתמש בכתובת chrome://settings/syncSetup.
  • מפעילים את ה-Bluetooth במחשב ובטלפון.
  • במחשב Chrome שמחובר לאותו פרופיל, פותחים את webauthn.io.
  • צריך להזין שם משתמש פשוט. משאירים את סוג האימות ואת סוג המאמת עבור הערכים ללא ולא מוגדר (ברירת המחדל). לוחצים על רישום.

6b49ff0298f5a0af.png

  • ייפתח חלון דפדפן שבו תתבקשו לאמת את הזהות שלכם. בוחרים את הטלפון שלכם מתוך הרשימה.

ffebe58ac826eaf2.png 852de328fcd4eb42.png

  • בטלפון אמורה להופיע התראה בשם אימות הזהות שלך. מקישים עליה.
  • בטלפון, תתבקשו להזין את קוד האימות של הטלפון (או לגעת בחיישן טביעות האצבע). מזינים אותו.
  • ב-webauthn.io במחשב, יופיע אינדיקטור "הצלחה"

fc0aa00aad412fa.png

  • ב-webauthn.io במחשב, לוחצים על לחצן ההתחברות.
  • שוב, נפתח חלון דפדפן. יש לבחור את הטלפון שלכם מהרשימה.
  • בטלפון, מקישים על ההתראה הקופצת ומזינים את קוד האימות (או נוגעים בחיישן טביעות האצבע).
  • webauthn.io צריך לדעת שהתחברת. הטלפון פועל כמו שצריך כמפתח אבטחה. הכול מוכן לסדנה!

אם אתה משתמש במפתח אבטחה בחיבור USB כמאמת

  • במחשב, פותחים את webauthn.io.
  • צריך להזין שם משתמש פשוט. משאירים את סוג האימות ואת סוג המאמת עבור הערכים ללא ולא מוגדר (ברירת המחדל). לוחצים על רישום.
  • ייפתח חלון דפדפן שבו תתבקשו לאמת את הזהות שלכם. בוחרים באפשרות מפתח אבטחה בחיבור USB ברשימה.

ffebe58ac826eaf2.png 9fe75f04e43da035.png

  • צריך להכניס את מפתח האבטחה לשולחן העבודה ולגעת בו.

923d5adb8aa8286c.png

  • ב-webauthn.io במחשב, יופיע אינדיקטור "הצלחה"

fc0aa00aad412fa.png

  • ב-webauthn.io במחשב, לוחצים על הלחצן התחברות.
  • שוב, ייפתח חלון דפדפן. בחר מפתח אבטחה של USB ברשימה.
  • נוגעים במפתח.
  • תוצג ב-Webauthn.io מידע על כך שהתחברת. מפתח האבטחה ב-USB פועל כראוי. הכול מוכן לסדנה!

7e1c0bb19c9f3043.png

5. להגדרה

במעבדה זו, תשתמשו ב-Glitch, עורך קוד מקוון, שלפרוס את הקוד באופן אוטומטי ומיידי.

פיצול הקוד למתחילים

פותחים את הפרויקט למתחילים.

לוחצים על הלחצן רמיקס.

הפעולה הזו יוצרת עותק של הקוד למתחילים. עכשיו יש לכם קוד משלכם כדי לערוך. המזלג (שם של מירכאות, "remix" ב-Glitch) הוא המקום שבו ניתן לעשות את כל העבודה עבור מעבדת קוד זו.

cf2b9f552c9809b6.png

מגלים את הקוד למתחילים

בואו לגלות את הקוד למתחילים שבו פוצלתם קצת.

חשוב לדעת שאחרי libs, כבר יש ספרייה בשם auth.js. זו ספרייה מותאמת אישית שמטפלת בלוגיקת האימות בצד השרת. היא משתמשת בספרייה fido בתור תלות.

6. הטמעה של רישום פרטי הכניסה

הטמעה של רישום פרטי הכניסה

כדי להגדיר אימות דו-שלבי באמצעות מפתח אבטחה, הדבר הראשון שאנחנו צריכים לעשות הוא לאפשר למשתמש ליצור פרטי כניסה.

בואו ונוסיף קודם פונקציה שעושים זאת בקוד שלנו בצד הלקוח.

ב-public/auth.client.js, חשוב לזכור שיש פונקציה בשם registerCredential()שלא עושה דבר עדיין. מוסיפים אליו את הקוד הבא:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    "/auth/credential-options",
    "POST"
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
    }
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Send the encoded credential to the backend for storage
  return await _fetch("/auth/credential", "POST", encodedCredential);
}

חשוב לזכור שהפונקציה הזו כבר מיוצאת בשבילך.

registerCredential עושה זאת:

  • הוא מאחזר את האפשרויות ליצירת פרטי כניסה מהשרת (/auth/credential-options)
  • מכיוון שאפשרויות השרת חוזרות לקודד, הן משתמשות בפונקציית הכלי decodeServerOptions כדי לפענח אותן.
  • הוא יוצר פרטי כניסה על ידי קריאה ל-API של האינטרנט navigator.credential.create. כשמתבצעת קריאה ל-navigator.credential.create, הדפדפן משתנה ומבקש מהמשתמש לבחור מפתח אבטחה.
  • הוא מקודד את פרטי הכניסה החדשים שנוצרו
  • זהו רישום של פרטי הכניסה החדשים בצד השרת על ידי שליחת בקשה אל /auth/credential שמכילה את פרטי הכניסה.

בצד: מבט בקוד השרת

registerCredential() מבצע שתי קריאות לשרת, לכן כדאי להקדיש רגע ולבדוק מה קורה בקצה העורפי.

אפשרויות ליצירת פרטי כניסה

כשהלקוח שולח בקשה (/auth/credential-options), השרת יוצר אובייקט אפשרויות ושולח אותו בחזרה ללקוח.

לאחר מכן הלקוח משתמש באובייקט הזה בשיחה בפועל ליצירת פרטי כניסה:

navigator.credentials.create({
    publicKey: {
    // Options generated server-side
    ...credentialCreationOptions
// ...
}

אז מה עושים בcredentialCreationOptions הזה שבסופו של דבר נעשה שימוש בregisterCredential של הצד הלקוח?

יש לבדוק את קוד השרת בקטע router.post("/credential-option", ....

לא נבחן את כל הנכסים, אבל הנה כמה נכסים מעניינים שתוכלו לראות באובייקט אפשרויות הקוד, שנוצרו באמצעות הספרייה של fido2 ובסופו של דבר מוחזרים ללקוח:

  • rpName ו-rpId מתארים את הארגון שרושם את המשתמש ומאמת אותו. חשוב לזכור שב-WebAuthn, פרטי הכניסה נכללים בדומיין מסוים, שהוא יתרון אבטחה; rpName ו-rpId כאן משמשים להגדרת פרטי הכניסה. rpId חוקי הוא לדוגמה שם המארח של האתר שלך. שימו לב איך הם מתעדכנים באופן אוטומטי כשפוצצים את הפרויקט למתחילים 🧘🏻 ♀️
  • excludeCredentials היא רשימה של פרטי כניסה. לא ניתן ליצור את פרטי הכניסה החדשים במאמת שמכיל גם אחד מפרטי הכניסה הרשומים ב-excludeCredentials. במעבדה שלנו, excludeCredentials הוא רשימה של פרטי הכניסה הקיימים של המשתמש הזה. באמצעות חשבון זה ו-user.id, אנחנו מוודאים שכל פרטי כניסה שהמשתמש יוצר יהיו פעילים במאמת חשבונות (מפתח אבטחה אחר). זו שיטה מומלצת, כי אם המשתמש רשם כמה פרטי כניסה, הוא ישתמש במאמתי חשבונות שונים (מפתחות אבטחה), ולכן אובדן מפתח אבטחה אחד לא יאפשר למשתמש להיכנס לחשבון שלו.
  • authenticatorSelection מגדיר את סוג המאמתים שברצונך לאפשר באפליקציה האינטרנט. נבחן מקרוב את authenticatorSelection:
    • המשמעות של residentKey: preferred היא שהאפליקציה הזו לא אוכפת פרטי כניסה מול הלקוח. פרטי כניסה ניתנים לזיהוי בצד הלקוח הם סוג של פרטי כניסה המאפשרים לאמת משתמש ללא צורך בזיהוי שלו. כאן, הגדרנו את preferred כי Lablab זה מתמקד בהטמעה הבסיסית. פרטי כניסה גלויים מיועדים לתהליכים מתקדמים יותר.
    • המוצר requireResidentKey קיים רק עבור תאימות לאחור עם WebAuthn v1.
    • userVerification: preferred – אם המאמת תומך באימות המשתמש — לדוגמה, אם מדובר במפתח אבטחה ביומטרי או במפתח עם תכונת קוד אימות מובנית – הצד המוסמך יבקש זאת במהלך יצירת פרטי הכניסה. אם מאמת החשבונות לא תומך במפתח האבטחה הבסיסי, השרת לא יבקש אימות משתמש.
  • ​​pubKeyCredParam מתאר, לפי סדר העדיפויות, את מאפייני הקריפטוגרפיה הרצויים של פרטי הכניסה.

כל האפשרויות האלה הן החלטות שאפליקציית האינטרנט צריכה לקבל עבור מודל האבטחה שלה. יש לשים לב שבשרת, האפשרויות האלה מוגדרות באובייקט authSettings יחיד.

האתגר

עוד דבר מעניין נוסף כאן: req.session.challenge = options.challenge;.

מכיוון ש-WebAuthn הוא פרוטוקול קריפטוגרפי, הוא תלוי באתגרים אקראיים כדי למנוע התקפות חוזרות – כאשר תוקף גונב מטען כדי להפעיל את האימות מחדש, כשהוא לא הבעלים של המפתח הפרטי שיאפשר אימות.

כדי לפתור את הבעיה, נוצר אתגר בשרת והוא נחתם בזמן אמת. לאחר מכן המערכת תשווה את החתימה למה שציפית. הפעולה הזו מאמתת שהמשתמש מחזיק את המפתח הפרטי בזמן יצירת פרטי הכניסה.

קוד רישום פרטי כניסה

יש לבדוק את קוד השרת בקטע router.post("/credential", ... .

כאן מזינים את פרטי הכניסה בצד השרת.

מה קורה בקטע הזה?

אחת הביטים החשובים ביותר בקוד הזה היא שיחת האימות, באמצעות fido2.verifyAttestationResponse:

  • בדיקת האתגר החתומה נבדקת כדי לוודא שפרטי הכניסה נוצרו על ידי מישהו ששמר בפועל את המפתח הפרטי בזמן היצירה.
  • המזהה של הצד המוסמך, המשויך למקור שלו, אומת גם הוא. הפעולה הזו מבטיחה שפרטי הכניסה משויכים לאפליקציית האינטרנט הזו (ורק באפליקציית האינטרנט הזו).

הוספת הפונקציונליות הזו לממשק המשתמש

עכשיו, אם הפונקציה מאפשרת ליצור פרטי כניסה, ``enrollcredential(),הוא מוכן, ההרשאה תהיה זמינה למשתמש.

ניתן לעשות זאת מהדף חשבון, מפני שזהו מיקום רגיל לניהול אימות.

בתגי העיצוב של account.html' מתחת לשם המשתמש, יש div עד עכשיו ריק עם פריסת פריסה class="flex-h-between". נשתמש ב-div הזה לרכיבי ממשק משתמש שקשורים לפונקציונליות 2FA.

הוספת ino זה div:

  • כותרת שהשם שלה הוא "אימות דו-שלבי&&;
  • לחצן ליצירת פרטי כניסה
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

מתחת ל-div הזה, מוסיפים div פרטי כניסה שנחוץ לנו מאוחר יותר:

<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>

בסקריפט המוטבע account.html, מייבאים את הפונקציה שיצרתם הרגע ומוסיפים פונקציה register שקוראים לה. כמו כן, מוסיפים handler של אירוע שמצורף ללחצן שיצרתם הרגע.

// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);

// Register a credential
async function register() {
  let user = {};
  try {
    const user = await registerCredential();
  } catch (e) {
    // Alert the user that something went wrong
    if (Array.isArray(e)) {
      alert(
        // `msg` not `message`, this is the key's name as per the express validator API
        `Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
      );
    } else {
      alert(`Registration failed. ${e}`);
    }
  }
}

הצגת פרטי הכניסה שהמשתמש יכול לראות

עכשיו לאחר שהוספתם את הפונקציונליות ליצירת פרטי כניסה, המשתמשים צריכים לראות את פרטי הכניסה שהם הוסיפו.

הדף חשבון הוא מקום טוב לעשות זאת.

ב-account.html, צריך לחפש את הפונקציה updateCredentialList().

יש להוסיף אליו את הקוד הבא שמבצע קריאה לקצה העורפי כדי לאחזר את כל פרטי הכניסה הרשומים של המשתמש המחובר כרגע, ולהציג את פרטי הכניסה שהוחזרו:

// Update the list that displays credentials
async function updateCredentialList() {
  // Fetch the latest credential list from the backend
  const response = await _fetch('/auth/credentials', 'GET');
  const credentials = response.credentials || [];
  // Generate the credential list as HTML and pass remove/rename functions as args
  const credentialListHtml = getCredentialListHtml(
    credentials,
    removeEl,
    renameEl
  );
  // Display the list of credentials in the DOM
  const list = document.querySelector('#credentials');
  render(credentialListHtml, list);
}    

בינתיים לא אכפת לך מ-removeEl ומ-renameEl; כדאי ללמוד עליהם מאוחר יותר ב-Codelab הזה.

צריך להוסיף קריאה אחת אל updateCredentialList בתחילת הסקריפט המוטבע: account.html. במסגרת השיחה הזו, פרטי הכניסה הזמינים מאוחזרים כאשר המשתמש נוחת בדף החשבון שלו.

<script type="module">
    // ... (imports)
    // Initialize the credential list by updating it once on page load
    updateCredentialList();

עכשיו יש להתקשר אל updateCredentialList אחרי השלמת ההגדרה של registerCredential, כך שברשימות יוצגו פרטי הכניסה החדשים שנוצרו:

async function register() {
  let user = {};
  try {
    // ...
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

כדאי לנסות! 👩🏻 💻

סיימת את תהליך רישום פרטי הכניסה! עכשיו המשתמשים יכולים ליצור פרטי כניסה המבוססים על מפתח אבטחה, ולהמחיש אותם בדף חשבון.

נסה את החיפושים הבאים:

  • יציאה.
  • התחברות — עם כל משתמש וסיסמה. כפי שצוין קודם לכן, הסיסמה אינה נבדקת בפועל כדי לוודא שהיא נכונה, כדי שהדברים יהיו פשוטים במעבדה זו. יש להזין סיסמה שאינה ריקה.
  • בדף חשבון, לוחצים על הוספת פרטי כניסה.
  • אמורה להופיע בקשה להכניס מפתח אבטחה ולגעת בו. עשו זאת.
  • לאחר יצירת פרטי הכניסה בהצלחה, יש להציג את פרטי הכניסה בדף החשבון.
  • טוענים מחדש את הדף חשבון. פרטי הכניסה אמורים להופיע.
  • אם יש שני מפתחות זמינים, נסו להוסיף שני מפתחות אבטחה שונים בתור פרטי כניסה. יש להציג את שניהם.
  • נסו ליצור שני פרטי כניסה עם אותו מאמת (מפתח); אתם תבחינו שלא תהיה תמיכה בהם. הסיבה לכך היא שאנחנו משתמשים ב-excludeCredentials בקצה העורפי.

7. הפעלת אימות דו-שלבי

המשתמשים שלך יכולים לרשום פרטי כניסה ולבטל את הרישום שלהם, אבל פרטי הכניסה מוצגים בלבד ועדיין לא נעשה בהם שימוש בפועל.

זה הזמן להשתמש בהם ולהגדיר אימות דו-שלבי בפועל.

בקטע הזה צריך לשנות את תהליך האימות באפליקציית האינטרנט מהתהליך הבסיסי הזה:

6ff49a7e520836d0.png

לזרימת הדו-שלבי הזו:

e7409946cd88efc7.png

הטמעה של אימות דו-שלבי

בואו ונוסיף תחילה את הפונקציונליות הדרושה ונטמיע את התקשורת עם הקצה העורפי; נוסיף אותה בממשק הקצה בשלב הבא.

מה שצריך להטמיע כאן הוא פונקציה שמאמתת את המשתמש באמצעות פרטי כניסה.

ב-public/auth.client.js, צריך לחפש את הפונקציה הריקה authenticateTwoFactor ולהוסיף לה את הקוד הבא:

async function authenticateTwoFactor() {
  // Fetch the 2F options from the backend
  const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
  // Decode them
  const decodedOptions = decodeServerOptions(optionsFromServer);
  // Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.get({
    publicKey: decodedOptions
  });
  // Encode the credential
  const encodedCredential = encodeCredential(credential);
  // Send it to the backend for verification
  return await _fetch("/auth/authenticate-two-factor", "POST", {
    credential: encodedCredential
  });
}

חשוב לזכור שהפונקציה הזו כבר מיוצאת. אנחנו צריכים אותה בשלב הבא.

authenticateTwoFactor עושה זאת:

  • הוא מבקש מהשרת שתי אפשרויות אימות. בדיוק כמו האפשרויות ליצירת פרטי כניסה שראית בעבר, גם הן מוגדרות בשרת ותלויות במודל האבטחה של אפליקציית האינטרנט. פרטים נוספים זמינים בקוד השרת מתחת ל-router.post("/two-factors-options", ....
  • שיחה אל navigator.credentials.get מאפשרת לדפדפן להשתלט על המשתמש ולבקש ממנו להכניס מפתח שנרשם בעבר ולגעת בו. פעולה זו תגרום לבחירה של פרטי כניסה עבור פעולת האימות הזו.
  • לאחר מכן פרטי הכניסה שנבחרו מועברים בבקשת קצה עורפי כדי לאחזר &"/auth/authenticate-two-fact"`. אם פרטי הכניסה תקפים למשתמש הזה, המשתמש מאומת.

בצד: מבט בקוד השרת

חשוב לשים לב שהדומיין server.js כבר מטפל בחלק מהניווט ומהגישה: הוא מבטיח שרק משתמשים מאומתים יוכלו לגשת לדף החשבון והוא יבצע כמה הפניות לכתובות אחרות.

עכשיו אפשר לבדוק את קוד השרת של router.post("/initialize-authentication", ....

יש שתי נקודות מעניינות שכדאי לשים לב אליהן:

  • גם הסיסמה וגם פרטי הכניסה נבדקים בו-זמנית בשלב הזה. זהו אמצעי אבטחה: עבור משתמשים שמגדירים אימות דו-שלבי, אנחנו לא רוצים שזרימות ממשק המשתמש ייראו שונות, בהתבסס על סיסמה נכונה. לכן, אנחנו בודקים גם את הסיסמה וגם את פרטי הכניסה בבת אחת.
  • אם הסיסמה ופרטי הכניסה תקפים, אנחנו משלימים את האימות בהתקשרות אל completeAuthentication(req, res);. בפועל, אנחנו מחליפים בין פעילויות באתר, מביקור זמני ב-auth שבו המשתמש עדיין לא מאומת, main לסשן הראשי שבו המשתמש מאומת.

כלול את דף האימות של גורם שני בתהליך המשתמש

בתיקייה views, שימו לב לדף החדש second-factor.html.

יש בו לחצן עם הכיתוב יש להשתמש במפתח אבטחה, אבל בינתיים אין צורך לעשות דבר.

יש ללחוץ על הלחצן הזה כדי להתקשר אל authenticateTwoFactor() בלחיצה.

  • אם המשתמש authenticateTwoFactor() הצליח, עליך להפנות את המשתמש לדף החשבון שלו.
  • אם התגובה לבקשה לא מוצלחת, צריך להודיע למשתמש שאירעה שגיאה. באפליקציה אמיתית, עליך ליישם הודעות שגיאה מועילות יותר – כדי לפשט את הדברים בהדגמה הזו, נשתמש בהתראה על חלון בלבד.
    <main>
...
    </main>
    <script type="module">
      import { authenticateTwoFactor, authStatuses } from "/auth.client.js";

      const button = document.querySelector("#authenticateButton");
      button.addEventListener("click", async e => {
        try {
          // Ask the user to authenticate with the second factor; this will trigger a browser prompt
          const response = await authenticateTwoFactor();
          const { authStatus } = response;
          if (authStatus === authStatuses.COMPLETE) {
            // The user is properly authenticated => Navigate to the Account page
            location.href = "/account";
          } else {
            throw new Error("Two-factor authentication failed");
          }
        } catch (e) {
          // Alert the user that something went wrong
          alert(`Two-factor authentication failed. ${e}`);
        }
      });
    </script>
  </body>
</html>

שימוש באימות דו-שלבי

הכול מוכן להוספה של שלב אימות דו-שלבי.

מה שצריך לעשות עכשיו הוא להוסיף את השלב הזה מ-index.html, למשתמשים שהגדירו אימות דו-שלבי.

322a5c49d865a0d8.png

ב-index.html, מתחת ל-location.href = "/account";, מוסיפים קוד שמנווט את המשתמש באופן מותנה לדף האימות השני, אם הוא הגדיר 2FA.

ב-Codelab הזה, יצירת פרטי כניסה מוסיפה את המשתמש באופן אוטומטי לאימות דו-שלבי.

חשוב לשים לב שמערכת server.js מיישמת גם בדיקת פעילות בצד השרת, כדי להבטיח שרק משתמשים מאומתים יוכלו לגשת אל account.html.

const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
  // The user is properly authenticated => navigate to account
  location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
  // Navigate to the two-factor-auth page because two-factor-auth is set up for this user
  location.href = '/second-factor';
}

כדאי לנסות! 👩🏻 💻

  • מתחברים לחשבון עם משתמש חדש johndoe.
  • התנתקות.
  • מתחברים לחשבון כ-johndoe. בודקים אם נדרשת רק סיסמה.
  • יצירת פרטי כניסה. פירוש הדבר הוא שהפעלת אימות דו-שלבי בתור johndoe.
  • התנתקות.
  • מזינים את שם המשתמש johndoe והסיסמה שלכם.
  • מידע נוסף על הניווט האוטומטי בדף האימות של גורם שני.
  • (כדאי לנסות לגשת לדף חשבון בכתובת /account. חשוב לציין כי מתבצעת הפניה אוטומטית לדף האינדקס כי אין לך אימות מלא: חסר לך גורם נוסף.)
  • חוזרים לדף האימות של הגורם השני ולוחצים על שימוש במפתח האבטחה כדי לבצע אימות דו-שלבי.
  • עכשיו יש חיבור לחשבון והוא אמור להופיע בדף חשבון.

8. שימוש קל יותר בפרטי הכניסה

סיימת עם הפונקציונליות הבסיסית של אימות דו-שלבי עם מפתח אבטחה 🚀

אבל... האם הבחנת?

נכון לעכשיו, רשימת פרטי הכניסה שלך לא נוחה במיוחד: מזהה פרטי הכניסה והמפתח הציבורי הם מחרוזות ארוכות שאינן מועילות בעת ניהול פרטי הכניסה! בני אדם לא טובים מדי במחרוזות ארוכות ובמספרים 🤖

אז בואו נשפר זאת, ונוסיף פונקציונליות כדי לתת שם לפרטי הכניסה ולשנות את השם שלהם באמצעות מחרוזות הניתנות לקריאה על ידי אנשים.

עיון ב-NameName

כדי לחסוך לכם זמן בהטמעה של הפונקציה הזו שלא עושה דבר פורץ דרך, נוספה פונקציה לשינוי שם פרטי הכניסה בקוד למתחילים, בauth.client.js:

async function renameCredential(credId, newName) {
  const params = new URLSearchParams({
    credId,
    name: newName
  });
  return _fetch(
    `/auth/credential?${params}`,
    "PUT"
  );
}

זוהי קריאה רגילה לעדכון מסד נתונים: הלקוח שולח לבקשת הקצה העורפי PUT, עם מזהה פרטי כניסה ושם חדש לפרטי הכניסה האלה.

הטמעה של שמות מותאמים אישית לפרטי כניסה

בaccount.html, שימו לב לפונקציה הריקה rename.

מוסיפים אליו את הקוד הבא:

// Rename a credential
async function rename(credentialId) {
  // Let the user input a new name
  const newName = window.prompt(`Name this credential:`);
  // Rename only if the user didn't cancel AND didn't enter an empty name
  if (newName && newName.trim()) {
    try {
      // Make the backend call to rename the credential (the name is sanitized) server-side
      await renameCredential(credentialId, newName);
    } catch (e) {
      // Alert the user that something went wrong
      if (Array.isArray(e)) {
        alert(
          // `msg` not `message`, this is the key's name as per the express validator API
          `Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
        );
      } else {
        alert(`Renaming failed. ${e}`);
      }
    }
    // Refresh the credential list to display the new name
    await updateCredentialList();
  }
}

הגיוני יותר לתת שם לפרטי הכניסה רק לאחר יצירת פרטי הכניסה בהצלחה. לכן, עליך ליצור פרטי כניסה ללא שם. לאחר יצירת פרטי הכניסה, יש לשנות את השם שלהם. עם זאת, התוצאה היא שתי שיחות לקצה העורפי.

יש להשתמש בפונקציה rename שב-register() כדי לאפשר למשתמשים לתת שמות לפרטי כניסה בזמן ההרשמה:

async function register() {
  let user = {};
  try {
    const user = await registerCredential();
    // Get the latest credential's ID (newly created credential)
    const allUserCredentials = user.credentials;
    const newCredential = allUserCredentials[allUserCredentials.length - 1];
    // Rename it
    await rename(newCredential.credId);
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

לתשומת ליבכם, קלט של משתמש יאומת וחיטוי בקצה העורפי:

  check("name")
    .trim()
    .escape()

הצגת שמות של פרטי כניסה

יש לעבור אל getCredentialHtml ב-templates.js.

לתשומת ליבך, כבר יש קוד שמציג את השם של פרטי הכניסה בחלק העליון של כרטיס פרטי הכניסה:

// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
 const { name, credId, publicKey } = credential;
 return html`
    <div class="credential-card">
      <div class="credential-name">
        ${name
          ? html`
              ${name}
            `
          : html`
              <span class="unnamed">(Unnamed)</span>
            `}
      </div>
     // ...
    </div>
  `;
};

כדאי לנסות! 👩🏻 💻

  • יצירת פרטי כניסה.
  • תוצג לכם בקשה לתת לו שם.
  • מזינים שם חדש ולוחצים על אישור.
  • פרטי הכניסה שונו עכשיו.
  • חוזרים על הפעולה ובודקים שהכול פועל כמו שצריך, אם משאירים את שדה השם ריק.

הפעלת שינוי שם של פרטי כניסה

יכול להיות שהמשתמשים יצטרכו לשנות את השם של פרטי הכניסה – לדוגמה, הם מוסיפים מפתח שני ורוצים לשנות את השם של המפתח הראשון כדי להבדיל ביניהם טוב יותר.

ב-account.html, צריך לחפש את הפונקציה renameEl ריקה עד עכשיו ולהוסיף לה את הקוד הבא:

// Rename a credential via HTML element
async function renameEl(el) {
  // Define the ID of the credential to update
  const credentialId = el.srcElement.dataset.credentialId;
  // Rename the credential
  await rename(credentialId);
  // Refresh the credential list to display the new name
  await updateCredentialList();
}

עכשיו, ב-getCredentialHtml&class="flex-end"

const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
 <div class="flex-end">
  <button
    data-credential-id="${credId}"
    @click="${renameEl}"
    class="secondary right"
  >
   Rename
  </button>
 </div>
 // ...
  `;
};

כדאי לנסות! 👩🏻 💻

  • לוחצים על שינוי שם.
  • יש להזין שם חדש כשתופיע בקשה.
  • לוחצים על אישור.
  • יש לשנות את שם פרטי הכניסה והרשימה אמורה להתעדכן באופן אוטומטי.
  • כשטוענים מחדש את הדף, השם החדש עדיין אמור להופיע (פעולה זו מציינת שהשם החדש נשאר בצד השרת).

הצגת התאריך ליצירת פרטי כניסה

תאריך היצירה לא קיים בפרטי הכניסה שנוצרו ב-navigator.credential.create().

אבל בגלל שהמידע הזה יכול להיות שימושי למשתמש כדי להבחין בין פרטי הכניסה, שינינו את הספרייה בצד השרת בקוד ההתחלה והוספנו שדה creationDate השווה ל-Date.now() בעת אחסון פרטי כניסה חדשים.

בתוך templates.js בתוך class="creation-date" div, יש להוסיף את הפרטים הבאים כדי להציג למשתמש פרטי תאריך יצירה:

<div class="creation-date">
  <label>Created:</label>
  <div class="info">
    ${new Date(creationDate).toLocaleDateString()}
    ${new Date(creationDate).toLocaleTimeString()}
  </div>
</div>

9. הקוד שלך יהיה ידידותי בעתיד

עד עכשיו, ביקשנו מהמשתמש לרשום רק מאמת חשבונות בנדידה. אפשר להשתמש בו כגורם שני במהלך הכניסה לחשבון.

גישה מתקדמת יותר היא להסתמך על סוג מאמת מתקדם יותר: מאמת אימות בנדידה (UVRA). UVRA יכול לספק שני גורמי אימות ועמידות בפני פישינג בתהליך הכניסה.

באופן אידיאלי, אתם תומכים בשתי הגישות. לשם כך, עליכם להתאים אישית את חוויית המשתמש:

  • אם למשתמש יש רק מאמת נדידה פשוט (שאינו אימות של משתמשים), אפשר לו להשתמש בו כדי להשיג רצועת מגפיים לחשבון עמיד בפני פישינג, אבל הוא יצטרך גם להקליד שם משתמש וסיסמה. זה מה ש-Codelab כבר עושה.
  • אם יש משתמש אחר שיש לו אימות מתקדם יותר של אימות משתמשים בנדידה, הוא יוכל לדלג על שלב הסיסמה (ואולי אפילו השלב של שם המשתמש) במהלך הפעלת אתחול של חשבון.

למידע נוסף על כך, ניתן להיכנס לאתחול לחשבון העמיד בפני ניסיונות פישינג באמצעות כניסה אופציונלית ללא סיסמה.

במעבדת קוד זו, לא נוסיף את חוויית המשתמש, אבל נגדיר את בסיס הקוד שלכם כדי שיהיו לכם את הנתונים הדרושים לכם כדי להתאים אישית את חוויית המשתמש.

צריך שני דברים:

  • צריך להגדיר את residentKey: preferred בהגדרות של הקצה העורפי. הפעולה הזו כבר בוצעה עבורך.
  • מגדירים דרך לבדוק אם נוצרו פרטי כניסה ניתנים לגילוי (שנקראים גם מפתחות תושבות).

כדי לבדוק אם נוצר פרטי כניסה ניתנים לגילוי או לא:

  • יצירת שאילתה לגבי הערך credProps בעת יצירת פרטי כניסה (credProps: true).
  • יצירת שאילתה לגבי הערך transports בעת יצירת פרטי כניסה. כך תוכלו לקבוע אם הפלטפורמה הבסיסית תומכת בפונקציונליות של UVRA, למשל אם מדובר בטלפון נייד.
  • מאחסנים את הערך של credProps ושל transports בקצה העורפי. פעולה זו כבר בוצעה עבורך בקוד למתחילים. אם יש לך סקרנות, אפשר לעיין ב-auth.js.

רוצה לקבל את הערך של credProps ושל transports ולשלוח אותו לקצה העורפי? ב-auth.client.js, משנים את registerCredential באופן הבא:

  • הוספת שדה extensions בשיחה עם navigator.credentials.create
  • הגדרת encodedCredential.transports ו-encodedCredential.credProps לפני שליחת פרטי הכניסה לקצה העורפי של האחסון.

registerCredential אמור להיראות כך:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    '/auth/credential-options',
    'POST'
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
      extensions: {
        credProps: true,
      },
    },
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Set transports and credProps for more advanced user flows
  encodedCredential.transports = credential.response.getTransports();
  encodedCredential.credProps =
    credential.getClientExtensionResults().credProps;
  // Send the encoded credential to the backend for storage
  return await _fetch('/auth/credential', 'POST', encodedCredential);
}

10. תמיכה בתמיכה בדפדפנים שונים

תמיכה בדפדפנים שאינם Chromium

בפונקציה registerCredential&339 של public/auth.client.js, אנחנו קוראים ל-credential.response.getTransports() בפרטי הכניסה החדשים שנוצרו כדי לשמור את המידע הזה בקצה העורפי כרמז לשרת.

עם זאת, getTransports() לא מיושם כרגע בכל הדפדפנים (בשונה מ-getClientExtensionResults שנתמך בדפדפנים שונים): הקריאה אל getTransports() תגרור שגיאה ב-Firefox וב-Safari, מה שימנע את יצירת פרטי הכניסה בדפדפנים האלה.

כדי לוודא שהקוד שלך יפעל בכל הדפדפנים העיקריים, צריך להקיף את השיחה ב-encodedCredential.transports בתנאי:

if (credential.response.getTransports) {
  encodedCredential.transports = credential.response.getTransports();
}

הערה: בשרת, transports מוגדר ל-transports || []. ב-Firefox וב-Safari, הרשימה transports לא תהיה undefined אלא רשימה ריקה [] שמונעת שגיאות.

אזהרת משתמשים שמשתמשים בדפדפנים שלא תומכים ב-WebAuthn

1e9c1be837d66ce8.png

אמנם WebAuthn נתמך בכל הדפדפנים העיקריים, אבל כדאי להציג אזהרה בדפדפנים שלא תומכים ב-WebAuthn.

בindex.html, יש לבחון את נוכחותו של המשתנה הזה:

<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>

בסקריפט המוטבע index.html'יש להוסיף את הקוד הבא כדי להציג את הבאנר בדפדפנים שלא תומכים ב-WebAuthn:

// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
  document.querySelector('#warningbanner').classList.remove('invisible');
}

באפליקציית אינטרנט אמיתית, ניתן לבצע פעולה מורכבת יותר באמצעות מנגנון גיבוי חלופי לדפדפנים אלה, אך כך תוכלו לבדוק אם יש תמיכה ב-WebAuthn.

11. כל הכבוד!

✨סיימת!

הטמעת אימות דו-שלבי באמצעות מפתח אבטחה.

במעבדה זו, דיברנו על העקרונות הבסיסיים. אם ברצונך לחקור את WebAuthn ל-2FA עוד, הנה כמה רעיונות לדברים שאפשר לנסות:

  • מוסיפים את פרטי הציטוט &שנעשה בהם שימוש אחרון&quot בכרטיס פרטי הכניסה. המידע הזה שימושי למשתמשים כדי לקבוע אם נעשה שימוש במפתח אבטחה פעיל או לא, במיוחד אם הם רשמו מספר מפתחות.
  • מיישמים טיפול טוב יותר בשגיאות והודעות שגיאה מדויקות יותר.
  • כדאי לבדוק את auth.js ולגלות מה קורה כשמשנים חלק מ-authSettings, במיוחד כשמשתמשים במפתח שתומך באימות משתמשים.