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

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

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

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

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

דרישות מוקדמות

מה תלמדו

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

2. להגדרה

ב-codelab הזה, תשכפלו אפליקציית הדגמה לא שלמה מ-GitHub, ואז תסיימו את ההטמעה של תמיכה במפתחות גישה.

שכפול הפרויקט

  1. פותחים את הפרויקט ב-GitHub.
  2. משכפלים או מורידים את הפרויקט.

ac587c53b746785a.png

הרצת הפרויקט

  1. פותחים טרמינל ומקלידים cd start כדי לשנות את הספרייה.
  2. מריצים את הפקודה npm install כדי להתקין את יחסי התלות של הפרויקט.
  3. בונים ומריצים את הפרויקט באמצעות npm run build && IS_LOCAL=1 npm run start.
  4. פותחים את http://localhost:8080/ בדפדפן.

בדיקת המצב ההתחלתי של האתר

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

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

מידע נוסף על אופן הפעולה של מפתחות גישה זמין במאמר איך פועלים מפתחות גישה?

3. הוספת אפשרות ליצירת מפתח גישה

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

9b84dbaec66afe9c.png

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

יצירת פונקציית registerCredential()

  1. בכלי לעריכת קוד שבוחרים, פותחים את הספרייה start.
  2. עוברים לקובץ public/client.js וגוללים לסוף.
  3. אחרי התגובה הרלוונטית, מוסיפים את הפונקציה הבאה registerCredential():

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to create a passkey: Create a credential.

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

};

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

קבלת האתגר ואפשרויות אחרות מנקודת הקצה של השרת

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

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

public/client.js

// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
const _options = await _fetch('/auth/registerRequest');

קטע הקוד הבא כולל אפשרויות לדוגמה שמתקבלות מהשרת:

{
  challenge: *****,
  rp: {
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },  
  pubKeyCredParams: [{
    alg: -7, type: "public-key"
  },{
    alg: -257, type: "public-key"
  }],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal', 'hybrid'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

הפרוטוקול בין השרת ללקוח לא נכלל במפרט WebAuthn. עם זאת, השרת של ה-codelab הזה נועד להחזיר קובץ JSON שדומה ככל האפשר למילון PublicKeyCredentialCreationOptions שמועבר ל-API של WebAuthn ‏navigator.credentials.create().

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

פרמטרים

תיאורים

challenge

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

user.id

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

user.name

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

user.displayName

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

rp.id

מזהה הצד המסתמך (RP) הוא דומיין. אתר יכול לציין את הדומיין שלו או סיומת שניתן לרשום. לדוגמה, אם המקור של ספק הזהויות הוא https://login.example.com:1337, מזהה ספק הזהויות יכול להיות login.example.com או example.com. אם מזהה ה-RP מוגדר כ-example.com, המשתמש יכול לבצע אימות ב-login.example.com או בכל תת-דומיין אחר של example.com.

pubKeyCredParams

בשדה הזה מציינים את האלגוריתמים של המפתחות הציבוריים שנתמכים על ידי ה-RP. מומלץ להגדיר את הערך [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. ההגדרה הזו מציינת תמיכה ב-ECDSA עם P-256 וב-RSA PKCS#1, ותמיכה בהן מספקת כיסוי מלא.

excludeCredentials

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

authenticatorSelection.authenticatorAttachment

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

authenticatorSelection.requireResidentKey

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

authenticatorSelection.userVerification

מגדירים ערך של "preferred" או משמיטים אותו כי הוא ערך ברירת המחדל. ההגדרה הזו מציינת אם אימות המשתמש באמצעות נעילת המסך של המכשיר הוא "required", "preferred" או "discouraged". אם מגדירים ערך של "preferred", המערכת תבקש אימות משתמש כשהמכשיר תומך בכך. מידע נוסף זמין במאמר ניתוח מעמיק של אימות משתמשים.

יצירת פרטי כניסה

  1. בגוף הפונקציה registerCredential() אחרי ההערה הרלוונטית, ממירים בחזרה לבינארי פרמטרים מסוימים שמקודדים באמצעות Base64URL, במיוחד המחרוזות user.id ו-challenge, ומופעים של המחרוזת id שכלולים במערך excludeCredentials. אפשר לעשות זאת באמצעות הפונקציה PublicKeyCredential.parseCreationOptionsFromJSON():

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.

// Deserialize and decode the `PublicKeyCredential.parseCreationOptionsFromJSON()`.
const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);
  1. בשורה הבאה, מגדירים את authenticatorSelection.authenticatorAttachment ל-"platform" ואת authenticatorSelection.requireResidentKey ל-true. ההגדרה הזו מאפשרת שימוש רק באמצעי אימות של הפלטפורמה (המכשיר עצמו) עם יכולת של פרטי כניסה שניתן לגלות.

public/client.js

// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
  authenticatorAttachment: 'platform',
  requireResidentKey: true
}
  1. בשורה הבאה, קוראים ל-method‏ navigator.credentials.create() כדי ליצור אישור.

public/client.js

// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
  publicKey: options,
});

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

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

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

בקטע הקוד הבא יש דוגמה לאובייקט של פרטי כניסה:

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "attestationObject": *****,
    "transports": ["internal", "hybrid"]
  },
  "authenticatorAttachment": "platform"
}

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

פרמטרים

תיאורים

id

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

rawId

גרסת האובייקט ArrayBuffer של מזהה האישורים.

response.clientDataJSON

אובייקט ArrayBuffer שמכיל נתוני לקוח מקודדים.

response.attestationObject

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

response.transports

רשימת פרוטוקולי התקשורת שהמכשיר תומך בהם: "internal" מציין שהמכשיר תומך במפתח גישה. "hybrid" אומר שהמכשיר תומך גם באימות במכשיר אחר.

authenticatorAttachment

הפונקציה מחזירה "platform" כשאמצעי האימות הזה נוצר במכשיר שתומך במפתחות גישה.

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

  1. מקודדים את הפרמטרים הבינאריים של פרטי הכניסה בפורמט Base64URL כדי שאפשר יהיה להעביר אותם לשרת כמחרוזת. כדי לעשות את זה, אפשר להשתמש ב-.toJSON():

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

// Encode and serialize the `PublicKeyCredential`.
const credential = JSON.stringify(cred);
  1. בשורה הבאה, שולחים את האובייקט לשרת:

public/client.js

return await _fetch('/auth/registerResponse', credential);

כשמריצים את התוכנית, השרת מחזיר HTTP code 200, שמציין שפרטי הכניסה רשומים.

עכשיו יש לכם את הפונקציה המלאה registerCredential().

בדיקת קוד הפתרון של הקטע הזה

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.

export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.

  const _options = await _fetch('/auth/registerRequest');

  // TODO: Add an ability to create a passkey: Create a credential.

  // Deserialize and decode the `PublicKeyCredential.parseCreationOptionsFromJSON()`.
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);

  // Use platform authenticator and discoverable credential.
  options.authenticatorSelection = {
    authenticatorAttachment: 'platform',
    requireResidentKey: true
  }

  // Invoke the WebAuthn create() method.
  const cred = await navigator.credentials.create({
    publicKey: options,
  });

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

  // Encode and serialize the `PublicKeyCredential`.
  const credential = JSON.stringify(cred);

  return await _fetch('/auth/registerResponse', credential);
};

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

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

bfa4e7cdda47669e.png

הוספת HTML של פלייסהולדר

  1. בעורך, עוברים לקובץ views/home.html.
  2. אחרי התגובה הרלוונטית, מוסיפים placeholder של ממשק משתמש שמציג לחצן לרישום מפתח גישה ורשימה של מפתחות גישה:

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3>Your registered passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mdui-button id="create-passkey" class="hidden" icon="fingerprint" type="button">Create a passkey</mdui-button>

הרכיב div#list הוא ה-placeholder של הרשימה.

בדיקה אם יש תמיכה במפתחות גישה

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

כדי לבדוק אם סביבה תומכת במפתחות גישה, פועלים לפי השלבים הבאים:

  1. בסוף הקובץ views/home.html, אחרי ההערה הרלוונטית, כותבים תנאי שמופעל אם window.PublicKeyCredential, PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable ו-PublicKeyCredential.isConditionalMediationAvailable הם true.

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  1. בגוף התנאי, בודקים אם המכשיר יכול ליצור מפתח גישה ואז בודקים אם אפשר להציע את מפתח הגישה במילוי אוטומטי של טופס.

views/home.html

try {
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    // Is conditional UI available in this browser?
    if (capabilities.conditionalGet === true &&
        capabilities.passkeyPlatformAuthenticator === true) {
  1. אם כל התנאים מתקיימים, מוצג לחצן ליצירת מפתח גישה. אחרת, מוצגת הודעת אזהרה.

views/home.html

      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

הצגת מפתחות גישה רשומים ברשימה

  1. מגדירים פונקציית renderCredentials() שמביאה מפתחות גישה רשומים מהשרת ומציגה אותם ברשימה. למזלכם, כבר יש לכם את נקודת הקצה של השרת /auth/getKeys כדי לאחזר מפתחות גישה רשומים עבור המשתמש שמחובר לחשבון.

views/home.html

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = res.length > 0 ? html`
    <mdui-list>
      ${res.map(cred => html`
        <mdui-list-item>
          ${cred.name || 'Unnamed'}
          <mdui-button-icon data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed'}" @click="${rename}" icon="edit" slot="end-icon"></mdui-button-icon>
          <mdui-button-icon data-cred-id="${cred.id}" @click="${remove}" icon="delete" slot="end-icon"></mdui-button-icon>
        </mdui-list-item>`)}
    </mdui-list>` : html`
    <mdui-list>
      <mdui-list-item>No credentials found.</mdui-list-item>
    </mdui-list>`;
  render(creds, list);
};
  1. בשורה הבאה, מפעילים את הפונקציה renderCredentials() כדי להציג את מפתחות הגישה הרשומים ברגע שהמשתמש מגיע לדף /home כאתחול.

views/home.html

renderCredentials();

יצירה ורישום של מפתח גישה

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

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

  1. בקובץ אחרי ה-placeholder של ה-HTML, מחפשים את ההצהרה הבאה של import:

views/home.html

import { 
  $, 
  _fetch, 
  loading, 
  updateCredential, 
  unregisterCredential, 
} from '/client.js';
  1. בסוף גוף ההצהרה import, מוסיפים את הפונקציה registerCredential().

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
  registerCredential
} from '/client.js';
  1. בסוף הקובץ, אחרי התגובה הרלוונטית, מגדירים פונקציה register() שמפעילה את הפונקציה registerCredential() וממשק משתמש לטעינה, וקוראת לפונקציה renderCredentials() אחרי הרשמה. ההסבר הזה מבהיר שהדפדפן יוצר מפתח גישה ומציג הודעת שגיאה אם משהו משתבש.

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  1. בגוף הפונקציה register(), תופסים חריגים. השיטה navigator.credentials.create() מחזירה שגיאה מסוג InvalidStateError אם מפתח גישה כבר קיים במכשיר. הבדיקה מתבצעת באמצעות המערך excludeCredentials. במקרה כזה, אתם מציגים למשתמש הודעה רלוונטית. היא גם מחזירה שגיאת NotAllowedError אם המשתמש מבטל את תיבת הדו-שיח לאימות. במקרה כזה, מתעלמים ממנה בשקט.

views/home.html

  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};
  1. בשורה שאחרי הפונקציה register(), מצרפים את הפונקציה register() לאירוע click של הלחצן יצירת מפתח גישה.

views/home.html

createPasskey.addEventListener('click', register);

בדיקת קוד הפתרון של הקטע הזה

views/home.html

<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3>Your registered passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mdui-button id="create-passkey" icon="fingerprint" type="button">Create a passkey</mdui-button>

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import { 
  $, 
  _fetch, 
  loading, 
  updateCredential, 
  unregisterCredential, 
  registerCredential 
} from '/client.js';

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');

// Is WebAuthn available in this browser?
if (window.PublicKeyCredential &&
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
  PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    // Is conditional UI available in this browser?
    if (capabilities.conditionalGet === true &&
      capabilities.passkeyPlatformAuthenticator === true) {
      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {
      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {
  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.

async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
    <mdui-list>
      ${res.map(cred => html`
        <mdui-list-item>
          ${cred.name || 'Unnamed'}
          <mdui-button-icon data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed'}" @click="${rename}" icon="edit" slot="end-icon"></mdui-button-icon>
          <mdui-button-icon data-cred-id="${cred.id}" @click="${remove}" icon="delete" slot="end-icon"></mdui-button-icon>
        </mdui-list-item>`)}
    </mdui-list>` : html`
    <mdui-list>
      <mdui-list-item>No credentials found.</mdui-list-item>
    </mdui-list>`}`;
  render(creds, list);
};

renderCredentials();

// TODO: Add an ability to create a passkey: Create and register a passkey.

async function register() {
  try {
    // Start the loading UI.
    loading.start();
    // Start creating a passkey.
    await registerCredential();
    // Stop the loading UI.
    loading.stop();
    // Render the updated passkey list.
    renderCredentials();
  } catch (e) {
    // Stop the loading UI.
    loading.stop();
    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');
      // A NotAllowedError indicates the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      return;
      // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};

createPasskey.addEventListener('click', register);

אני רוצה לנסות

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

כדי לנסות את התכונה, מבצעים את השלבים הבאים:

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

מפתחות הגישה הרשומים מופיעים בדף /home.

שינוי שם והסרה של מפתחות גישה רשומים

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

ב-Chrome, אפשר להסיר מפתחות גישה רשומים מהכתובת chrome://settings/passkeys במחשב או ממנהל הסיסמאות בהגדרות ב-Android.

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

5. הוספת אפשרות לאימות באמצעות מפתח גישה

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

יצירת פונקציית authenticate()

  • בקובץ public/client.js, אחרי ההערה הרלוונטית, יוצרים פונקציה בשם authenticate() שמאמתת את המשתמש באופן מקומי ואז מול השרת:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.

};

קבלת האתגר ואפשרויות אחרות מנקודת הקצה של השרת

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

  • בגוף הפונקציה authenticate(), אחרי ההערה הרלוונטית, קוראים לפונקציה _fetch() כדי לשלוח בקשת POST לשרת:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.

// Base64URL decode the challenge.
const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);

השרת של ה-codelab הזה מתוכנן להחזיר JSON שדומה ככל האפשר למילון PublicKeyCredentialRequestOptions שמועבר ל-WebAuthn navigator.credentials.get() API. קטע הקוד הבא כולל אפשרויות לדוגמה שצריכות להתקבל:

{
  "challenge": *****,
  "rpId": "localhost",
  "allowCredentials": []
}

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

פרמטרים

תיאורים

challenge

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

rpId

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

allowCredentials

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

userVerification

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

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

  1. בגוף הפונקציה authenticate() אחרי ההערה הרלוונטית, ממירים את הפרמטר challenge בחזרה לבינארי:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
  1. מעבירים מערך ריק לפרמטר allowCredentials כדי לפתוח את בורר החשבונות כשמשתמש מאמת את עצמו:

public/client.js

// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];

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

  1. מבצעים קריאה ל-navigator.credentials.get() עם אפשרות mediation: 'conditional':

public/client.js

// Invoke the WebAuthn get() method.
const cred = await navigator.credentials.get({
  publicKey: options,

  // Request a conditional UI.
  mediation: 'conditional'
});

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

אימות האישורים

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

קטע הקוד הבא כולל אובייקט לדוגמה PublicKeyCredential:

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "authenticatorData": *****,
    "signature": *****,
    "userHandle": *****
  },
  authenticatorAttachment: "platform"
}

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

פרמטרים

תיאורים

id

המזהה של פרטי הכניסה של מפתח הגישה המאומת בקידוד Base64URL.

rawId

גרסת האובייקט ArrayBuffer של מזהה האישורים.

response.clientDataJSON

אובייקט ArrayBuffer של נתוני לקוחות. השדה הזה מכיל מידע, כמו האתגר והמקור ששרת ה-RP צריך לאמת.

response.authenticatorData

אובייקט ArrayBuffer של נתוני אמצעי אימות. השדה הזה מכיל מידע כמו מזהה RP.

response.signature

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

response.userHandle

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

authenticatorAttachment

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

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

  1. בגוף הפונקציה authenticate(), אחרי ההערה הרלוונטית, מקודדים את הפרמטרים הבינאריים של פרטי הכניסה כדי שאפשר יהיה להעביר אותם לשרת כמחרוזת. כדי לעשות את זה, אפשר להשתמש ב-.toJSON():

public/client.js

// TODO: Add an ability to authenticate with a passkey: Verify the credential.
// Encode and serialize the `PublicKeyCredential`.
const credential = JSON.stringify(cred);
  1. שולחים את האובייקט לשרת:

public/client.js

return await _fetch(`/auth/signinResponse`, credential);

כשמריצים את התוכנית, השרת מחזיר HTTP code 200, שמציין שפרטי הכניסה אומתו.

עכשיו יש לכם את הפונקציה המלאה authentication().

בדיקת קוד הפתרון של הקטע הזה

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the 
  challenge and other options from the server endpoint.
  const options = await _fetch('/auth/signinRequest');

  // TODO: Add an ability to authenticate with a passkey: Locally verify 
  the user and get a credential.
  // Base64URL decode the challenge.
  options.challenge = base64url.decode(options.challenge);

  // The empty allowCredentials array invokes an account selector 
  by discoverable credentials.
  options.allowCredentials = [];

  // Invoke the WebAuthn get() function.
  const cred = await navigator.credentials.get({
    publicKey: options,

    // Request a conditional UI.
    mediation: 'conditional'
  });

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const authenticatorData = 
  base64url.encode(cred.response.authenticatorData);
  const signature = base64url.encode(cred.response.signature);
  const userHandle = base64url.encode(cred.response.userHandle);

  credential.response = {
    clientDataJSON,
    authenticatorData,
    signature,
    userHandle,
  };

  return await _fetch(`/auth/signinResponse`, credential);
};

6. הוספת מפתחות גישה למילוי אוטומטי בדפדפן

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

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

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

d616744939063451.png

הפעלת ממשק משתמש מותנה

כדי להפעיל ממשק משתמש מותנה, צריך רק להוסיף טוקן webauthn במאפיין autocomplete של שדה להזנת קלט. אחרי שמגדירים את קבוצת האסימונים, אפשר להפעיל את השיטה navigator.credentials.get() עם המחרוזת mediation: 'conditional' כדי להפעיל את ממשק המשתמש של נעילת המסך בתנאי.

  • כדי להפעיל ממשק משתמש מותנה, מחליפים את שדות הקלט הקיימים של שם המשתמש בקוד ה-HTML הבא אחרי ההערה הרלוונטית בקובץ view/index.html:

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<mdui-text-field id="username" label="Username" name="username" autocomplete="username webauthn" autofocus></mdui-text-field>

זיהוי תכונות, הפעלת WebAuthn והפעלת ממשק משתמש מותנה

  1. בקובץ view/index.html, אחרי התגובה הרלוונטית, מחליפים את ההצהרה הקיימת import בקוד הבא:

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
  $,
  _fetch,
  loading,
  authenticate 
} from "/client.js";

הקוד הזה מייבא את הפונקציה authenticate() שהטמעתם קודם.

  1. מוודאים שאובייקט window.PulicKeyCredential זמין ושהמתודה PublicKeyCredential.isConditionalMediationAvailable() מחזירה ערך true, ואז קוראים לפונקציה authenticate():

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
if (window.PublicKeyCredential &&
    PublicKeyCredential.getClientCapabilities) {
  try {

    // Is conditional UI available in this browser?
      const capabilities = await PublicKeyCredential.getClientCapabilities();
      if (capabilities.conditionalGet) {

      // If conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $("#username").value = user.username;
        loading.start();
        location.href = "/home";
      } else {
        throw new Error("User not found.");
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== "NotAllowedError") {
      console.error(e);
      alert(e.message);
    }
  }
}

בדיקת קוד הפתרון של הקטע הזה

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<mdui-text-field id="username" label="Username" name="username" autocomplete="username webauthn" autofocus></mdui-text-field>

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import { 
  $, 
  _fetch, 
  loading, 
  authenticate 
} from '/client.js';

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.        

// Is WebAuthn available on this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.getClientCapabilities) {
  try {
    // Is conditional UI available in this browser?
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    if (capabilities.conditionalGet) {
      // If conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {
        // Proceed only when authentication succeeds.
        $('#username').value = user.username;
        loading.start();
        location.href = '/home';
      } else {
        throw new Error('User not found.');
      }
    }
  } catch (e) {
    loading.stop();
    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== 'NotAllowedError') {
      console.error(e);
      alert(e.message);
    }
  }
}

אני רוצה לנסות

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

כדי לנסות את התכונה, מבצעים את השלבים הבאים:

  1. עוברים לכרטיסייה 'תצוגה מקדימה'.
  2. אם צריך, יוצאים מהחשבון.
  3. לוחצים על תיבת הטקסט של שם המשתמש. מופיעה תיבת דו-שיח.
  4. בוחרים את החשבון שאיתו רוצים להיכנס.
  5. מאמתים את הזהות באמצעות השיטה לפתיחת הנעילה של המכשיר. תועברו לדף /home ותיכנסו לחשבון.

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

7. מעולה!

סיימתם את ה-Codelab הזה! אם יש לכם שאלות, אתם יכולים לשאול אותן ברשימת התפוצה FIDO-DEV או ב-StackOverflow עם התג passkey.

מידע נוסף