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

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

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

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

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

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

מה תלמדו

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

מה נדרש

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

  • ‫Google Chrome עם מכשיר Android בגרסה 9 ואילך, רצוי עם חיישן ביומטרי.
  • ‫Chrome במכשיר Windows עם Windows מגרסה 10 ואילך.
  • ‫Safari 16 ואילך באייפון עם iOS 16 ואילך, או באייפד עם iPadOS 16 ואילך.
  • ‫Safari מגרסה 16 ומעלה או Chrome במכשיר שולחני של Apple עם macOS Ventura ומעלה.

2. להגדרה

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

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

  1. פותחים את הפרויקט ב-Glitch.
  2. לוחצים על Remix כדי ליצור עותק של פרויקט Glitch.
  3. בתפריט הניווט בחלק התחתון של Glitch, לוחצים על תצוגה מקדימה > תצוגה מקדימה בחלון חדש. תיפתח כרטיסייה נוספת בדפדפן.

הלחצן 'תצוגה מקדימה בחלון חדש' בתפריט הניווט בחלק התחתון של Glitch

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

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

במצב הזה, המשתמשים צריכים להזין סיסמה בכל פעם שהם מתחברים. אתם מוסיפים תמיכה במפתחות גישה לטופס הזה כדי שהמשתמשים יוכלו להיכנס באמצעות הפונקציה של נעילת המסך במכשיר. אפשר לנסות את מצב הסיום בכתובת https://passkeys-codelab.glitch.me/.

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

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

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

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

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

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

  1. ב-Glitch, עוברים לקובץ public/client.js וגוללים לסוף.
  2. אחרי התגובה הרלוונטית, מוסיפים את הפונקציה הבאה 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 שמועבר אל WebAuthn navigator.credentials.create() API.

הטבלה הבאה לא כוללת את כל הפרמטרים, אבל היא מכילה את הפרמטרים החשובים במילון 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:

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);

if (options.excludeCredentials) {
  for (let cred of options.excludeCredentials) {
    cred.id = base64url.decode(cred.id);
  }
}
  1. בשורה הבאה, מגדירים את authenticatorSelection.authenticatorAttachment ל-"platform" ואת authenticatorSelection.requireResidentKey ל-true. ההגדרה הזו מאפשרת שימוש רק באמצעי אימות של הפלטפורמה (המכשיר עצמו) עם יכולת גילוי של פרטי הכניסה.

public/client.js

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

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 כדי שאפשר יהיה להעביר אותם לשרת כמחרוזת:

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
  credential.authenticatorAttachment = cred.authenticatorAttachment;
}

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject = base64url.encode(cred.response.attestationObject);

// Obtain transports.
const transports = cred.response.getTransports ? cred.response.getTransports() : [];

credential.response = {
  clientDataJSON,
  attestationObject,
  transports
};
  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 server endpoint.
  const options = await _fetch('/auth/registerRequest');
  
  // TODO: Add an ability to create a passkey: Create a credential.
  // Base64URL decode some values.

  options.user.id = base64url.decode(options.user.id);
  options.challenge = base64url.decode(options.challenge);

  if (options.excludeCredentials) {
    for (let cred of options.excludeCredentials) {
      cred.id = base64url.decode(cred.id);
    }
  }

  // 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.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
  if (cred.authenticatorAttachment) {
    credential.authenticatorAttachment = cred.authenticatorAttachment;
  }

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const attestationObject =  
  base64url.encode(cred.response.attestationObject);

  // Obtain transports.
  const transports = cred.response.getTransports ? 
  cred.response.getTransports() : [];

  credential.response = {
    clientDataJSON,
    attestationObject,
    transports
  };

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

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

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

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

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

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

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3 class="mdc-typography mdc-typography--headline6"> Your registered 
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-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 results = await Promise.all([

    // Is platform authenticator available in this browser?
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

    // Is conditional UI available in this browser?
    PublicKeyCredential.isConditionalMediationAvailable()
  ]);
  1. אם כל התנאים מתקיימים, מוצג לחצן ליצירת מפתח גישה. אחרת, מוצגת הודעת אזהרה.

views/home.html

    if (results.every(r => r === 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.';
}

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

  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 = html`${res.length > 0 ? html`
    <mwc-list>
      ${res.map(cred => html`
        <mwc-list-item>
          <div class="list-item">
            <div class="entity-name">
              <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}"  
            data-name="${cred.name || 'Unnamed' }" @click="${rename}"  
            icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" 
            icon="delete"></mwc-icon-button>
          </div>
         </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-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 class="mdc-typography mdc-typography--headline6"> Your registered  
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-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');

// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const results = await Promise.all([

      // Is platform authenticator available in this browser?
      PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

      // Is conditional UI available in this browser?
      PublicKeyCredential.isConditionalMediationAvailable()
    ]);
    if (results.every(r => r === 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`
  <mwc-list>
    ${res.map(cred => html`
      <mwc-list-item>
        <div class="list-item">
          <div class="entity-name">
            <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed' }" @click="${rename}" icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" icon="delete"></mwc-icon-button>
          </div>
        </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-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 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);
    }
  }
};

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.
const options = await _fetch('/auth/signinRequest');

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

{
  "challenge": *****,
  "rpId": "passkeys-codelab.glitch.me",
  "allowCredentials": []
}

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

פרמטרים

תיאורים

challenge

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

rpId

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

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 שמכיל את מזהה המשתמש שהוגדר בזמן היצירה. אפשר להשתמש בערך הזה במקום במזהה האישורים אם השרת צריך לבחור את ערכי המזהים שבהם הוא משתמש, או אם ה-Backend רוצה להימנע מיצירת אינדקס במזהי האישורים.

authenticatorAttachment

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

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

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

public/client.js

// 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,
};
  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 שמאפשרת לכם ליצור שדה קלט בטופס כדי להציע מפתח גישה כחלק מפריטים של מילוי אוטומטי, בנוסף לסיסמאות. אם משתמש מקיש על מפתח גישה בהצעות למילוי אוטומטי, הוא מתבקש להשתמש בנעילת המסך של המכשיר כדי לאמת את הזהות שלו באופן מקומי. חוויית המשתמש חלקה כי פעולת המשתמש כמעט זהה לפעולה של כניסה שמבוססת על סיסמה.

מפתח גישה מוצע כחלק ממילוי אוטומטי של טופס.

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

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

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

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus />

זיהוי תכונות, הפעלת 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.isConditionalMediationAvailable
) {
  try {

    // Is conditional UI available in this browser?
    const cma =
      await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // 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. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus 
/>

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 avaiable in this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {

    // Is a conditional UI available in this browser?
    const cma= await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If a 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.

מידע נוסף