רוצה ליצור את אפליקציית WebAuthn הראשונה שלך?

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

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

ה-API תומך בשימוש במאמתי UBLE F, NFC ו-USB בנדידה – הידועים גם כמפתחות אבטחה – וגם במאמת פלטפורמה, שמאפשר למשתמשים לאמת את טביעות האצבע או את נעילות המסך שלהם.

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

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

  • הבנה בסיסית של אופן הפעולה של WebAuthn
  • מיומנויות תכנות בסיסיות עם JavaScript

הפעולות שתבצעו:

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

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

  • אחד מהמכשירים הבאים:
    • מכשיר Android, רצוי עם חיישן ביומטרי
    • iPhone או iPad עם Touch ID או Face ID ב-iOS 14 ואילך
    • MacBook Pro או Air עם Touch ID ב-macOS Big Sur או גרסה מתקדמת יותר
    • Windows 10 מגרסה 19H1 ואילך עם הגדרת Windows Hello
  • אחד מהדפדפנים הבאים:
    • Google Chrome מגרסה 67 ואילך
    • Microsoft Edge בגרסה 85 ואילך
    • Safari בגרסה 14 ומעלה

2. להגדרה

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

נכנסים לכתובת https://glitch.com/edit/#!/webauthn-codelab-start.

איך זה עובד

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

  1. לוחצים על 62bb7a6aac381af8.png הצגה > 3343769d04c09851.png בחלון חדש כדי לראות את האתר הפעיל.
  2. מזינים שם משתמש לבחירתכם ולוחצים על הבא.
  3. מזינים סיסמה ולוחצים על כניסה.

המערכת מתעלמת מהסיסמה, אבל אתה עדיין מאומת. אתם מגיעים לדף הבית.

  1. לוחצים על Try reauth וחוזרים על השלבים השני, השלישי והרביעי.
  2. לוחצים על יציאה.

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

רמיקס של הקוד

  1. כניסה אל WebAuthn / FIDO2 API Lab.
  2. לוחצים על שם הפרויקט > הרמיקס Project 306122647ce93305.png כדי לפצל את הפרויקט ולהמשיך לגרסה משלכם בכתובת URL חדשה.

8d42bd24f0fd185c.png

3. רישום פרטי כניסה באמצעות טביעת אצבע

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

הוספת התכונה הזו לדף /home:

260aab9f1a2587a7.png

יצירת פונקציה אחת (registerCredential())

יש ליצור פונקציה registerCredential() כדי לרשום פרטי כניסה חדשים.

public/client.js

export const registerCredential = async () => {

};

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

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

יש להוסיף את הקוד הבא אל registerCredential().

public/client.js

const opts = {
  attestation: 'none',
  authenticatorSelection: {
    authenticatorAttachment: 'platform',
    userVerification: 'required',
    requireResidentKey: false
  }
};

const options = await _fetch('/auth/registerRequest', opts);

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

פרמטרים

תיאורים

attestation

העדפה להעברה של אימות (attestation) – none, indirect או direct. יש לבחור ב-none אם לא צריך.

excludeCredentials

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

authenticatorSelection

authenticatorAttachment

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

userVerification

קובעים אם אימות משתמש מקומי של מאמת החשבונות הוא " required", "preferred" או " discouraged". אם רוצים לאמת אימות באמצעות טביעת אצבע או נעילת מסך, יש להשתמש ב-"required".

requireResidentKey

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

מידע נוסף על האפשרויות האלה מפורט 5.4. אפשרויות ליצירת פרטי כניסה (מילון PublicKeyCredentialCreationOptions).

לפניכם אפשרויות לדוגמה שאתם מקבלים מהשרת.

{
  "rp": {
    "name": "WebAuthn Codelab",
    "id": "webauthn-codelab.glitch.me"
  },
  "user": {
    "displayName": "User Name",
    "id": "...",
    "name": "test"
  },
  "challenge": "...",
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    }, {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [
    {
      "id": "...",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "userVerification": "required"
  }
}

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

  1. מכיוון שהאפשרויות האלה מקודדות לעבור פרוטוקול HTTP, יש להמיר חלק מהפרמטרים חזרה לבינארי. באופן ספציפי, user.id, challenge ומופעים של id הכלולים במערך excludeCredentials:

public/client.js

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. צריך להתקשר לשיטה navigator.credentials.create() כדי ליצור פרטי כניסה חדשים.

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

public/client.js

const cred = await navigator.credentials.create({
  publicKey: options,
});

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

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

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

{
  "id": "...",
  "rawId": "...",
  "type": "public-key",
  "response": {
    "clientDataJSON": "...",
    "attestationObject": "..."
  }
}
  1. למשל, כשמקבלים אובייקט אפשרות לרישום פרטי כניסה, מקודדים את הפרמטרים הבינאריים של פרטי הכניסה כדי שניתן יהיה להעביר אותם לשרת כמחרוזת:

public/client.js

const credential = {};
credential.id = cred.id;
credential.rawId = base64url.encode(cred.rawId);
credential.type = cred.type;

if (cred.response) {
  const clientDataJSON =
    base64url.encode(cred.response.clientDataJSON);
  const attestationObject =
    base64url.encode(cred.response.attestationObject);
  credential.response = {
    clientDataJSON,
    attestationObject,
  };
}
  1. מאחסנים את מזהה פרטי הכניסה באופן מקומי כדי להשתמש בו לאימות כשהמשתמש חוזר.

public/client.js

localStorage.setItem(`credId`, credential.id);
  1. שולחים את האובייקט לשרת, ואם הוא מחזיר את HTTP code 200, צריך לרשום את פרטי הכניסה החדשים בתור נרשמים בהצלחה.

public/client.js

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

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

הקוד הסופי של הקטע הזה

public/client.js

...
export const registerCredential = async () => {
  const opts = {
    attestation: 'none',
    authenticatorSelection: {
      authenticatorAttachment: 'platform',
      userVerification: 'required',
      requireResidentKey: false
    }
  };

  const options = await _fetch('/auth/registerRequest', opts);

  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);
    }
  }
  
  const cred = await navigator.credentials.create({
    publicKey: options
  });

  const credential = {};
  credential.id =     cred.id;
  credential.rawId =  base64url.encode(cred.rawId);
  credential.type =   cred.type;

  if (cred.response) {
    const clientDataJSON =
      base64url.encode(cred.response.clientDataJSON);
    const attestationObject =
      base64url.encode(cred.response.attestationObject);
    credential.response = {
      clientDataJSON,
      attestationObject
    };
  }

  localStorage.setItem(`credId`, credential.id);
  
  return await _fetch('/auth/registerResponse' , credential);
};
...

4. יש לך אפשרות לבנות את ממשק המשתמש כדי לרשום, לקבל ולהסיר פרטי כניסה

מומלץ לכלול רשימה של פרטי כניסה ולחצנים רשומים כדי להסיר אותם.

9b5b5ae4a7b316bd.png

placeholder לבניית ממשק משתמש

יש להוסיף ממשק משתמש כדי לרשום פרטי כניסה ולחצן לרישום פרטי כניסה חדשים. תלוי אם התכונה זמינה או לא, מסירים את הכיתה hidden מהודעת האזהרה או מהלחצן לרישום פרטי כניסה חדשים. ul#list הוא ה-placeholder להוספת רשימה של פרטי הכניסה הרשומים.

צפיות/home.html

<p id="uvpa_unavailable" class="hidden">
  This device does not support User Verifying Platform Authenticator. You can't register a credential.
</p>
<h3 class="mdc-typography mdc-typography--headline6">
  Your registered credentials:
</h3>
<section>
  <div id="list"></div>
</section>
<mwc-button id="register" class="hidden" icon="fingerprint" raised>Add a credential</mwc-button>

זיהוי תכונות וזמינות UVPA

כך בודקים את הזמינות של UVPA:

  1. יש לבדוק את window.PublicKeyCredential כדי לראות אם WebAuthn זמין.
  2. התקשרו אל PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() כדי לבדוק אם UVPA זמין . אם הם זמינים, מציגים את הלחצן לרישום פרטי כניסה חדשים. אם אף אחת מהאפשרויות האלה אינה זמינה, תופיע הודעת אזהרה.

צפיות/home.html

const register = document.querySelector('#register');

if (window.PublicKeyCredential) {
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(uvpaa => {
    if (uvpaa) {
      register.classList.remove('hidden');
    } else {
      document
        .querySelector('#uvpa_unavailable')
        .classList.remove('hidden');
    }
  });        
} else {
  document
    .querySelector('#uvpa_unavailable')
    .classList.remove('hidden');
}

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

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

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

צפיות/home.html

const getCredentials = async () => {
  const res = await _fetch('/auth/getKeys');
  const list = document.querySelector('#list');
  const creds = html`${res.credentials.length > 0 ? res.credentials.map(cred => html`
    <div class="mdc-card credential">
      <span class="mdc-typography mdc-typography--body2">${cred.credId}</span>
      <pre class="public-key">${cred.publicKey}</pre>
      <div class="mdc-card__actions">
        <mwc-button id="${cred.credId}" @click="${removeCredential}" raised>Remove</mwc-button>
      </div>
    </div>`) : html`
    <p>No credentials found.</p>
    `}`;
  render(creds, list);
};
  1. יש להפעיל getCredentials()כדי להציג פרטי כניסה זמינים ברגע שהמשתמש נוחת בדף /home.

צפיות/home.html

getCredentials();

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

ברשימת פרטי הכניסה הוספת לחצן להסרת כל פרטי הכניסה. אפשר לשלוח בקשה אל /auth/removeKey יחד עם פרמטר השאילתה credId כדי להסיר אותה.

public/client.js

export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
  1. צירוף unregisterCredential להצהרת import הקיימת.

צפיות/home.html

import { _fetch, unregisterCredential } from '/client.js';
  1. מוסיפים פונקציה שרוצים להתקשר אליה כשמשתמש לוחץ על הסרה.

צפיות/home.html

const removeCredential = async e => {
  try {
    await unregisterCredential(e.target.id);
    getCredentials();
  } catch (e) {
    alert(e);
  }
};

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

אפשר להתקשר אל registerCredential() כדי לרשום פרטי כניסה חדשים כשהמשתמש לוחץ על הוספת פרטי כניסה.

  1. צירוף registerCredential להצהרת import הקיימת.

צפיות/home.html

import { _fetch, registerCredential, unregisterCredential } from '/client.js';
  1. הפעלה של registerCredential() עם אפשרויות עבור navigator.credentials.create().

לא לשכוח לחדש את רשימת פרטי הכניסה באמצעות התקשרות אל getCredentials() לאחר ההרשמה.

צפיות/home.html

register.addEventListener('click', e => {
  registerCredential().then(user => {
    getCredentials();
  }).catch(e => alert(e));
});

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

הקוד הסופי של הקטע הזה

צפיות/home.html

...
      <p id="uvpa_unavailable" class="hidden">
        This device does not support User Verifying Platform Authenticator. You can't register a credential.
      </p>
      <h3 class="mdc-typography mdc-typography--headline6">
        Your registered credentials:
      </h3>
      <section>
        <div id="list"></div>
        <mwc-fab id="register" class="hidden" icon="add"></mwc-fab>
      </section>
      <mwc-button raised><a href="/reauth">Try reauth</a></mwc-button>
      <mwc-button><a href="/auth/signout">Sign out</a></mwc-button>
    </main>
    <script type="module">
      import { _fetch, registerCredential, unregisterCredential } from '/client.js';
      import { html, render } from 'https://unpkg.com/lit-html@1.0.0/lit-html.js?module';

      const register = document.querySelector('#register');

      if (window.PublicKeyCredential) {
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(uvpaa => {
          if (uvpaa) {
            register.classList.remove('hidden');
          } else {
            document
              .querySelector('#uvpa_unavailable')
              .classList.remove('hidden');
          }
        });        
      } else {
        document
          .querySelector('#uvpa_unavailable')
          .classList.remove('hidden');
      }

      const getCredentials = async () => {
        const res = await _fetch('/auth/getKeys');
        const list = document.querySelector('#list');
        const creds = html`${res.credentials.length > 0 ? res.credentials.map(cred => html`
          <div class="mdc-card credential">
            <span class="mdc-typography mdc-typography--body2">${cred.credId}</span>
            <pre class="public-key">${cred.publicKey}</pre>
            <div class="mdc-card__actions">
              <mwc-button id="${cred.credId}" @click="${removeCredential}" raised>Remove</mwc-button>
            </div>
          </div>`) : html`
          <p>No credentials found.</p>
          `}`;
        render(creds, list);
      };

      getCredentials();

      const removeCredential = async e => {
        try {
          await unregisterCredential(e.target.id);
          getCredentials();
        } catch (e) {
          alert(e);
        }
      };

      register.addEventListener('click', e => {
        registerCredential({
          attestation: 'none',
          authenticatorSelection: {
            authenticatorAttachment: 'platform',
            userVerification: 'required',
            requireResidentKey: false
          }
        })
        .then(user => {
          getCredentials();
        })
        .catch(e => alert(e));
      });
    </script>
...

public/client.js

...
export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
...

5. אימות המשתמש באמצעות טביעת אצבע

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

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

b8770c4e7475b075.png

יצירת פונקציה אחת (authenticate())

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

public/client.js

export const authenticate = async () => {

};

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

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

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

public/client.js

const opts = {};

let url = '/auth/signinRequest';
const credId = localStorage.getItem(`credId`);
if (credId) {
  url += `?credId=${encodeURIComponent(credId)}`;
}
  1. לפני שהמשתמש מבקש לאמת את החשבון, צריך לבקש ממנו לשלוח אתגר ופרמטרים אחרים. קריאה ל-_fetch() עם opts כארגומנט לשליחת בקשת POST לשרת.

public/client.js

const options = await _fetch(url, opts);

הנה כמה דוגמאות לדוגמה שאמורות להופיע (תואם ל-PublicKeyCredentialRequestOptions).

{
  "challenge": "...",
  "timeout": 1800000,
  "rpId": "webauthn-codelab.glitch.me",
  "userVerification": "required",
  "allowCredentials": [
    {
      "id": "...",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ]
}

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

  1. יש לפתור את ההבטחה באמצעות null כשהמערך הוא allowCredentials ריק כדי שממשק המשתמש יחזור לבקש סיסמה.
if (options.allowCredentials.length === 0) {
  console.info('No registered credentials found.');
  return Promise.resolve(null);
}

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

  1. מכיוון שהאפשרויות האלה מקודדות כדי לעבור בפרוטוקול HTTP, יש להמיר חלק מהפרמטרים חזרה לבינארי. בייחוד את challenge והמופעים של id הכלולים במערך allowCredentials:

public/client.js

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

for (let cred of options.allowCredentials) {
  cred.id = base64url.decode(cred.id);
}
  1. יש להתקשר לשיטה navigator.credentials.get() כדי לאמת את זהות המשתמש באמצעות UVPA.

public/client.js

const cred = await navigator.credentials.get({
  publicKey: options
});

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

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

הנה אובייקט לדוגמה PublicKeyCredential (response הוא AuthenticatorAssertionResponse) שהיית אמור לקבל:

{
  "id": "...",
  "type": "public-key",
  "rawId": "...",
  "response": {
    "clientDataJSON": "...",
    "authenticatorData": "...",
    "signature": "...",
    "userHandle": ""
  }
}
  1. מקודדים את הפרמטרים הבינאריים של פרטי הכניסה כדי שניתן יהיה להעביר אותם לשרת כמחרוזת:

public/client.js

const credential = {};
credential.id = cred.id;
credential.type = cred.type;
credential.rawId = base64url.encode(cred.rawId);

if (cred.response) {
  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. שולחים את האובייקט לשרת, ואם הוא מחזיר את HTTP code 200, צריך לחשוב שהמשתמש מחובר לחשבון:

public/client.js

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

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

הקוד הסופי של הקטע הזה

public/client.js

...
export const authenticate = async () => {
  const opts = {};

  let url = '/auth/signinRequest';
  const credId = localStorage.getItem(`credId`);
  if (credId) {
    url += `?credId=${encodeURIComponent(credId)}`;
  }
  
  const options = await _fetch(url, opts);
  
  if (options.allowCredentials.length === 0) {
    console.info('No registered credentials found.');
    return Promise.resolve(null);
  }

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

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

  const cred = await navigator.credentials.get({
    publicKey: options
  });

  const credential = {};
  credential.id = cred.id;
  credential.type = cred.type;
  credential.rawId = base64url.encode(cred.rawId);

  if (cred.response) {
    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. הפעלה של חוויית אימות מחדש

ממשק משתמש של Build

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

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

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

19da999b0145054.png

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

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

צפיות/reauth.html

<div id="uvpa_available" class="hidden">
  <h2>
    Verify your identity
  </h2>
  <div>
    <mwc-button id="reauth" raised>Authenticate</mwc-button>
  </div>
  <div>
    <mwc-button id="cancel">Sign-in with password</mwc-button>
  </div>
</div>
  1. יש לצרף את class="hidden" לטופס:

צפיות/reauth.html

<form id="form" method="POST" action="/auth/password" class="hidden">

זיהוי תכונות וזמינות UVPA

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

  • WebAuthn לא זמין.
  • UVPA לא זמין.
  • מזהה פרטי הכניסה של ה-UVPA הזה לא גלוי.

הצגת לחצן האימות באופן סלקטיבי או הסתרה שלו:

צפיות/reauth.html

if (window.PublicKeyCredential) {
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(uvpaa => {
    if (uvpaa && localStorage.getItem(`credId`)) {
      document
        .querySelector('#uvpa_available')
        .classList.remove('hidden');
    } else {
      form.classList.remove('hidden');
    }
  });        
} else {
  form.classList.remove('hidden');
}

חזרה למצב ראשוני של סיסמה

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

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

צפיות/reauth.html

const cancel = document.querySelector('#cancel');
cancel.addEventListener('click', e => {
  form.classList.remove('hidden');
  document
    .querySelector('#uvpa_available')
    .classList.add('hidden');
});

c4a82800889f078c.png

הפעלת האימות הביומטרי

לבסוף, מפעילים את האימות הביומטרי.

  1. הוספה של authenticate להצהרת import הקיימת:

צפיות/reauth.html

import { _fetch, authenticate } from '/client.js';
  1. יש להפעיל את authenticate() כשהמשתמש מקיש על אימות כדי להתחיל את האימות הביומטרי.

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

צפיות/reauth.html

const button = document.querySelector('#reauth');
button.addEventListener('click', e => {
  authenticate().then(user => {
    if (user) {
      location.href = '/home';
    } else {
      throw 'User not found.';
    }
  }).catch(e => {
    console.error(e.message || e);
    alert('Authentication failed. Use password to sign-in.');
    form.classList.remove('hidden');
    document.querySelector('#uvpa_available').classList.add('hidden');
  });        
});

הקוד הסופי של הקטע הזה

צפיות/reauth.html

...
    <main class="content">
      <div id="uvpa_available" class="hidden">
        <h2>
          Verify your identity
        </h2>
        <div>
          <mwc-button id="reauth" raised>Authenticate</mwc-button>
        </div>
        <div>
          <mwc-button id="cancel">Sign-in with password</mwc-button>
        </div>
      </div>
      <form id="form" method="POST" action="/auth/password" class="hidden">
        <h2>
          Enter a password
        </h2>
        <input type="hidden" name="username" value="{{username}}" />
        <div class="mdc-text-field mdc-text-field--filled">
          <span class="mdc-text-field__ripple"></span>
          <label class="mdc-floating-label" id="password-label">password</label>
          <input type="password" class="mdc-text-field__input" aria-labelledby="password-label" name="password" />
          <span class="mdc-line-ripple"></span>
        </div>
        <input type="submit" class="mdc-button mdc-button--raised" value="Sign-In" />
        <p class="instructions">password will be ignored in this demo.</p>
      </form>
    </main>
    <script src="https://unpkg.com/material-components-web@7.0.0/dist/material-components-web.min.js"></script>
    <script type="module">
      new mdc.textField.MDCTextField(document.querySelector('.mdc-text-field'));
      import { _fetch, authenticate } from '/client.js';
      const form = document.querySelector('#form');
      form.addEventListener('submit', e => {
        e.preventDefault();
        const form = new FormData(e.target);
        const cred = {};
        form.forEach((v, k) => cred[k] = v);
        _fetch(e.target.action, cred)
        .then(user => {
          location.href = '/home';
        })
        .catch(e => alert(e));
      });

      if (window.PublicKeyCredential) {
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(uvpaa => {
          if (uvpaa && localStorage.getItem(`credId`)) {
            document
              .querySelector('#uvpa_available')
              .classList.remove('hidden');
          } else {
            form.classList.remove('hidden');
          }
        });        
      } else {
        form.classList.remove('hidden');
      }

      const cancel = document.querySelector('#cancel');
      cancel.addEventListener('click', e => {
        form.classList.remove('hidden');
        document
          .querySelector('#uvpa_available')
          .classList.add('hidden');
      });

      const button = document.querySelector('#reauth');
      button.addEventListener('click', e => {
        authenticate().then(user => {
          if (user) {
            location.href = '/home';
          } else {
            throw 'User not found.';
          }
        }).catch(e => {
          console.error(e.message || e);
          alert('Authentication failed. Use password to sign-in.');
          form.classList.remove('hidden');
          document.querySelector('#uvpa_available').classList.add('hidden');
        });        
      });
    </script>
...

7. מעולה!

סיימת את שיעור Lab הזה!

מידע נוסף

תודה רבה ל-Yuriy Ackerman מ-FIDO Alliance על העזרה שלך.