Rejestracja klucza po stronie serwera

Omówienie

Oto ogólny przegląd najważniejszych czynności związanych z rejestracją kluczy dostępu:

Proces rejestracji klucza dostępu

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

W sekcjach poniżej znajdziesz szczegółowe informacje o poszczególnych krokach.

Tworzenie opcji tworzenia danych logowania

Pierwszym krokiem na serwerze jest utworzenie obiektu PublicKeyCredentialCreationOptions.

W tym celu korzystaj z biblioteki FIDO po stronie serwera. Zwykle dostępna jest funkcja użytkowa umożliwiająca utworzenie tych opcji. SimpleWebAuthn oferuje np. generateRegistrationOptions.

PublicKeyCredentialCreationOptions powinien zawierać wszystko, co jest potrzebne do utworzenia klucza dostępu: informacje o użytkowniku, informacje o RP i konfiguracja właściwości tworzonych danych logowania. Gdy zdefiniujesz wszystkie te zasady, przekaż je odpowiednio do funkcji w bibliotece po stronie serwera FIDO odpowiedzialnej za utworzenie obiektu PublicKeyCredentialCreationOptions.

Niektóre z PublicKeyCredentialCreationOptions' mogą być stałe. Inne powinny być dynamicznie zdefiniowane na serwerze:

  • rpId: aby uzupełnić identyfikator RP na serwerze, użyj funkcji lub zmiennych po stronie serwera, które zapewnią Ci 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 użytkownika, jeśli użytkownik tworzy klucz dostępu podczas rejestracji). user.name to zwykle adres e-mail, który jest unikalny dla grupy objętej ograniczeniami. user.displayName to przyjazna dla użytkownika nazwa. Pamiętaj, że nie wszystkie platformy korzystają z displayName.
  • user.id: losowy, unikalny ciąg znaków wygenerowany podczas tworzenia konta. Musi być ona trwała, w przeciwieństwie do nazwy użytkownika, którą można edytować. Identyfikator użytkownika identyfikuje konto, ale nie powinien zawierać żadnych informacji umożliwiających identyfikację. Prawdopodobnie masz już w swoim systemie identyfikator użytkownika, ale w razie potrzeby utwórz go specjalnie dla kluczy dostępu, aby nie zawierał żadnych informacji umożliwiających identyfikację.
  • excludeCredentials: lista istniejących danych logowania identyfikatory zapobiegające duplikowaniu klucza 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 istnieje.
  • challenge: w przypadku rejestracji danych logowania test zabezpieczający nie ma zastosowania, chyba że korzystasz z atestu – bardziej zaawansowanej metody weryfikacji tożsamości dostawcy kluczy dostępu i przesyłanych przez niego danych. Jednak nawet wtedy, gdy nie używasz atestu, test zabezpieczający nadal jest wymagany. Dla uproszczenia w takim przypadku możesz ustawić dla tego wyzwania tylko 1 0. Instrukcje tworzenia bezpiecznego testu zabezpieczającego uwierzytelnianie znajdziesz w sekcji Uwierzytelnianie za pomocą klucza dostępu po stronie serwera.

Kodowanie i dekodowanie

PublicKeyCredentialCreationOptions wysłana przez serwer
Wiadomość PublicKeyCredentialCreationOptions wysłana przez serwer. Aby wiadomości PublicKeyCredentialCreationOptions mogły być dostarczane przez HTTPS, challenge, user.id i excludeCredentials.credentials muszą być zakodowane po stronie serwera w kodzie base64URL.

PublicKeyCredentialCreationOptions zawiera pola o wartości ArrayBuffer, więc JSON.stringify() nie obsługuje ich. Oznacza to, że w tej chwili, aby dostarczyć PublicKeyCredentialCreationOptions przez HTTPS, niektóre pola trzeba zakodować ręcznie na serwerze za pomocą base64URL, a następnie zdekodować po stronie klienta.

  • Na serwerze kodowaniem i dekodowaniem zajmuje się zazwyczaj biblioteka FIDO po stronie serwera.
  • Po stronie klienta kodowanie i dekodowanie należy wykonać ręcznie. W przyszłości będzie to łatwiejsze: udostępnimy metodę konwertowania opcji w formacie JSON na format PublicKeyCredentialCreationOptions. Sprawdź stan implementacji w Chrome.

Przykładowy kod: tworzenie opcji tworzenia danych logowania

W przykładach używamy biblioteki SimpleWebAuthn. Tutaj przekazujemy funkcję generateRegistrationOptions tworzenia opcji danych logowania klucza publicznego.

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

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

Gdy uda się rozwiązać problem navigator.credentials.create na kliencie, oznacza to, ż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ę klienta dotyczącą utworzenia klucza dostępu. Zawiera on informacje o nowych danych logowania, których potrzebujesz jako grupy objętej ograniczeniami do późniejszego uwierzytelnienia użytkownika. Więcej informacji o AuthenticatorAttestationResponse znajdziesz w Załączniku: AuthenticatorAttestationResponse.

Wyślij obiekt PublicKeyCredential na serwer. Gdy go otrzymasz, zweryfikuj go.

Przekaż ten etap weryfikacji do biblioteki FIDO po stronie serwera. Zwykle oferuje do tego funkcję użytkową. SimpleWebAuthn oferuje np. verifyRegistrationResponse. Więcej informacji o tym, co dzieje się poza ukrytymi systemami, znajdziesz w Załączniku: weryfikacja odpowiedzi na proces rejestracji.

Po pomyślnym przeprowadzeniu weryfikacji zapisz informacje o danych 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 dla danych logowania klucza publicznego powiązanych z kluczami dostępu. Użytkownik może mieć tylko 1 hasło, ale może mieć wiele kluczy dostępu – na przykład klucz zsynchronizowany przez pęk kluczy Apple iCloud i 1 za pomocą Menedżera haseł Google.

Oto przykładowy schemat, którego można użyć do przechowywania danych logowania:

Schemat bazy danych kluczy dostępu

  • Tabela Użytkownicy:
    • user_id: podstawowy identyfikator użytkownika. Losowy, unikalny, stały identyfikator użytkownika. Użyj go jako klucza podstawowego w 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ę, powiązany z kluczem dostępu, reprezentowany przez user.id w opcjach rejestracji. Gdy użytkownik później spróbuje się uwierzytelnić, uwierzytelnianie udostępni ten identyfikator passkey_user_id w odpowiedzi uwierzytelniania w userHandle. Nie zalecamy ustawiania klucza passkey_user_id jako klucza podstawowego. Klucze podstawowe zwykle stają się informacjami umożliwiającymi identyfikację osób w systemach, ponieważ są powszechnie używane.
  • Tabela danych logowania klucza publicznego:
    • id: identyfikator certyfikatu. Użyj go jako klucza podstawowego w tabeli Dane logowania klucza publicznego.
    • public_key: klucz publiczny danych logowania.
    • passkey_user_id: użyj tego klucza jako obcego klucza, aby utworzyć połączenie z tabelą Users (Użytkownicy).
    • backed_up: klucz dostępu jest kopiowany, jeśli jest zsynchronizowany przez dostawcę klucza. Przechowywanie stanu kopii zapasowej jest przydatne, jeśli w przyszłości zechcesz usunąć hasła użytkowników, którzy mają backed_up kluczy dostępu. Aby sprawdzić, czy kopia zapasowa klucza została utworzona, przejrzyj flagi w authenticatorData lub skorzystaj z funkcji biblioteki FIDO po stronie serwera, która jest zwykle dostępna w celu zapewnienia łatwego dostępu do tych informacji. Przechowywanie kwalifikacji do tworzenia kopii zapasowych może pomóc w odpowiedzi na potencjalne pytania użytkowników.
    • name: opcjonalnie wyświetlana nazwa danych logowania, która umożliwia użytkownikom nadawanie danych logowania niestandardowych nazw.
    • transports: tablica transportów. Przechowywanie transportów jest przydatne podczas uwierzytelniania użytkowników. Gdy dostępne są przenoszenie, przeglądarka może działać odpowiednio i wyświetlić interfejs zgodny z transportem używanym przez dostawcę klucza do komunikacji z klientami – zwłaszcza w przypadku ponownego uwierzytelniania, gdy pole allowCredentials nie jest puste.

Aby zwiększyć 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 o projektowaniu interfejsu kluczy dostępu.

Przykładowy kod: przechowywanie danych logowania

W przykładach używamy biblioteki SimpleWebAuthn. W tym przypadku przekazujemy weryfikację odpowiedzi rejestracyjnej jej 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 });
  }
});

Załącznik: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse zawiera dwa ważne obiekty:

  • response.clientDataJSON to wersja JSON danych klienta, która w internecie jest danymi widocznymi dla przeglądarki. Zawiera źródło RP, wyzwanie i androidPackageName, jeśli klient to aplikacja na Androida. Czytanie clientDataJSON daje dostęp do informacji, które przeglądarka widziała w momencie wysłania żądania create.
  • response.attestationObjectzawiera 2 informacje:
    • attestationStatement, który nie ma zastosowania, chyba że użyjesz atestu.
    • authenticatorData to dane widoczne dla dostawcy klucza dostępu. authenticatorData daje Ci dostęp do danych wyświetlonych przez dostawcę klucza dostępu i zwróconych w momencie żądania create.

authenticatorDatazawiera kluczowe informacje o danych logowania klucza publicznego powiązanych z nowo utworzonym kluczem:

  • Dane logowania klucza publicznego wraz z unikalnym identyfikatorem danych logowania.
  • Identyfikator grupy objętej ograniczeniami powiązany z danym uwierzytelniającym.
  • Flagi opisujące stan użytkownika w momencie utworzenia klucza dostępu: czy użytkownik był tam rzeczywiście i czy przeszedł weryfikację (patrz userVerification).
  • AAGUID, który identyfikuje dostawcę klucza dostępu. Wyświetlanie dostawcy kluczy dostępu może być przydatne dla użytkowników, zwłaszcza jeśli mają oni zarejestrowany klucz dostępu dla usługi u różnych dostawców kluczy dostępu.

Mimo że tag authenticatorData jest umieszczony w obrębie attestationObject, zawarte w nim informacje są potrzebne do implementacji klucza dostępu niezależnie od tego, czy używasz atestu. authenticatorData jest zakodowany i zawiera pola zakodowane w formacie binarnym. Biblioteka po stronie serwera zazwyczaj zajmuje się analizą i dekodowaniem. Jeśli nie korzystasz z biblioteki po stronie serwera, rozważ wykorzystanie getAuthenticatorData() po stronie klienta, aby zaoszczędzić na analizie i dekodowaniu plików roboczych po stronie serwera.

Załącznik: weryfikacja odpowiedzi na rejestrację

Weryfikacja odpowiedzi na rejestrację składa się z tych etapów weryfikacji:

  • Upewnij się, że identyfikator grupy objętej ograniczeniami jest zgodny z identyfikatorem Twojej witryny.
  • Upewnij się, że źródło żądania jest oczekiwanym źródłem Twojej witryny (główny adres URL witryny, aplikacja na Androida).
  • Jeśli wymagasz weryfikacji użytkownika, upewnij się, że flaga weryfikacji użytkownika authenticatorData.uv ma wartość true. Sprawdź, czy flaga obecności użytkownika authenticatorData.up to true, ponieważ w przypadku kluczy dostępu obecność użytkownika jest zawsze wymagana.
  • Sprawdź, czy klient był w stanie zrealizować zadane przez Ciebie wyzwanie. Jeśli nie używasz atestu, ta weryfikacja jest nieistotna. Wdrożenie tej metody sprawdzania jest jednak sprawdzoną metodą. Dzięki niej masz pewność, że kod będzie gotowy, jeśli w przyszłości zdecydujesz się na użycie atestu.
  • Upewnij się, że identyfikator danych logowania nie jest jeszcze zarejestrowany dla żadnego użytkownika.
  • Sprawdź, czy algorytm używany przez dostawcę kluczy dostępu do tworzenia danych logowania jest wymienionym przez Ciebie algorytmem wymienionym na liście (w każdym polu alg obiektu publicKeyCredentialCreationOptions.pubKeyCredParams, które jest zwykle zdefiniowane w bibliotece po stronie serwera i nie jest dla Ciebie widoczne). Dzięki temu użytkownicy będą mogli rejestrować się tylko przy użyciu dozwolonych przez Ciebie algorytmów.

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

Następny krok

Uwierzytelnianie za pomocą klucza dostępu po stronie serwera