Rejestracja klucza po stronie serwera

Przegląd

Oto ogólne omówienie najważniejszych czynności związanych z rejestracją klucza dostępu:

Proces rejestracji klucza

  • Określ opcje tworzenia klucza dostępu. Wyślij je do klienta, aby przekazać je do wywołania tworzenia klucza dostępu: do wywołania interfejsu WebAuthn API navigator.credentials.create w przeglądarce i credentialManager.createCredential na urządzeniu z Androidem. Gdy użytkownik potwierdzi utworzenie klucza dostępu, wywołanie tworzenia klucza dostępu zostanie zakończone i zwrócone dane logowania PublicKeyCredential.
  • Sprawdź dane logowania i zapisz je na serwerze.

W sekcjach poniżej znajdziesz szczegółowe informacje na temat każdego kroku.

Tworzenie opcji tworzenia danych logowania

Najpierw musisz utworzyć na serwerze obiekt PublicKeyCredentialCreationOptions.

W tym celu należy skorzystać z biblioteki FIDO po stronie serwera. Zwykle zawiera on funkcję, która umożliwia utworzenie tych opcji. Oferta SimpleWebAuthn, to na przykład generateRegistrationOptions.

Plik PublicKeyCredentialCreationOptions powinien zawierać wszystko, co jest potrzebne do utworzenia klucza dostępu: informacje o użytkowniku, grupie objętej ograniczeniami i konfiguracji właściwości tworzonych danych logowania. Gdy to zrobisz, w razie potrzeby przekaż je do funkcji w bibliotece po stronie serwera FIDO, która odpowiada za utworzenie obiektu PublicKeyCredentialCreationOptions.

.

Niektóre z pól PublicKeyCredentialCreationOptions mogą być stałymi. Inne powinny być dynamicznie zdefiniowane na serwerze:

  • rpId: aby wypełnić identyfikator RP na serwerze, użyj funkcji lub zmiennych po stronie serwera, które dostarczają nazwę hosta aplikacji internetowej, np. example.com.
  • user.name i user.displayName: aby wypełnić te pola, użyj informacji o sesji zalogowanego użytkownika (lub informacji o nowym koncie, jeśli użytkownik tworzy klucz dostępu podczas rejestracji). user.name to zwykle adres e-mail przypisany do grupy objętej ograniczeniami. user.displayName to nazwa przyjazna dla użytkownika. Pamiętaj, że nie wszystkie platformy będą korzystać z elementu displayName.
  • user.id: losowy, unikalny ciąg znaków wygenerowany podczas tworzenia konta. Musi być trwała, w przeciwieństwie do nazwy użytkownika, którą można edytować. Identyfikator użytkownika określa konto, ale nie powinien zawierać żadnych informacji umożliwiających identyfikację osoby. Prawdopodobnie masz już w systemie identyfikator użytkownika, ale w razie potrzeby utwórz go specjalnie na potrzeby kluczy dostępu, aby nie zawierał żadnych informacji umożliwiających identyfikację.
  • excludeCredentials: lista istniejących identyfikatorów danych logowania, aby zapobiec duplikowaniu klucza dostępu od dostawcy kluczy dostępu. Aby wypełnić to pole, wyszukaj istniejące dane logowania tego użytkownika w bazie danych. Szczegółowe informacje znajdziesz w sekcji Blokowanie tworzenia nowego klucza dostępu, jeśli taki klucz już istnieje.
  • challenge: w przypadku rejestracji danych logowania wyzwanie nie ma zastosowania, chyba że korzystasz z atestu, czyli bardziej zaawansowanej metody weryfikacji tożsamości dostawcy klucza dostępu i wysyłanych przez niego danych. Jednak nawet jeśli nie korzystasz z atestu, wyzwanie jest polem wymaganym. W takim przypadku dla uproszczenia możesz ustawić dla tego wyzwania na jedną wartość 0. Instrukcje tworzenia bezpiecznego testu zabezpieczającego do uwierzytelniania znajdziesz w artykule Uwierzytelnianie klucza po stronie serwera.

Kodowanie i dekodowanie

Opcja PublicKeyCredentialCreationOptions została wysłana przez serwer
Wysłano PublicKeyCredentialCreationOptions przez serwer. Parametry challenge, user.id i excludeCredentials.credentials muszą być zakodowane po stronie serwera jako base64URL, aby można było wyświetlać PublicKeyCredentialCreationOptions przez HTTPS.

PublicKeyCredentialCreationOptions zawierają pola typu ArrayBuffer, więc nie są obsługiwane przez JSON.stringify(). Oznacza to, że obecnie, aby wyświetlić PublicKeyCredentialCreationOptions przez HTTPS, niektóre pola muszą być ręcznie zakodowane na serwerze za pomocą base64URL, a potem dekodowane po stronie klienta.

  • Na serwerze kodowanie i dekodowanie jest zwykle obsługiwane przez bibliotekę FIDO po stronie serwera.
  • W tej chwili po stronie klienta kodowanie i dekodowanie musi być wykonywane ręcznie. W przyszłości stanie się to łatwiejsze: dostępna będzie metoda konwertowania opcji z formatu JSON na PublicKeyCredentialCreationOptions. Sprawdź stan implementacji w Chrome.

Przykładowy kod: tworzenie opcji tworzenia danych logowania

W naszych przykładach używamy biblioteki SimpleWebAuthn. Tutaj przekazujemy tworzenie opcji danych logowania klucza publicznego do funkcji generateRegistrationOptions.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

Przechowywanie klucza publicznego

Opcja PublicKeyCredentialCreationOptions została wysłana przez serwer
navigator.credentials.create zwraca obiekt PublicKeyCredential.

Jeśli navigator.credentials.create uda się rozwiązać problem na kliencie, będzie to oznaczać, że klucz dostępu został utworzony. Zwracany jest obiekt PublicKeyCredential.

Obiekt PublicKeyCredential zawiera obiekt AuthenticatorAttestationResponse, który reprezentuje odpowiedź dostawcy klucza dostępu na instrukcję tworzenia klucza dostępu klienta. Zawiera on informacje o nowych danych logowania, które będą Ci potrzebne w ramach grupy objętej ograniczeniami do późniejszego uwierzytelnienia użytkownika. Więcej informacji o programie AuthenticatorAttestationResponse znajdziesz w Dodatku: AuthenticatorAttestationResponse.

Wyślij obiekt PublicKeyCredential do serwera. Po otrzymaniu dokumentu zweryfikuj go.

Przekaż ten etap weryfikacji do swojej biblioteki po stronie serwera FIDO. Zwykle zawiera on funkcję użytkową. Oferta SimpleWebAuthn, to na przykład verifyRegistrationResponse. Więcej informacji znajdziesz w artykule Dodatek: weryfikacja odpowiedzi rejestracyjnej.

Po pomyślnej weryfikacji zapisz dane logowania w bazie danych, aby użytkownik mógł później uwierzytelnić się za pomocą klucza dostępu powiązanego z tymi danymi logowania.

Użyj dedykowanej tabeli na dane logowania klucza publicznego powiązane z kluczami dostępu. Użytkownik może mieć tylko 1 hasło, ale może mieć kilka kluczy dostępu – na przykład klucz dostępu zsynchronizowany z pękiem kluczy Apple iCloud i drugi w Menedżerze haseł Google.

Oto przykładowy schemat, w którym możesz przechowywać informacje o danych logowania:

Schemat bazy danych na potrzeby kluczy dostępu

  • Tabela Użytkownicy:
    • user_id: główny identyfikator użytkownika. Losowy, unikalny, stały identyfikator użytkownika. Używaj go jako klucza podstawowego dla tabeli Użytkownicy.
    • username. Zdefiniowana przez użytkownika nazwa użytkownika, którą można edytować.
    • passkey_user_id: identyfikator użytkownika bez informacji umożliwiających identyfikację osoby powiązany z kluczem, reprezentowany przez user.id w opcjach rejestracji. Gdy użytkownik później spróbuje się uwierzytelnić, mechanizm uwierzytelniający udostępni ten element (passkey_user_id) w odpowiedzi na uwierzytelnienie w narzędziu userHandle. Nie zalecamy ustawiania klucza passkey_user_id jako klucza podstawowego. Klucze podstawowe stają się zwykle w systemach informacjami umożliwiającymi identyfikację osób, ponieważ są one powszechnie używane.
  • Tabela danych logowania klucza publicznego:
    • id: identyfikator danych logowania. Możesz go używać jako klucza podstawowego dla tabeli Dane logowania klucza publicznego.
    • public_key: klucz publiczny danych logowania.
    • passkey_user_id: użyj go jako klucza obcego, aby utworzyć połączenie z tabelą Użytkownicy.
    • backed_up: Kopia zapasowa klucza dostępu jest tworzona, jeśli zostanie zsynchronizowana przez dostawcę klucza. Przechowywanie stanu kopii zapasowej jest przydatne, jeśli zamierzasz w przyszłości zrezygnować z haseł w przypadku użytkowników, którzy mają klucze dostępu (backed_up). Aby sprawdzić, czy klucz dostępu ma kopię zapasową, przejrzyj flagi w authenticatorData lub użyj funkcji biblioteki po stronie serwera FIDO, która zwykle zapewnia łatwy dostęp do tych informacji. Przechowywanie uprawnień do korzystania z kopii zapasowej może pomóc w rozwiązaniu potencjalnych pytań użytkowników.
    • name: opcjonalnie wyświetlana nazwa danych logowania, która umożliwia użytkownikom nadawanie im niestandardowych nazw.
    • transports: tablica transportu. Przechowywanie danych ułatwia uwierzytelnianie. Gdy dostępne są mechanizmy transportowe, przeglądarka może działać w odpowiedni sposób i wyświetlać interfejs zgodny z interfejsem używanym przez dostawcę klucza dostępu do komunikacji z klientami – w szczególności w przypadku ponownego uwierzytelniania, gdy pole allowCredentials nie jest puste.

Aby zadbać o wygodę użytkowników, warto przechowywać inne informacje, takie jak dostawca klucza dostępu, czas utworzenia danych logowania i czas ostatniego użycia. Więcej informacji znajdziesz w artykule na temat projektowania interfejsu kluczy dostępu.

Przykładowy kod: przechowywanie danych logowania

W naszych przykładach używamy biblioteki SimpleWebAuthn. Tutaj przekazujemy weryfikację odpowiedzi na żądanie rejestracji funkcji verifyRegistrationResponse.

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('Verification failed.');
    }

    const { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

    // Kill the challenge for this session
    delete req.session.challenge;

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

Dodatek: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse zawiera 2 ważne obiekty:

  • response.clientDataJSON to wersja JSON danych klienta, które w internecie są danymi widocznymi dla przeglądarki. Zawiera ona źródło grupy objętej ograniczeniami, wyzwanie i androidPackageName, jeśli klient jest aplikacją na Androida. Ponieważ RP udzielana jest w trybie RP, odczyt clientDataJSONumożliwia dostęp do informacji, które przeglądarka zobaczyła w momencie wysyłania żądania create.
  • response.attestationObjectzawiera 2 informacje:
    • attestationStatement, który nie jest odpowiedni, chyba że użyjesz atestu.
    • authenticatorData to dane widoczne dla dostawcy klucza dostępu. W ramach grupy objętej ograniczeniami odczyt authenticatorDataumożliwia dostęp do danych widocznych dla dostawcy klucza dostępu i zwracanych w momencie wysyłania żądania create.

authenticatorDatazawiera ważne informacje o danych logowania klucza publicznego powiązanych z nowo utworzonym kluczem dostępu:

  • Dane uwierzytelniające klucza publicznego i jego unikalny identyfikator.
  • Identyfikator RP powiązany z danymi logowania.
  • Flagi opisujące stan użytkownika podczas tworzenia klucza dostępu: czy użytkownik rzeczywiście był obecny i czy jego weryfikacja przebiegła pomyślnie (patrz userVerification).
  • AAGUID – identyfikuje dostawcę klucza dostępu. Wyświetlenie dostawcy kluczy dostępu może być przydatne dla użytkowników, zwłaszcza jeśli zarejestrowali oni klucz dostępu w Twojej usłudze u wielu dostawców kluczy.

Mimo że atrybut authenticatorData jest zagnieżdżony w elemencie attestationObject, zawarte w nim informacje są potrzebne do implementacji klucza dostępu niezależnie od tego, czy używasz atestu. Parametr authenticatorData jest zakodowany i zawiera pola zakodowane w formacie binarnym. Biblioteka po stronie serwera zazwyczaj obsługuje analizowanie i dekodowanie. Jeśli nie korzystasz z biblioteki po stronie serwera, rozważ wykorzystanie getAuthenticatorData() po stronie klienta, aby zaoszczędzić sobie czas na analizowanie i dekodowanie po stronie serwera.

Załącznik: weryfikacja odpowiedzi rejestracyjnej

Proces weryfikowania odpowiedzi rejestracji składa się z następujących etapów:

  • Upewnij się, że identyfikator RP jest zgodny z identyfikatorem w witrynie.
  • Upewnij się, że źródłem żądania jest oczekiwane źródło Twojej witryny (adres URL głównej witryny, aplikacja na Androida).
  • Jeśli wymagasz weryfikacji użytkownika, ustaw flagę weryfikacji użytkownika authenticatorData.uv na wartość true. Sprawdź, czy flaga obecności użytkownika authenticatorData.up to true, ponieważ obecność użytkownika jest zawsze wymagana w przypadku kluczy dostępu.
  • Sprawdź, czy klient mógł wykonać zadanie. Jeśli nie korzystasz z atestu, ten test jest nieważny. Stosowanie tej weryfikacji jest jednak sprawdzoną metodą: dzięki niej kod będzie gotowy, jeśli w przyszłości zdecydujesz się na korzystanie z atestu.
  • Upewnij się, że identyfikator danych logowania nie został jeszcze zarejestrowany dla żadnego użytkownika.
  • Sprawdź, czy algorytm użyty przez dostawcę klucza dostępu do utworzenia danych logowania to algorytm podany przez Ciebie (w każdym polu alg w publicKeyCredentialCreationOptions.pubKeyCredParams, który jest zwykle zdefiniowany w bibliotece po stronie serwera i nie jest dla Ciebie widoczny). Dzięki temu użytkownicy będą mogli rejestrować się tylko za pomocą dozwolonych przez Ciebie algorytmów.

Aby dowiedzieć się więcej, zapoznaj się z kodem źródłowym witryny verifyRegistrationResponse dostępnym w witrynie SimpleWebAuthn lub zapoznaj się z pełną listą weryfikacji w specyfikacji.

Następny

Uwierzytelnianie klucza po stronie serwera