Implementacja kluczy dostępu z autouzupełnianiem formularzy w aplikacji internetowej

1. Zanim zaczniesz

Korzystanie z kluczy dostępu zamiast haseł to świetny sposób na zwiększenie bezpieczeństwa kont użytkowników w witrynach, a także na uproszczenie i ułatwienie ich obsługi. Umożliwiają logowanie się w witrynie lub aplikacji za pomocą funkcji blokady ekranu urządzenia, np. odcisku palca, skanu twarzy lub kodu PIN. Zanim użytkownik będzie mógł zalogować się za pomocą klucza dostępu, musi go utworzyć, powiązać z kontem użytkownika i zapisać jego klucz publiczny na serwerze.

W tym ćwiczeniu przekształcisz podstawowe logowanie za pomocą formularza z nazwą użytkownika i hasłem w logowanie obsługujące klucze dostępu, które obejmuje:

  • Przycisk, który tworzy klucz dostępu po zalogowaniu się użytkownika.
  • Interfejs z listą zarejestrowanych kluczy dostępu.
  • Istniejący formularz logowania, który umożliwia użytkownikom logowanie się za pomocą zarejestrowanego klucza dostępu dzięki autouzupełnianiu formularza.

Wymagania wstępne

Czego się nauczysz

  • Jak utworzyć klucz dostępu
  • Jak uwierzytelniać użytkowników za pomocą klucza dostępu.
  • Jak umożliwić formularzowi sugerowanie klucza dostępu jako opcji logowania.

2. Konfiguracja

W tym ćwiczeniu sklonujesz z GitHuba niekompletną aplikację w wersji demonstracyjnej, a następnie dokończysz implementację obsługi kluczy dostępu.

Klonowanie projektu

  1. Otwórz projekt na GitHubie.
  2. Sklonuj lub pobierz projekt.

ac587c53b746785a.png

Uruchamianie projektu

  1. Otwórz terminal i użyj polecenia cd start, aby zmienić katalog.
  2. Aby zainstalować zależności projektu, uruchom npm install.
  3. Skompiluj i uruchom projekt za pomocą polecenia npm run build && IS_LOCAL=1 npm run start.
  4. Otwórz w przeglądarce adres http://localhost:8080/.

Sprawdź stan początkowy witryny

  1. Na stronie wpisz losową nazwę użytkownika i kliknij Dalej.
  2. Wpisz losowe hasło, a potem kliknij Zaloguj się. Hasło jest ignorowane, ale nadal jesteś uwierzytelniony(-a) i przekierowywany(-a) na stronę główną.
  3. Jeśli chcesz zmienić wyświetlaną nazwę, zrób to. To wszystko, co możesz zrobić w stanie początkowym.
  4. Kliknij Wyloguj się.

W tym stanie użytkownicy muszą wpisywać hasło przy każdym logowaniu. Dodaj do tego formularza obsługę kluczy dostępu, aby użytkownicy mogli logować się za pomocą funkcji blokady ekranu urządzenia.

Więcej informacji o działaniu kluczy dostępu znajdziesz w artykule Jak działają klucze dostępu?.

3. Dodawanie możliwości utworzenia klucza dostępu

Aby umożliwić użytkownikom uwierzytelnianie za pomocą klucza dostępu, musisz dać im możliwość utworzenia i zarejestrowania klucza dostępu oraz przechowywania jego klucza publicznego na serwerze.

9b84dbaec66afe9c.png

Chcesz zezwolić na tworzenie klucza dostępu po zalogowaniu się użytkownika za pomocą hasła i dodać interfejs, który umożliwi użytkownikom tworzenie klucza dostępu i wyświetlanie listy wszystkich zarejestrowanych kluczy dostępu na stronie /home. W następnej sekcji utworzysz funkcję, która tworzy i rejestruje klucz dostępu.

Tworzenie funkcji registerCredential()

  1. W wybranym edytorze kodu otwórz katalog start.
  2. Otwórz plik public/client.js i przewiń go na koniec.
  3. Po odpowiednim komentarzu dodaj tę funkcję 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.

};

Ta funkcja tworzy i rejestruje klucz dostępu na serwerze.

Pobieranie wyzwania i innych opcji z punktu końcowego serwera

Zanim utworzysz klucz dostępu, musisz poprosić serwer o przekazanie parametrów do WebAuthn, w tym wyzwania. WebAuthn to interfejs API przeglądarki, który umożliwia użytkownikowi utworzenie klucza dostępu i uwierzytelnienie go za pomocą tego klucza. Na szczęście w tym laboratorium masz już punkt końcowy serwera, który odpowiada takimi parametrami.

  • Aby uzyskać wyzwanie i inne opcje z punktu końcowego serwera, dodaj ten kod do treści funkcji registerCredential() po odpowiednim komentarzu:

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');

Poniższy fragment kodu zawiera przykładowe opcje, które otrzymujesz z serwera:

{
  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,
  }
}

Protokół między serwerem a klientem nie jest częścią specyfikacji WebAuthn. Serwer w tym laboratorium kodu został jednak zaprojektowany tak, aby zwracać plik JSON jak najbardziej podobny do słownika PublicKeyCredentialCreationOptions, który jest przekazywany do interfejsu WebAuthn API navigator.credentials.create().

Poniższa tabela nie jest wyczerpująca, ale zawiera ważne parametry w słowniku PublicKeyCredentialCreationOptions:

Parametry

Teksty reklam

challenge

Wygenerowane przez serwer wyzwanie w obiekcie ArrayBuffer dla tej rejestracji. Jest to wymagane, ale nieużywane podczas rejestracji, chyba że wykonujesz atest – zaawansowane zagadnienie, które nie jest omawiane w tym ćwiczeniu.

user.id

Unikalny identyfikator użytkownika. Ta wartość musi być obiektem ArrayBuffer, który nie zawiera informacji umożliwiających identyfikację osoby, takich jak adresy e-mail czy nazwy użytkowników. Dobrze sprawdzi się losowa 16-bajtowa wartość wygenerowana dla każdego konta.

user.name

To pole powinno zawierać unikalny identyfikator konta rozpoznawalny przez użytkownika, np. jego adres e-mail lub nazwę użytkownika. Jest on widoczny w selektorze kont. (Jeśli używasz nazwy użytkownika, użyj tej samej wartości co w przypadku uwierzytelniania za pomocą hasła).

user.displayName

To pole zawiera opcjonalną, przyjazną dla użytkownika nazwę konta. Nie musi być niepowtarzalna i może być wybraną przez użytkownika nazwą. Jeśli w witrynie nie ma odpowiedniej wartości, którą można tu umieścić, przekaż pusty ciąg znaków. W zależności od przeglądarki może się ona wyświetlać w selektorze kont.

rp.id

Identyfikator strony ufającej (RP) to domena. Witryna może określić swoją domenę lub sufiks, który można zarejestrować. Jeśli na przykład źródło RP to https://login.example.com:1337, identyfikator RP może mieć postać login.example.com lub example.com. Jeśli identyfikator RP to example.com, użytkownik może się uwierzytelnić w domenie login.example.com lub w dowolnej innej subdomenie domeny example.com.

pubKeyCredParams

To pole określa obsługiwane przez RP algorytmy klucza publicznego. Zalecamy ustawienie tej wartości na [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. Określa to obsługę ECDSA z P-256 i RSA PKCS#1, co zapewnia pełny zasięg.

excludeCredentials

Zawiera listę zarejestrowanych identyfikatorów danych logowania, aby zapobiec ponownej rejestracji tego samego urządzenia. Jeśli jest podany, element transports powinien zawierać wynik wywołania funkcji getTransports() podczas rejestracji każdego loginu. Więcej informacji znajdziesz w naszej dokumentacji na temat zapobiegania tworzeniu nowego klucza dostępu, jeśli już istnieje.

authenticatorSelection.authenticatorAttachment

Ustaw wartość "platform". Oznacza to, że chcesz używać uwierzytelniania wbudowanego w urządzenie platformy, aby użytkownik nie musiał wkładać np. klucza bezpieczeństwa USB.

authenticatorSelection.requireResidentKey

Ustaw wartość logiczną true. Klucz możliwy do wykrycia (klucz rezydentny) może być używany bez konieczności podawania przez serwer identyfikatora danych logowania, dzięki czemu jest zgodny z autouzupełnianiem. Więcej informacji znajdziesz w szczegółowym omówieniu certyfikatów z możliwością wyszukiwania.

authenticatorSelection.userVerification

Ustaw wartość "preferred" lub pomiń ją, ponieważ jest to wartość domyślna. Określa, czy weryfikacja użytkownika, która korzysta z blokady ekranu urządzenia, jest "required", "preferred" czy "discouraged". Ustawienie wartości "preferred" powoduje, że gdy urządzenie ma taką możliwość, wymagana jest weryfikacja użytkownika. Więcej informacji znajdziesz w naszym szczegółowym omówieniu weryfikacji użytkownika.

Tworzenie danych logowania

  1. W treści funkcji registerCredential() po odpowiednim komentarzu przekonwertuj niektóre parametry zakodowane w formacie Base64URL z powrotem na postać binarną, a mianowicie ciągi tekstowe user.idchallenge oraz wystąpienia ciągu tekstowego id zawarte w tablicy excludeCredentials. Możesz to zrobić za pomocą funkcji 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. W następnym wierszu ustaw authenticatorSelection.authenticatorAttachment na "platform", a authenticatorSelection.requireResidentKey na true. Umożliwia to korzystanie tylko z uwierzytelniacza platformy (samego urządzenia) z możliwością wykrywalnych danych logowania.

public/client.js

// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
  authenticatorAttachment: 'platform',
  requireResidentKey: true
}
  1. W następnym wierszu wywołaj metodę navigator.credentials.create(), aby utworzyć dane logowania.

public/client.js

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

W ramach tego wywołania przeglądarka próbuje zweryfikować tożsamość użytkownika za pomocą blokady ekranu urządzenia.

Zarejestruj dane logowania w punkcie końcowym serwera.

Po zweryfikowaniu tożsamości użytkownika tworzony i zapisywany jest klucz dostępu. Witryna otrzymuje obiekt danych logowania, który zawiera klucz publiczny, który możesz wysłać na serwer, aby zarejestrować klucz dostępu.

Ten fragment kodu zawiera przykładowy obiekt danych logowania:

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

Poniższa tabela nie jest wyczerpująca, ale zawiera ważne parametry w obiekcie PublicKeyCredential:

Parametry

Teksty reklam

id

Identyfikator utworzonego klucza dostępu zakodowany w formacie Base64URL. Ten identyfikator pomaga przeglądarce określić, czy podczas uwierzytelniania na urządzeniu znajduje się pasujący klucz dostępu. Ta wartość musi być przechowywana w bazie danych na backendzie.

rawId

ArrayBufferWersja obiektu identyfikatora danych logowania.

response.clientDataJSON

Obiekt ArrayBuffer zawierający zakodowane dane klienta.

response.attestationObject

Zakodowany obiekt atestu ArrayBuffer. Zawiera ważne informacje, takie jak identyfikator RP, flagi i klucz publiczny.

response.transports

Lista transportów obsługiwanych przez urządzenie: "internal" oznacza, że urządzenie obsługuje klucz dostępu. "hybrid" oznacza, że obsługuje też uwierzytelnianie na innym urządzeniu.

authenticatorAttachment

Zwraca wartość "platform", gdy te dane logowania są tworzone na urządzeniu obsługującym klucze dostępu.

Aby wysłać obiekt danych logowania na serwer, wykonaj te czynności:

  1. Zakoduj parametry binarne danych logowania w formacie Base64URL, aby można je było przekazać na serwer jako ciąg znaków. Aby to zrobić, możesz użyć .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. W następnej linii wyślij obiekt na serwer:

public/client.js

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

Gdy uruchomisz program, serwer zwróci wartość HTTP code 200, co oznacza, że dane logowania są zarejestrowane.

Masz już kompletną funkcję registerCredential().

Sprawdź kod rozwiązania w tej sekcji

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. Tworzenie interfejsu do rejestrowania danych logowania za pomocą klucza dostępu i zarządzania nimi

Teraz, gdy funkcja registerCredential() jest dostępna, potrzebujesz przycisku, aby ją wywołać. Musisz też wyświetlać listę zarejestrowanych kluczy dostępu.

bfa4e7cdda47669e.png

Dodawanie kodu HTML obiektu zastępczego

  1. W edytorze otwórz plik views/home.html.
  2. Po odpowiednim komentarzu dodaj element interfejsu, który wyświetla przycisk rejestracji klucza dostępu i listę kluczy dostępu:

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>

Element div#list jest elementem zastępczym listy.

Sprawdzanie obsługi kluczy dostępu

Aby wyświetlać opcję tworzenia klucza dostępu tylko użytkownikom, których urządzenia obsługują klucze dostępu, musisz najpierw sprawdzić, czy WebAuthn jest dostępny. Jeśli tak, musisz usunąć klasę hidden, aby wyświetlić przycisk Utwórz klucz dostępu.

Aby sprawdzić, czy środowisko obsługuje klucze dostępu, wykonaj te czynności:

  1. Na końcu pliku views/home.html, po odpowiednim komentarzu, napisz warunek, który zostanie wykonany, jeśli zmienne window.PublicKeyCredential, PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable i PublicKeyCredential.isConditionalMediationAvailable mają wartość 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. W treści warunku sprawdź, czy urządzenie może utworzyć klucz dostępu, a następnie sprawdź, czy klucz dostępu można zasugerować w ramach automatycznego wypełniania formularza.

views/home.html

try {
    const capabilities = await PublicKeyCredential.getClientCapabilities();
    // Is conditional UI available in this browser?
    if (capabilities.conditionalGet === true &&
        capabilities.passkeyPlatformAuthenticator === true) {
  1. Jeśli wszystkie warunki są spełnione, wyświetl przycisk tworzenia klucza dostępu. W przeciwnym razie wyświetl komunikat ostrzegawczy.

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.';
}

Wyświetlanie zarejestrowanych kluczy dostępu na liście

  1. Zdefiniuj funkcję renderCredentials(), która pobiera zarejestrowane klucze dostępu z serwera i wyświetla je na liście. Na szczęście masz już /auth/getKeys punkt końcowy serwera, który umożliwia pobieranie zarejestrowanych kluczy dostępu zalogowanego użytkownika.

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. W następnym wierszu wywołaj funkcję renderCredentials(), aby wyświetlić zarejestrowane klucze dostępu, gdy tylko użytkownik wejdzie na stronę /home w ramach inicjowania.

views/home.html

renderCredentials();

Tworzenie i rejestrowanie klucza dostępu

Aby utworzyć i zarejestrować klucz dostępu, musisz wywołać zaimplementowaną wcześniej funkcję registerCredential().

Aby wywołać funkcję registerCredential() po kliknięciu przycisku Utwórz klucz dostępu, wykonaj te czynności:

  1. W pliku po zastępczym kodzie HTML znajdź następującą instrukcję import:

views/home.html

import { 
  $, 
  _fetch, 
  loading, 
  updateCredential, 
  unregisterCredential, 
} from '/client.js';
  1. Na końcu treści instrukcji import dodaj funkcję 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. Na końcu pliku po odpowiednim komentarzu zdefiniuj funkcję register(), która wywołuje funkcję registerCredential() i interfejs ładowania oraz wywołuje funkcję renderCredentials() po rejestracji. Wyjaśnia to, że przeglądarka tworzy klucz dostępu i wyświetla komunikat o błędzie, gdy coś pójdzie nie tak.

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. W treści funkcji register() przechwyć wyjątki. Metoda navigator.credentials.create() zgłasza błąd InvalidStateError, gdy na urządzeniu istnieje już klucz dostępu. Jest to badane za pomocą tablicy excludeCredentials. W takim przypadku wyświetlasz użytkownikowi odpowiedni komunikat. W przypadku anulowania okna uwierzytelniania przez użytkownika zwraca też błąd NotAllowedError. W takim przypadku zignoruj go.

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. W wierszu po funkcji register() dołącz funkcję register() do zdarzenia click dla przycisku Utwórz klucz dostępu.

views/home.html

createPasskey.addEventListener('click', register);

Sprawdź kod rozwiązania w tej sekcji

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);

Wypróbuj

Jeśli wykonasz wszystkie opisane dotychczas czynności, wdrożysz w witrynie możliwość tworzenia, rejestrowania i wyświetlania kluczy dostępu.

Aby wypróbować tę funkcję, wykonaj te czynności:

  1. Zaloguj się w witrynie, używając losowej nazwy użytkownika i hasła.
  2. Kliknij Utwórz klucz dostępu.
  3. Potwierdź swoją tożsamość za pomocą blokady ekranu urządzenia.
  4. Sprawdź, czy klucz dostępu jest zarejestrowany i wyświetla się w sekcji Twoje zarejestrowane klucze dostępu na stronie internetowej.

Zarejestrowane klucze dostępu wymienione na stronie /home.

Zmienianie nazwy i usuwanie zarejestrowanych kluczy dostępu

Powinna być możliwość zmiany nazwy zarejestrowanych kluczy dostępu lub ich usunięcia z listy. Możesz sprawdzić, jak to działa w kodzie, ponieważ jest on dostępny w ramach codelabu.

W Chrome możesz usunąć zarejestrowane klucze dostępu ze strony chrome://settings/passkeys na komputerze lub z menedżera haseł w ustawieniach na urządzeniu z Androidem.

Informacje o tym, jak zmieniać nazwy zarejestrowanych kluczy dostępu i usuwać je na innych platformach, znajdziesz na odpowiednich stronach pomocy tych platform.

5. Dodanie możliwości uwierzytelniania za pomocą klucza dostępu

Użytkownicy mogą teraz tworzyć i rejestrować klucze dostępu, a potem bezpiecznie używać ich do uwierzytelniania w Twojej witrynie. Teraz musisz dodać do swojej witryny możliwość uwierzytelniania za pomocą klucza dostępu.

Tworzenie funkcji authenticate()

  • W pliku public/client.js po odpowiednim komentarzu utwórz funkcję o nazwie authenticate(), która lokalnie weryfikuje użytkownika, a następnie na serwerze:

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.

};

Pobieranie wyzwania i innych opcji z punktu końcowego serwera

Zanim poprosisz użytkownika o uwierzytelnienie, musisz poprosić serwer o przekazanie parametrów do WebAuthn, w tym wyzwania.

  • W treści funkcji authenticate() po odpowiednim komentarzu wywołaj funkcję _fetch(), aby wysłać żądanie POST do serwera:

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);

Serwer w tym laboratorium kodu został zaprojektowany tak, aby zwracać JSON, który jest jak najbardziej podobny do słownika PublicKeyCredentialRequestOptions przekazywanego do interfejsu WebAuthn navigator.credentials.get() API. Poniższy fragment kodu zawiera przykładowe opcje, które powinny zostać zwrócone:

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

Poniższa tabela nie jest wyczerpująca, ale zawiera ważne parametry w słowniku PublicKeyCredentialRequestOptions:

Parametry

Teksty reklam

challenge

Wygenerowane przez serwer wyzwanie w obiekcie ArrayBuffer. Jest to wymagane, aby zapobiec atakom typu replay. Nigdy nie akceptuj tego samego wyzwania w odpowiedzi dwukrotnie.

rpId

Identyfikator RP to domena. Witryna może określić swoją domenę lub sufiks, który można zarejestrować. Ta wartość musi być zgodna z parametrem rp.id używanym podczas tworzenia klucza dostępu.

allowCredentials

Ta właściwość służy do znajdowania uwierzytelniaczy kwalifikujących się do tego uwierzytelniania. Przekaż pustą tablicę lub pozostaw ją nieokreśloną, aby przeglądarka wyświetliła selektor kont. Dowiedz się więcej o tym, jak zachowują się allowCredentials.

userVerification

Ustaw wartość "preferred" lub pomiń ją, ponieważ jest to wartość domyślna. Określa, czy weryfikacja użytkownika za pomocą blokady ekranu urządzenia jest "required", "preferred" czy "discouraged". Ustawienie wartości "preferred" powoduje, że gdy urządzenie ma taką możliwość, wymagana jest weryfikacja użytkownika. Dowiedz się więcej o zachowaniu podczas weryfikacji użytkownika.

Lokalna weryfikacja użytkownika i uzyskanie danych logowania

  1. W treści funkcji authenticate() po odpowiednim komentarzu przekonwertuj parametr challenge z powrotem na postać binarną:

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. Aby otworzyć selektor konta, gdy użytkownik się uwierzytelni, przekaż pustą tablicę do parametru allowCredentials:

public/client.js

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

Selektor konta korzysta z informacji o użytkowniku przechowywanych w kluczu dostępu.

  1. Wywołaj metodę navigator.credentials.get() wraz z opcją mediation: 'conditional':

public/client.js

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

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

Ta opcja nakazuje przeglądarce warunkowe sugerowanie kluczy dostępu w ramach automatycznego wypełniania formularzy.

Weryfikacja danych logowania

Gdy użytkownik potwierdzi swoją tożsamość lokalnie, otrzymasz obiekt danych logowania zawierający podpis, który możesz zweryfikować na serwerze.

Ten fragment kodu zawiera przykładowy obiekt PublicKeyCredential:

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

Poniższa tabela nie jest wyczerpująca, ale zawiera ważne parametry w obiekcie PublicKeyCredential:

Parametry

Teksty reklam

id

Identyfikator uwierzytelnionych danych logowania klucza dostępu zakodowany w formacie Base64URL.

rawId

ArrayBufferWersja obiektu identyfikatora danych logowania.

response.clientDataJSON

Obiekt ArrayBuffer z danymi klienta. To pole zawiera informacje, takie jak test i pochodzenie, które serwer RP musi zweryfikować.

response.authenticatorData

Obiekt ArrayBuffer z danymi uwierzytelniającymi. To pole zawiera informacje takie jak identyfikator RP.

response.signature

Obiekt ArrayBuffer podpisu. Ta wartość jest podstawą danych logowania i musi zostać zweryfikowana na serwerze.

response.userHandle

Obiekt ArrayBuffer zawierający identyfikator użytkownika ustawiony w momencie tworzenia. Tej wartości można użyć zamiast identyfikatora danych logowania, jeśli serwer musi wybrać używane wartości identyfikatora lub jeśli backend chce uniknąć tworzenia indeksu identyfikatorów danych logowania.

authenticatorAttachment

Zwraca ciąg znaków "platform", gdy te dane logowania pochodzą z urządzenia lokalnego. W przeciwnym razie zwraca ciąg znaków "cross-platform", zwłaszcza gdy użytkownik loguje się na telefonie. Jeśli użytkownik musi zalogować się za pomocą telefonu, poproś go o utworzenie klucza dostępu na urządzeniu lokalnym.

Aby wysłać obiekt danych logowania na serwer, wykonaj te czynności:

  1. W treści funkcji authenticate() po odpowiednim komentarzu zakoduj parametry binarne danych logowania, aby można je było przesłać na serwer jako ciąg tekstowy. Aby to zrobić, możesz użyć .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. Wyślij obiekt na serwer:

public/client.js

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

Po uruchomieniu programu serwer zwraca wartość HTTP code 200, co oznacza, że dane logowania zostały zweryfikowane.

Masz teraz pełną funkcję authentication().

Sprawdź kod rozwiązania w tej sekcji

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. Dodawanie kluczy dostępu do autouzupełniania w przeglądarce

Gdy użytkownik wróci, chcesz, aby mógł zalogować się jak najłatwiej i najbezpieczniej. Jeśli dodasz do strony logowania przycisk Zaloguj się za pomocą klucza dostępu, użytkownik może go nacisnąć, wybrać klucz dostępu w selektorze kont w przeglądarce i użyć blokady ekranu, aby potwierdzić tożsamość.

Przejście z hasła na klucz dostępu nie następuje jednak u wszystkich użytkowników jednocześnie. Oznacza to, że nie możesz pozbyć się haseł, dopóki wszyscy użytkownicy nie przejdą na klucze dostępu, więc do tego czasu musisz pozostawić formularz logowania oparty na hasłach. Jeśli jednak pozostawisz formularz hasła i przycisk klucza dostępu, użytkownicy będą musieli dokonać niepotrzebnego wyboru, którego z nich użyć do zalogowania się. Najlepiej, aby proces logowania był prosty.

W takiej sytuacji przydaje się interfejs warunkowy. Warunkowy interfejs to funkcja WebAuthn, która umożliwia wyświetlanie pola do wprowadzania danych formularza, aby sugerować klucz dostępu jako część elementów autouzupełniania oprócz haseł. Jeśli użytkownik kliknie klucz dostępu w sugestiach autouzupełniania, pojawi się prośba o użycie blokady ekranu urządzenia w celu lokalnego potwierdzenia tożsamości. Zapewnia to płynne logowanie, ponieważ działanie użytkownika jest niemal identyczne jak w przypadku logowania za pomocą hasła.

d616744939063451.png

Włączanie interfejsu warunkowego

Aby włączyć interfejs warunkowy, wystarczy dodać token webauthn do atrybutu autocomplete pola do wprowadzania danych. Po ustawieniu tokena możesz wywołać metodę navigator.credentials.get() za pomocą ciągu znaków mediation: 'conditional', aby warunkowo wywołać interfejs blokady ekranu.

  • Aby włączyć interfejs warunkowy, zastąp istniejące pola wprowadzania nazwy użytkownika tym kodem HTML po odpowiednim komentarzu w pliku 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>

Wykrywanie funkcji, wywoływanie WebAuthn i włączanie interfejsu warunkowego

  1. W pliku view/index.html po odpowiednim komentarzu zastąp istniejącą instrukcję import tym kodem:

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";

Ten kod importuje funkcję authenticate(), którą zaimplementowano wcześniej.

  1. Sprawdź, czy obiekt window.PulicKeyCredential jest dostępny i czy metoda PublicKeyCredential.isConditionalMediationAvailable() zwraca wartość true, a następnie wywołaj funkcję 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);
    }
  }
}

Sprawdź kod rozwiązania w tej sekcji

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);
    }
  }
}

Wypróbuj

Wdrożono tworzenie, rejestrowanie, wyświetlanie i uwierzytelnianie kluczy dostępu w witrynie.

Aby wypróbować tę funkcję, wykonaj te czynności:

  1. Otwórz kartę podglądu.
  2. W razie potrzeby wyloguj się.
  3. Kliknij pole tekstowe nazwy użytkownika. Pojawi się okno.
  4. Wybierz konto, na które chcesz się zalogować.
  5. Potwierdź swoją tożsamość za pomocą blokady ekranu urządzenia. Nastąpi przekierowanie na stronę /home i zalogujesz się.

Okno dialogowe z prośbą o potwierdzenie tożsamości za pomocą zapisanego hasła lub klucza dostępu.

7. Gratulacje!

To ćwiczenie zostało ukończone. Jeśli masz pytania, zadaj je na liście adresowej FIDO-DEV lub na StackOverflow, dodając tag passkey.

Więcej informacji