Zabezpieczanie witryny za pomocą uwierzytelniania dwuskładnikowego za pomocą klucza bezpieczeństwa (WebAuthn)

1. Co stworzysz

Rozpoczniesz od podstawowej aplikacji internetowej, która obsługuje logowanie oparte na haśle.

Następnie dodasz obsługę uwierzytelniania dwuskładnikowego za pomocą klucza bezpieczeństwa opartego na WebAuthn. W tym celu wykonaj następujące czynności:

  • Sposób rejestrowania danych logowania przez WebAuthn.
  • Proces uwierzytelniania dwuskładnikowego, w którym użytkownik jest proszony o drugi czynnik – dane logowania WebAuthn – jeśli go zarejestrował.
  • Interfejs zarządzania danymi logowania: lista danych logowania, która umożliwia użytkownikom zmienianie i usuwanie danych logowania.

16ce77744061c5f7.png

Spójrz na gotową aplikację internetową i wypróbuj ją.

2. Informacje o WebAuthn

Podstawy WebAuthn

Dlaczego WebAuthn?

Wyłudzanie informacji to poważny problem z zabezpieczeniami w internecie: większość naruszeń bezpieczeństwa konta polega na słabszym lub skradzionym haśle w wielu witrynach. Zbiorowa reakcja na ten problem to uwierzytelnianie wielopoziomowe. Implementacje są rozproszone, a wiele z nich nadal dobrze sprawdza się podczas wyłudzania informacji.

Interfejs Web Authentication API (WebAuthn) to standardowy protokół odporny na phishing, który może być używany przez dowolną aplikację internetową.

Jak to działa

Źródło: webauthn.guide

WebAuthn umożliwia rejestrowanie i uwierzytelnianie użytkowników przy użyciu kryptografii klucza publicznego, a nie hasła. Witryny mogą tworzyć dane logowania składające się z prywatnej pary kluczy.

  • Klucz prywatny jest bezpiecznie przechowywany na urządzeniu użytkownika.
  • Klucz publiczny i losowo wygenerowany identyfikator danych logowania są wysyłane do serwera w celu przechowywania.

Klucz publiczny jest używany przez serwer do potwierdzenia tożsamości użytkownika. Nie jest ona tajemnicą, ponieważ jest bezużyteczna bez odpowiedniego klucza prywatnego.

Zalety

WebAuthn ma 2 główne korzyści:

  • Brak udostępnionego obiektu tajnego: serwer nie przechowuje żadnego tajnego obiektu. Dzięki temu bazy danych są mniej atrakcyjne dla hakerów, ponieważ klucze publiczne nie są dla nich przydatne.
  • Dane logowania z zakresu: danych logowania zarejestrowanych w site.example nie można używać evil-site.example. Dzięki temu WebAuthn jest odporny na phishing.

Przypadki użycia

Jednym z przypadków użycia WebAuthn jest uwierzytelnianie dwuskładnikowe za pomocą klucza bezpieczeństwa. Może to być szczególnie ważne w przypadku firmowych aplikacji internetowych.

Obsługa przeglądarek

Jest napisany przez W3C i FIDO przy współpracy Google, Mozilla, Microsoft, Yubico i innych.

Glosariusz

  • Authenticator: oprogramowanie lub sprzęt, które mogą rejestrować użytkownika oraz potwierdzać własność zarejestrowanych danych logowania. Istnieją dwa typy mechanizmów uwierzytelniania:
  • Uwierzytelnianie w roamingu: narzędzie uwierzytelniające dostępne na każdym urządzeniu, z którego użytkownik próbuje się zalogować. Przykład: klucz bezpieczeństwa USB, smartfon.
  • Uwierzytelnianie platformy: moduł uwierzytelniający wbudowany w urządzenie użytkownika. Przykład: Touch ID firmy Apple.
  • Dane logowania: prywatna para kluczy
  • Strona polegająca na: (serwer) witryny, która próbuje uwierzytelnić użytkownika
  • Serwer FIDO: serwer używany do uwierzytelniania. FIDO to rodzina protokołów opracowanych przez sojusz FIDO. Jednym z nich jest WebAuthn.

Podczas tych warsztatów połączymy się z uwierzytelniającym roamingiem.

3. Zanim zaczniesz

Czego potrzebujesz

Aby ukończyć ćwiczenia z programowania, musisz mieć:

  • Podstawowe informacje o WebAuthn.
  • Podstawowa wiedza o języku JavaScript i HTML.
  • Nowoczesna przeglądarka, która obsługuje WebAuthn.
  • Klucz bezpieczeństwa zgodny z U2F.

Możesz użyć jednego z tych kluczy jako klucza bezpieczeństwa:

  • Telefon z Androidem, na którym działa Android>=7 (Nougat), działający w Chrome. W takim przypadku potrzebujesz też komputera z systemem Windows, macOS lub Chrome OS z Bluetoothem.
  • Klucz USB, na przykład YubiKey.

6539dc7ffec2538c.png

Źródło: https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

Czego się nauczysz

Dowiesz się, co zrobić ❤

  • Jak zarejestrować klucz bezpieczeństwa i używać go jako drugiego składnika uwierzytelniania WebAuthn.
  • Jak sprawić, by ten proces był łatwy w obsłudze

Nie nauczysz się ❌

  • Jak utworzyć serwer FIDO, czyli serwer używany do uwierzytelniania. To normalne, ponieważ jako programista aplikacji lub witryny korzystasz zazwyczaj z dotychczasowych implementacji serwera FIDO. Pamiętaj, aby zawsze sprawdzać działanie i jakość implementacji serwera, na których Ci zależy. W ramach tych ćwiczeń serwer FIDO używa SimpleWebAuthn. Inne opcje znajdziesz na oficjalnej stronie FIDO Alliance. W przypadku bibliotek open source zobacz webauthn.io lub AwesomeWebAuthn.

Wyłączenie odpowiedzialności

Aby się zalogować, użytkownik musi podać hasło. Jednak dla uproszczenia podczas tego ćwiczenia z hasłem nie jest przechowywane ani sprawdzane hasło. W przypadku prawdziwej aplikacji należy sprawdzić, czy jest ona po stronie serwera.

W tym ćwiczeniu wykonasz podstawowe testy zabezpieczeń, takie jak kontrole CSRF, weryfikację sesji i dezynfekcję danych wejściowych. Wiele zabezpieczeń nie jest jednak takich jak na przykład, nie ma ograniczenia wpisywania danych dotyczących haseł, aby zapobiec atakom metodą brute-force. Nie ma to tu znaczenia, ponieważ hasła nie są przechowywane, ale uważaj, by nie używać tego kodu w wersji produkcyjnej.

4. Skonfiguruj aplikację

Jeśli używasz telefonu z Androidem jako aplikacji uwierzytelniającej

  • Upewnij się, że zarówno Chrome na komputerze, jak i na telefonie jest aktualny.
  • Na komputerze i na telefonie otwórz Chrome i zaloguj się za pomocą tego samego profilu, którego chcesz używać w tych warsztatach.
  • Włącz synchronizację tego profilu na komputerze i telefonie. Aby to zrobić, użyj chrome://settings/syncSetup.
  • Włącz Bluetooth zarówno na komputerze, jak i na telefonie.
  • W logowaniu na komputerze z tym samym profilem otwórz webauthn.io.
  • Wpisz prostą nazwę użytkownika. W sekcjach Typ poświadczenia i Typ uwierzytelniania pozostaw wartości Brak i Nieokreślony (domyślny). Kliknij Zarejestruj.

6b49ff0298f5a0af.png

  • Powinno otworzyć się okno przeglądarki z prośbą o potwierdzenie tożsamości. Wybierz telefon z listy.

ffebe58ac826eaf2.png 852de328fcd4eb42.png

  • Na telefonie powinno pojawić się powiadomienie Potwierdź swoją tożsamość. Kliknij ją.
  • Na telefonie pojawi się prośba o podanie kodu PIN na telefonie (lub dotknięcie czytnika linii papilarnych). Wpisz go.
  • Na stronie webauthn.io na komputerze powinien pojawić się wskaźnik "Sukces".

fc0acf00a4d412fa.png

  • Na stronie webauthn.io kliknij przycisk Zaloguj się.
  • Powinno otworzyć się okno przeglądarki – wybierz telefon z listy.
  • Na telefonie kliknij powiadomienie, które się pojawi, i wpisz kod PIN (lub kliknij czytnik linii papilarnych).
  • Strona webauthn.io powinna Cię poinformować, że jesteś zalogowany. Twój telefon działa poprawnie jako klucz bezpieczeństwa – wszystko gotowe.

Jeśli używasz klucza bezpieczeństwa USB jako mechanizmu uwierzytelniania

  • Na komputerze otwórz webauthn.io.
  • Wpisz prostą nazwę użytkownika. W sekcjach Typ poświadczenia i Typ uwierzytelniania pozostaw wartości Brak i Nieokreślony (domyślny). Kliknij Zarejestruj.
  • Powinno otworzyć się okno przeglądarki z prośbą o potwierdzenie tożsamości. Na liście wybierz Klucz bezpieczeństwa USB.

ffebe58ac826eaf2.png 9fe75f04e43da035.png

  • Włóż klucz bezpieczeństwa do pulpitu i dotknij go.

923d5adb8aa8286c.png

  • Na stronie webauthn.io na komputerze powinien pojawić się wskaźnik "Sukces".

fc0acf00a4d412fa.png

  • Na stronie webauthn.io kliknij przycisk Login (Zaloguj się).
  • Powinno otworzyć się okno przeglądarki – wybierz z listy Klucz bezpieczeństwa USB.
  • Dotknij klawisza.
  • Strona Webauthn.io powinna Cię poinformować, że jesteś zalogowany. Klucz bezpieczeństwa USB działa prawidłowo. Wszystkie warsztaty są gotowe.

7e1c0bb19c9f3043.png

5. Konfiguracja

W tym ćwiczeniu użyjesz Glitch – edytora kodu online, który automatycznie i błyskawicznie wdroży Twój kod.

Rozwiń kod startowy

Otwórz projekt początkowy.

Kliknij przycisk Remiks.

Zostanie utworzona kopia kodu startowego. Masz teraz własny kod do edycji. W tym ćwiczeniu wykonasz wszystkie ćwiczenia z użyciem rozwidlenia (tzw. remiks&&t; w Glitch).

cf2b9f552c9809b6.png

Poznaj kod startowy

Odkryj kod startowy, który masz trochę rozwinięty.

Zwróć uwagę, że poniżej libs znajduje się biblioteka o nazwie auth.js. Jest to biblioteka niestandardowa, która obsługuje logikę uwierzytelniania po stronie serwera. Wykorzystuje bibliotekę fido jako zależność.

6. Wdrażanie rejestracji danych logowania

Wdrażanie rejestracji danych logowania

Aby skonfigurować uwierzytelnianie dwuskładnikowe za pomocą klucza bezpieczeństwa, musisz najpierw umożliwić użytkownikowi utworzenie danych logowania.

Najpierw dodajmy funkcję, która to robi w kodzie po stronie klienta.

W funkcji public/auth.client.js jest funkcja registerCredential(), która jeszcze niczego nie robi. Dodaj do niego ten kod:

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

Pamiętaj, że ta funkcja jest już wyeksportowana za Ciebie.

Co robi registerCredential:

  • Pobiera opcje tworzenia danych logowania z serwera (/auth/credential-options)
  • Ponieważ opcje serwera są zakodowane, do ich dekodowania używana jest funkcja użytkowa decodeServerOptions.
  • Tworzy on dane logowania, wywołując interfejs API usługi internetowej navigator.credential.create. Gdy wywołana jest metoda navigator.credential.create, przeglądarka przejmuje kontrolę i wyświetla użytkownikowi prośbę o wybranie klucza bezpieczeństwa.
  • Dekoduje nowo utworzone dane logowania
  • Rejestruje nowe dane logowania po stronie serwera, wysyłając żądanie do /auth/credential zawierające zakodowane dane logowania.

Na marginesie: spójrz na kod serwera

registerCredential() przesyła wywołania do serwera, dlatego przyjrzyjmy się temu, co dzieje się w backendzie.

Opcje tworzenia danych logowania

Gdy klient wysyła żądanie do (/auth/credential-options), serwer generuje obiekt opcji i wysyła go z powrotem do klienta.

Następnie używa tego obiektu w rzeczywistym wywołaniu danych logowania:

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

A co w tym celu (credentialCreationOptions) udało się zastosować w registerCredential po stronie klienta w poprzednim kroku?

Spójrz na kod serwera w sekcji router.post("/credential-options", ....

Nie pokazujemy każdej usługi, ale oto kilka ciekawych obiektów, które możesz zobaczyć w kodzie opcji serwera wygenerowanym przy użyciu biblioteki fido2 i powrót do klienta:

  • rpName i rpId opisują organizację, która rejestruje i uwierzytelnia użytkownika. Pamiętaj, że w WebAuthn dane logowania są ograniczone do określonej domeny, co jest związane z bezpieczeństwem. rpName i rpId są tutaj używane do określania danych logowania. Prawidłowy rpId to na przykład nazwa hosta Twojej witryny. Zwróć uwagę, jak będą się one automatycznie aktualizować, gdy będzie trzeba rozpędzić projekt początkowy 🧘🏻 ♀️
  • excludeCredentials to lista danych uwierzytelniających. Nowe dane logowania nie mogą być tworzone w narzędziu do uwierzytelniania, który zawiera również jeden z danych logowania wymienionych w dokumencie excludeCredentials. W ćwiczeniach z programowania excludeCredentials znajdziesz listę istniejących danych logowania tego użytkownika. Dzięki temu i user.id możemy mieć pewność, że wszystkie dane logowania, które utworzy użytkownik, będą działać na innym urządzeniu uwierzytelniającym (kluczu bezpieczeństwa). Warto to zrobić, ponieważ jeśli użytkownik zarejestruje wiele danych uwierzytelniających, będą korzystać z różnych mechanizmów uwierzytelniania, co oznacza, że utrata jednego klucza bezpieczeństwa nie zablokuje użytkownikowi dostępu do konta.
  • authenticatorSelection określa typ uwierzytelniania, na który chcesz zezwolić w swojej aplikacji internetowej. Przyjrzyjmy się bliżej urządzeniu authenticatorSelection:
    • residentKey: preferred oznacza, że ta aplikacja nie wymaga uwierzytelniania danych logowania po stronie klienta. Dane uwierzytelniające wykrywane po stronie klienta to specjalny typ danych uwierzytelniających, które umożliwiają uwierzytelnianie użytkownika bez konieczności ich wstępnego identyfikowania. Tutaj konfigurujemy preferred, bo to ćwiczenie ćwiczeń skupia się na podstawowej implementacji. Wykrywalne dane uwierzytelniające są przeznaczone do bardziej zaawansowanych przepływów.
    • Atrybut requireResidentKey jest dostępny tylko w przypadku zgodności wstecznej z WebAuthn w wersji 1.
    • userVerification: preferred oznacza, że jeśli mechanizm uwierzytelniania obsługuje weryfikację użytkownika – na przykład biometryczny klucz bezpieczeństwa lub klucz z wbudowaną funkcją kodu PIN – strona uzależniona poprosi o to podczas tworzenia danych logowania. Jeśli mechanizm uwierzytelniający nie jest zgodny z podstawowym kluczem bezpieczeństwa, serwer nie zażąda weryfikacji użytkownika.
  • ​​pubKeyCredParam opisuje, według potrzeb, właściwości kryptograficzne danych logowania.

Wszystkie te opcje to decyzje, które musi podjąć aplikacja internetowa dla swojego modelu zabezpieczeń. Zwróć uwagę, że na serwerze opcje są zdefiniowane w pojedynczym obiekcie authSettings.

Wyzwanie

Jeszcze ciekawszy fragment to req.session.challenge = options.challenge;.

WebAuthn jest protokołem kryptograficznym, więc aby uniknąć ponownego odtwarzania ataków, należy użyć losowych testów zabezpieczających – gdy atakujący przechwyci ładunek, by powtórzyć uwierzytelnianie, gdy nie jest właścicielem klucza prywatnego, który umożliwiłby uwierzytelnianie.

Aby temu zapobiec, na serwerze generowany jest test i będzie on podpisany na bieżąco. Następnie podpis zostanie porównany z oczekiwaniami. Sprawdza to, czy użytkownik zachowuje klucz prywatny w momencie generowania danych logowania.

Kod rejestracji danych logowania

Spójrz na kod serwera w sekcji router.post("/credential", ....

To tutaj dane logowania są rejestrowane po stronie serwera.

I co się dzieje?

Jednym z najważniejszych punktów w kodzie jest weryfikacja za pomocą fido2.verifyAttestationResponse:

  • Podpisane testy zabezpieczające są sprawdzane pod kątem tego, że dane logowania zostały utworzone przez osobę, która w danym momencie zachowała klucz prywatny.
  • Zweryfikowany jest również identyfikator strony zależnej. Dzięki temu dane logowania są powiązane z tą aplikacją internetową (i tylko ta aplikacja internetowa).

Dodaj tę funkcję do interfejsu użytkownika

Skoro Twoja funkcja tworzenia danych uwierzytelniających jest gotowa, funkcja ``registerCredential(),jest gotowa, a my udostępnimy ją użytkownikowi.

Możesz to zrobić na stronie Konto, ponieważ jest to standardowa lokalizacja do zarządzania uwierzytelnianiem.

W znacznikach account.html pod nazwą użytkownika jest pusty fragment div z klasy układu class="flex-h-between". Ten element div będzie używany w przypadku elementów interfejsu związanych z funkcją 2FA.

Dodaj jednostkę organizacyjną div:

  • Tytuł o treści „Uwierzytelnianie dwuskładnikowe”.
  • Przycisk pozwalający utworzyć dane logowania
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

Poniżej tego elementu div dodaj element div uwierzytelniający, który będzie potrzebny później:

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

W skryptach account.html zaimportuj utworzoną funkcję i dodaj funkcję register, która ją wywołuje, oraz moduł obsługi zdarzeń załączony do utworzonego przed chwilą przycisku.

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

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

Wyświetl dane logowania użytkownika, które ma zobaczyć

Teraz, gdy masz już utworzoną funkcję tworzenia danych logowania, użytkownicy muszą zobaczyć dane logowania, które chcą dodać.

Dobrym miejscem jest strona Konto.

W funkcji account.html znajdź funkcję o nazwie updateCredentialList().

Dodaj do niego ten kod, który wywołuje backend, aby pobrać wszystkie zarejestrowane dane logowania zalogowanego obecnie użytkownika i wyświetla zwrócone dane logowania:

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

Na razie nie warto zastanawiać się nad tym, removeEl i renameEl. O tym dowiesz się później z ćwiczenia z programowania.

Dodaj jedno wywołanie do updateCredentialList na początku skryptu wbudowanego w account.html. W przypadku tego wywołania dostępne dane uwierzytelniające są pobierane, gdy użytkownik wyświetla stronę swojego konta.

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

Wywołaj updateCredentialList, gdy operacja registerCredential zostanie zakończona, aby na listach były widoczne nowo utworzone dane logowania:

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

Wypróbuj tę funkcję. 👩🏻 💻

Rejestracja danych logowania została ukończona Użytkownicy mogą teraz tworzyć dane logowania oparte na kluczach bezpieczeństwa i wizualizować je na stronie Konto.

Wypróbuj te zapytania:

  • Wyloguj się.
  • Zaloguj się za pomocą dowolnego użytkownika i hasła. Jak już wspomnieliśmy, hasło nie jest w rzeczywistości sprawdzane pod kątem poprawności, więc dla uproszczenia w tym ćwiczeniu z programowania uprościmy. Wpisz dowolne hasło.
  • Na stronie Konto kliknij Dodaj dane logowania.
  • Powinna pojawić się prośba o włączenie klucza bezpieczeństwa. Zrób to.
  • Dane logowania powinny się pojawić na stronie konta po ich utworzeniu.
  • Załaduj ponownie stronę Konto. Powinny się wyświetlić dane logowania.
  • Jeśli masz 2 klucze, spróbuj dodać 2 klucze bezpieczeństwa jako dane logowania. Obie usługi powinny być wyświetlane.
  • Spróbuj utworzyć 2 dane logowania z tym samym mechanizmem uwierzytelniającym. Jest to zamierzone, ponieważ korzystamy w excludeCredentials z backendu.

7. Włącz uwierzytelnianie dwuskładnikowe

Użytkownicy mogą rejestrować i wyrejestrowywać dane logowania, ale dane logowania są tylko wyświetlane i nie są jeszcze używane.

Teraz należy ich użyć i skonfigurować uwierzytelnianie dwuskładnikowe.

W tej sekcji zmienisz przepływ uwierzytelniania w aplikacji internetowej z podstawowego schematu:

6ff49a7e520836d0.png

Aby włączyć ten proces:

e7409946cd88efc7.png

Wdrażanie uwierzytelniania dwuskładnikowego

Najpierw dodajmy odpowiednią funkcję i wdrożymy komunikację z backendem. W następnym kroku dodamy ją w frontendzie.

Musisz tu zaimplementować funkcję, która uwierzytelnia użytkownika za pomocą danych logowania.

W public/auth.client.js znajdź pustą funkcję authenticateTwoFactor i dodaj do niej ten kod:

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

Ta funkcja została już wyeksportowana. Będzie Ci potrzebna w następnym kroku.

Co robi authenticateTwoFactor:

  • Powoduje wysłanie do serwera opcji uwierzytelniania dwuskładnikowego. Podobnie jak w przypadku opcji tworzenia danych logowania, które były wcześniej znane, są one definiowane na serwerze i zależą od modelu zabezpieczeń aplikacji internetowej. Więcej informacji znajdziesz w kodzie serwera pod adresem router.post("/two-factors-options", ....
  • Gdy wywołasz navigator.credentials.get, przeglądarka przejmie przeglądarkę i poprosi użytkownika o włączenie i dotknięcie wcześniej zarejestrowanego klucza. Spowoduje to wybranie danych logowania tej konkretnej operacji uwierzytelniania dwuskładnikowego.
  • Wybrane dane logowania są wtedy przekazywane w żądaniu backendu do pobrania("/auth/authenticate-two-factor"`. Jeśli dane logowania są prawidłowe dla tego użytkownika, jest on uwierzytelniany.

Na marginesie: spójrz na kod serwera

Pamiętaj, że server.js już teraz obsługuje nawigację i dostęp: gwarantuje, że dostęp do strony Konto będą mogli uzyskać tylko uwierzytelnieni użytkownicy i wykona kilka niezbędnych przekierowań.

Teraz spójrz na kod serwera w sekcji router.post("/initialize-authentication", ....

Pamiętaj o 2 interesach:

  • Zarówno hasło, jak i dane logowania są sprawdzane jednocześnie na tym etapie. Jest to środek bezpieczeństwa: użytkownicy korzystający z uwierzytelniania dwuskładnikowego nie chcą, aby przepływy interfejsu wyglądały inaczej w zależności od tego, czy hasło było poprawne. Na tym etapie sprawdzamy jednocześnie hasło i dane logowania.
  • Jeśli hasło i dane logowania są prawidłowe, sfinalizujemy uwierzytelnianie, wywołując completeAuthentication(req, res);. Oznacza to, że w ramach sesji przełączamy się z tymczasowej sesji auth, w której użytkownik nie jest jeszcze uwierzytelniony, na sesję główną main, w której użytkownik jest uwierzytelniony.

Uwzględnij stronę uwierzytelniania dwuskładnikowego w procesie użytkownika

W folderze views zwróć uwagę na nową stronę second-factor.html.

Na liście jest przycisk Użyj klucza bezpieczeństwa, ale na razie nie ma on żadnych funkcji.

Ten przycisk powoduje wywołanie przycisku authenticateTwoFactor().

  • W przypadku powodzenia authenticateTwoFactor() przekieruj użytkownika do strony Konto.
  • Jeśli nie powiedzie się, powiadom użytkownika, że wystąpił błąd. W prawdziwej aplikacji możesz zaimplementować bardziej przydatne komunikaty o błędach – dla ułatwienia w tej demonstracji użyjemy tylko alertu o oknach.
    <main>
...
    </main>
    <script type="module">
      import { authenticateTwoFactor, authStatuses } from "/auth.client.js";

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

Użyj uwierzytelniania dwuskładnikowego

Teraz możesz dodać krok uwierzytelniania dwuskładnikowego.

Teraz musisz dodać ten krok z domeny index.html dla użytkowników, którzy skonfigurowały uwierzytelnianie dwuskładnikowe.

322a5c49d865a0d8.png

W sekcji index.html poniżej kodu location.href = "/account"; dodaj kod, który warunkowo przekierowuje użytkownika na stronę uwierzytelniania dwuskładnikowego (jeśli skonfigurowała 2FA).

W tym ćwiczeniu z programowania utworzenie danych logowania automatycznie powoduje włączenie uwierzytelniania dwuskładnikowego.

Pamiętaj, że server.js umożliwia też przeprowadzenie kontroli sesji po stronie serwera, dzięki czemu dostęp do account.html mają tylko uwierzytelnieni użytkownicy.

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

Wypróbuj tę funkcję. 👩🏻 💻

  • Zaloguj się jako nowy użytkownik jan.
  • Wyloguj się.
  • Zaloguj się na swoje konto jako jankowalski. Pamiętaj, że wymagane jest tylko hasło.
  • Utwórz dane logowania. W ten sposób będziesz mieć pewność, że uwierzytelnianie dwuskładnikowe jest włączone jako jankowalski.
  • Wyloguj się.
  • Wpisz swoją nazwę użytkownika jan i hasło.
  • Zobacz, jak automatycznie przechodzisz na stronę uwierzytelniania dwuskładnikowego.
  • (Otwórz stronę Konto pod adresem /account. Zwróć uwagę, w jaki sposób przekierowujesz na stronę indeksu, ponieważ nie jesteś w pełni uwierzytelniony: brakuje drugiego składnika).
  • Wróć na stronę uwierzytelniania dwuskładnikowego i kliknij Użyj klucza bezpieczeństwa, aby przeprowadzić uwierzytelnianie dwuskładnikowe.
  • Jesteś zalogowany(-a) i powinna wyświetlić się strona Twojego konta.

8. Łatwiejsze korzystanie z danych logowania

Podstawowe funkcje uwierzytelniania dwuskładnikowego za pomocą klucza bezpieczeństwa zostały gotowe. 🚀

Ale... Czy widzisz?

W tej chwili nasza lista danych logowania nie jest zbyt łatwa: identyfikator i klucz publiczny to długie ciągi znaków, które nie pomagają w zarządzaniu danymi logowania. Ludzie nie są zbyt dobre w przypadku długich ciągów znaków i cyfr 🤖

Poprawmy więc tę sytuację i dodajmy funkcję obsługi nazw i zmian nazw danych przy użyciu ciągów czytelnych dla człowieka.

Sprawdź zmianę danych logowania

Aby zaoszczędzić czas potrzebny na wdrożenie tej funkcji, która nie robi nic przełomowego, dodaliśmy w kodzie początkowym funkcję umożliwiającą zmianę nazwy danych logowania w auth.client.js:

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

To jest zwykłe wywołanie aktualizacji bazy danych: klient wysyła do backendu żądanie PUT z identyfikatorem danych logowania i nową nazwą tych danych logowania.

Wdrażanie niestandardowych nazw danych logowania

W funkcji account.html zwróć uwagę na pustą funkcję rename.

Dodaj do niego ten kod:

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

Dane logowania można nazwać dopiero po ich utworzeniu. Utwórzmy więc dane logowania bez nazwy, a po udanym utworzeniu danych logowania – zmień ich nazwy. Spowoduje to jednak wywołanie dwóch backendu.

Użyj funkcji rename w register(), aby umożliwić użytkownikom nadawanie nazw danym logowania podczas rejestracji:

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

Pamiętaj, że dane wprowadzane przez użytkowników będą weryfikowane i oczyszczane w backendzie:

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

Wyświetl nazwy danych logowania

Idź do: getCredentialHtml w: templates.js.

Kod w polu u góry karty danych logowania umożliwia już wyświetlanie:

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

Wypróbuj tę funkcję. 👩🏻 💻

  • Utwórz dane logowania.
  • Pojawi się prośba o podanie nazwy.
  • Wpisz nową nazwę i kliknij OK.
  • Zmieniono dane logowania.
  • Pozostaw pole nazwy puste i upewnij się, że wszystko działa tak samo.

Włącz zmianę nazwy danych logowania

Użytkownicy mogą musieć zmienić nazwę danych logowania, na przykład dodając drugi klucz, aby zmienić nazwę pierwszego klucza, aby lepiej je odróżniać.

W account.html znajdź pustą funkcję renameEl i dodaj do niej ten kod:

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

Teraz w tagu templates.js w elemencie div class="flex-end" dodaj ten kod, który dodaje do szablonu karty logowania przycisk Zmień nazwę. Gdy go klikniesz, wywoła on utworzoną przed chwilą funkcję renameEl:

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

Wypróbuj tę funkcję. 👩🏻 💻

  • Kliknij Zmień nazwę.
  • Wpisz nową nazwę, gdy pojawi się taka prośba.
  • Kliknij OK.
  • Należy zmienić nazwę tych danych logowania, a lista powinna zostać zaktualizowana automatycznie.
  • Ponowne załadowanie strony powinno nadal wyświetlać nową nazwę (wskazuje to, że nowa nazwa jest niezmienna po stronie serwera).

Wyświetl datę utworzenia danych logowania

Data utworzenia nie jest podana w danych logowania utworzonych przez navigator.credential.create().

Jednak dla ułatwienia użytkownikom rozróżniania danych logowania dostosowaliśmy bibliotekę po stronie serwera w kodzie startowym i dodaliśmy pole creationDate równe Date.now(), gdy przechowujemy nowe dane logowania.

W sekcji templates.js w class="creation-date" div dodaj te informacje, aby wyświetlić użytkownikowi informacje o dacie utworzenia:

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

9. Przygotuj kod na przyszłość

Dotychczas poprosiliśmy użytkownika o zarejestrowanie tylko prostego mechanizmu uwierzytelniania w roamingu, który będzie używany jako drugi składnik podczas logowania.

Bardziej zaawansowanym podejściem jest skorzystanie z bardziej wiarygodnego typu mechanizmu uwierzytelniania: UVA do weryfikacji użytkowników. W przypadku logowania jednoetapowego proces UVRA może zapewniać 2 czynniki uwierzytelniania i odporność na phishing.

Najlepiej byłoby, gdyby oba te rozwiązania były obsługiwane. W tym celu musisz dostosować interfejs użytkownika do swoich potrzeb:

  • Jeśli użytkownik korzysta z prostego (nieweryfikowającego) mechanizmu uwierzytelniania w roamingu, pozwól mu użyć tego ustawienia do uzyskania odpornego na phishing phishingu, ale będzie on musiał wpisać również nazwę użytkownika i hasło. Właśnie tak robią nasze ćwiczenia z programowania.
  • Jeśli inny użytkownik korzysta z bardziej zaawansowanego mechanizmu uwierzytelniania w roamingu, może pominąć krok hasła – a nawet nazwę użytkownika – podczas procesu uruchamiania konta.

Więcej informacji znajdziesz w artykule Stosowanie odpornego na phishing phishingu przy użyciu opcjonalnego logowania bez hasła.

W trakcie tych ćwiczeń nie dostosujemy interfejsu użytkownika, ale skonfigurujemy bazę kodu tak, aby dostarczyć Ci wszystkie niezbędne dane, aby dostosować witrynę do potrzeb użytkowników.

Potrzebujesz dwóch rzeczy:

  • Ustaw residentKey: preferred w ustawieniach backendu. Ta czynność została już wykonana.
  • Skonfiguruj sposób sprawdzania, czy wprowadzono możliwe do wykrycia dane logowania (nazywane też kluczem mieszkalnym).

Aby sprawdzić, czy dane logowania możliwe do wykrycia zostały utworzone:

  • Utwórz zapytanie o wartość credProps po utworzeniu danych logowania (credProps: true).
  • Utwórz zapytanie o wartość transports podczas tworzenia danych logowania. Pomoże Ci to określić, czy dana platforma obsługuje funkcje UVRA, niezależnie od tego, czy jest to na przykład telefon komórkowy.
  • Zapisz wartość credProps i transports w backendzie. Ta czynność została już wykonana w kodzie początkowym. Jeśli chcesz się czegoś dowiedzieć, odwiedź stronę auth.js.

Pobierzmy wartości credProps i transports i prześlij je do backendu. W auth.client.js zmień registerCredential w ten sposób:

  • Dodaj pole extensions podczas wywoływania funkcji navigator.credentials.create
  • Zanim wyślesz dane logowania do backendu, aby skonfigurować pamięć, ustaw encodedCredential.transports i encodedCredential.credProps.

registerCredential powinien wyglądać tak:

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

10. Zapewnij obsługę różnych przeglądarek

Obsługa przeglądarek innych niż Chromium

W nowo utworzonych danych logowania w funkcji public/auth.client.jsregisterCredential wywoływamy funkcję credential.response.getTransports(), aby ostatecznie zapisać te informacje w backendie jako wskazówkę dla serwera.

getTransports() nie jest jednak obecnie zaimplementowany we wszystkich przeglądarkach (w przeciwieństwie do getClientExtensionResults, który jest obsługiwany w różnych przeglądarkach): wywołanie getTransports() spowoduje wystąpienie błędu w Firefoksie i Safari, co uniemożliwi utworzenie danych logowania w tych przeglądarkach.

Aby mieć pewność, że Twój kod będzie działać we wszystkich popularnych przeglądarkach, umieść wywołanie encodedCredential.transports w warunku:

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

Na serwerze ustawienie transports jest ustawione na transports || []. W Firefoksie i Safari lista transports nie będzie mieć wartości undefined, a zostanie pusta lista [] – możesz w ten sposób uniknąć błędów.

Ostrzegaj użytkowników korzystających z przeglądarek, które nie obsługują WebAuthn

1e9c1be837d66ce8.png

Mimo że uwierzytelnianie WebAuthn jest obsługiwane we wszystkich popularnych przeglądarkach, zalecamy wyświetlanie ostrzeżenia w przeglądarkach, które nie obsługują WebAuthn.

Obserwuj ten element div w aplikacji index.html:

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

W skrypcie index.html należy dodać ten kod, aby wyświetlać baner w przeglądarkach, które nie obsługują uwierzytelniania WebAuthn:

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

W rzeczywistej aplikacji internetowej możesz robić coś bardziej złożonego i mieć odpowiedni mechanizm zastępczy, który pokazuje, jak sprawdzić obsługę WebAuthn.

11. Brawo!

{/7}Gotowe!

Udało Ci się zastosować uwierzytelnianie dwuskładnikowe za pomocą klucza bezpieczeństwa.

W tym module omówiliśmy podstawy. Jeśli chcesz dalej korzystać z WebAuthn w przypadku weryfikacji dwuetapowej, oto co możesz teraz zrobić:

  • Dodaj informacje o ostatnim użyciu do karty uwierzytelniającej. Jest to przydatna informacja dla użytkowników, którzy chcą sprawdzić, czy dany klucz bezpieczeństwa jest aktywnie używany, zwłaszcza jeśli zarejestrowali już kilka kluczy.
  • Zaimplementuj silniejszą obsługę błędów i bardziej precyzyjne komunikaty o błędach.
  • Przejrzyj auth.js i sprawdź, co się stanie, gdy zmienisz część authSettings, zwłaszcza jeśli używasz klucza obsługującego weryfikację użytkownika.