Rejestracja klucza po stronie serwera

Przegląd

Oto ogólny przegląd kluczowych kroków rejestracji klucza 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 tworzenia klucza dostępu zostanie rozwiązane i zwróci dane logowania PublicKeyCredential.
  • Sprawdź dane logowania i zapisz je na serwerze.

W kolejnych sekcjach znajdziesz szczegółowe informacje o każdym kroku.

Tworzenie opcji tworzenia danych logowania

Pierwszym krokiem, jaki musisz wykonać na serwerze, jest utworzenie obiektu PublicKeyCredentialCreationOptions.

W tym celu użyj biblioteki FIDO po stronie serwera. Zwykle oferuje funkcję narzędziową, która może utworzyć te opcje. SimpleWebAuthn oferuje na przykład generateRegistrationOptions.

PublicKeyCredentialCreationOptions powinna zawierać wszystko, co jest potrzebne do utworzenia klucza dostępu: informacje o użytkowniku, o podmiocie zależnym oraz konfigurację właściwości tworzonych danych logowania. Po zdefiniowaniu wszystkich tych wartości przekaż je w razie potrzeby do funkcji w bibliotece po stronie serwera FIDO, która jest odpowiedzialna za tworzenie obiektu PublicKeyCredentialCreationOptions.

Niektóre pola PublicKeyCredentialCreationOptions mogą być stałymi. Pozostałe powinny być definiowane dynamicznie na serwerze:

  • rpId: aby wypełnić identyfikator RP na serwerze, użyj funkcji lub zmiennych po stronie serwera, które podają nazwę hosta aplikacji internetowej, np. example.com.
  • user.nameuser.displayName: aby wypełnić te pola, użyj informacji o sesji zalogowanego użytkownika (lub informacji o nowym koncie użytkownika, jeśli tworzy on klucz dostępu podczas rejestracji). user.name to zwykle adres e-mail, który jest unikalny dla RP. user.displayName to przyjazna dla użytkownika nazwa. Pamiętaj, że nie wszystkie platformy będą używać displayName.
  • user.id: losowy, unikalny ciąg znaków generowany podczas tworzenia konta. Powinien być trwały, 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ę osoby. 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 identyfikatorów istniejących danych logowania, która zapobiega duplikowaniu klucza dostępu przez dostawcę kluczy dostępu. Aby wypełnić to pole, wyszukaj w bazie danych istniejące dane logowania tego użytkownika. Szczegółowe informacje znajdziesz w artykule Zapobieganie tworzeniu nowego klucza dostępu, jeśli już istnieje.
  • challenge: w przypadku rejestracji danych logowania test nie ma znaczenia, chyba że używasz atestu, czyli bardziej zaawansowanej techniki weryfikacji tożsamości dostawcy klucza dostępu i emitowanych przez niego danych. Nawet jeśli nie używasz atestu, wyzwanie jest polem wymaganym. Instrukcje tworzenia bezpiecznego wyzwania na potrzeby uwierzytelniania znajdziesz w artykule Uwierzytelnianie za pomocą klucza dostępu po stronie serwera.

Kodowanie i dekodowanie

Opcje tworzenia danych logowania klucza publicznego wysłane przez serwer
PublicKeyCredentialCreationOptions wysłane przez serwer. challenge, user.idexcludeCredentials.credentials muszą być zakodowane po stronie serwera w base64URL, aby PublicKeyCredentialCreationOptions mogło być dostarczane przez HTTPS.

PublicKeyCredentialCreationOptions zawierają pola, które są ArrayBuffer, więc nie są obsługiwane przez JSON.stringify(). Oznacza to, że obecnie, aby dostarczyć PublicKeyCredentialCreationOptions przez HTTPS, niektóre pola muszą być ręcznie zakodowane na serwerze za pomocą base64URL, a następnie zdekodowane na kliencie.

  • Na serwerze kodowanie i dekodowanie jest zwykle obsługiwane przez bibliotekę FIDO po stronie serwera.
  • Na kliencie kodowanie i dekodowanie musi być obecnie wykonywane ręcznie. W przyszłości będzie to łatwiejsze: dostępna będzie metoda konwertowania opcji w formacie JSON na PublicKeyCredentialCreationOptions. Sprawdź stan wdrożenia w Chrome.

Przykładowy kod: tworzenie opcji tworzenia danych logowania

W naszych przykładach używamy biblioteki SimpleWebAuthn. W tym przypadku przekazujemy tworzenie opcji danych logowania za pomocą klucza publicznego do funkcji generateRegistrationOptions.

.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 = await 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

Opcje tworzenia danych logowania klucza publicznego wysłane przez serwer
navigator.credentials.create zwraca obiekt PublicKeyCredential.

Gdy navigator.credentials.create zostanie pomyślnie rozwiązany na urządzeniu klienta, 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 informacje o nowych danych logowania, które są potrzebne stronie ufającej do późniejszego uwierzytelniania użytkownika. Więcej informacji o AuthenticatorAttestationResponse znajdziesz w Dodatku: AuthenticatorAttestationResponse.

Wyślij obiekt PublicKeyCredential na serwer. Po otrzymaniu adresu e-mail zweryfikuj go.

Przekaż ten krok weryfikacji do biblioteki FIDO po stronie serwera. Zwykle oferuje w tym celu funkcję narzędziową. SimpleWebAuthn oferuje na przykład verifyRegistrationResponse. Więcej informacji o tym, co dzieje się w tle, znajdziesz w Dodatku: weryfikacja odpowiedzi na rejestrację.

Po pomyślnej 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.

Używaj osobnej tabeli na potrzeby danych logowania klucza publicznego powiązanych z kluczami dostępu. Użytkownik może mieć tylko jedno hasło, ale może mieć wiele kluczy dostępu – na przykład klucz dostępu zsynchronizowany za pomocą pęku kluczy iCloud od Apple i klucz dostępu zsynchronizowany za pomocą Menedżera haseł Google.

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

Schemat bazy danych kluczy dostępu

  • Tabela Użytkownicy:
    • user_id: identyfikator głównego użytkownika. Losowy, unikalny i trwały identyfikator użytkownika. Użyj go jako klucza podstawowego w tabeli Użytkownicy.
    • username Nazwa użytkownika zdefiniowana przez użytkownika, którą można edytować.
    • passkey_user_id: identyfikator użytkownika bez informacji umożliwiających identyfikację osoby, który jest powiązany z kluczem dostępu i reprezentowany przez symbol user.idopcjach rejestracji. Gdy użytkownik spróbuje później się uwierzytelnić, uwierzytelniający udostępni tenpasskey_user_id w odpowiedzi na uwierzytelnianie w userHandle. Nie zalecamy ustawiania passkey_user_id jako klucza podstawowego. Klucze podstawowe zwykle stają się w systemach danymi osobowymi, ponieważ są powszechnie używane.
  • Tabela Dane logowania za pomocą klucza publicznego:
    • id: identyfikator dokumentu potwierdzającego tożsamość. Użyj go jako klucza podstawowego w tabeli Dane logowania za pomocą klucza publicznego.
    • public_key: klucz publiczny danych logowania.
    • passkey_user_id: użyj tego pola jako klucza obcego, aby utworzyć połączenie z tabelą Users.
    • backed_up: Klucz dostępu jest zapisywany w kopii zapasowej, jeśli jest synchronizowany przez dostawcę kluczy dostępu. Przechowywanie stanu kopii zapasowej jest przydatne, jeśli w przyszłości chcesz zrezygnować z haseł w przypadku użytkowników, którzy mają backed_up klucze dostępu. Możesz sprawdzić, czy klucz dostępu jest objęty kopią zapasową, sprawdzając flagę BE w authenticatorData lub korzystając z funkcji biblioteki po stronie serwera FIDO, która zwykle jest dostępna, aby ułatwić Ci dostęp do tych informacji. Przechowywanie informacji o możliwości utworzenia kopii zapasowej może być przydatne w odpowiadaniu na potencjalne pytania użytkowników.
    • name: opcjonalnie nazwa wyświetlana uprawnień, która umożliwia użytkownikom nadawanie im niestandardowych nazw.
    • transports: tablica środków transportu. Przechowywanie transportów jest przydatne w przypadku uwierzytelniania użytkowników. Gdy dostępne są transporty, przeglądarka może odpowiednio reagować i wyświetlać interfejs użytkownika dopasowany do transportu, którego dostawca klucza dostępu używa do komunikacji z klientami – w szczególności w przypadku ponownego uwierzytelniania, gdy pole allowCredentials nie jest puste.

Inne informacje mogą być przydatne do przechowywania ze względu na wygodę użytkownika, w tym takie jak dostawca klucza dostępu, czas utworzenia danych logowania i czas ostatniego użycia. Więcej informacji znajdziesz w artykule Projekt interfejsu użytkownika kluczy dostępu.

Przykładowy kod: zapisywanie danych logowania

W naszych przykładach używamy biblioteki SimpleWebAuthn. W tym miejscu przekazujemy weryfikację odpowiedzi na rejestrację do 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 {
      aaguid,
      credentialPublicKey,
      credentialID,
      credentialBackedUp
    } = registrationInfo;

    // Name the credential based on AAGUID
    const name =
      aaguid === undefined ||
      aaguid === '000000-0000-0000-0000-00000000' ?
        req.useragent?.platform : aaguids[aaguid].name;

    const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
    const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);

    // Existing, signed-in user
    const { user } = res.locals;

    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      transports: response.response.transports,
      backed_up: credentialBackedUp,
      registered_at: new Date().getTime()
    });

    // 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, czyli danych widocznych w przeglądarce. Zawiera źródło RP, wyzwanie i informację, androidPackageName czy klient jest aplikacją na Androida. Jako RP możesz odczytać clientDataJSON, aby uzyskać dostęp do informacji, które przeglądarka widziała w momencie wysłania żądania create.
  • response.attestationObjectzawiera 2 rodzaje informacji:
    • attestationStatement, która nie jest istotna, chyba że używasz atestu.
    • authenticatorData to dane widoczne dla dostawcy klucza dostępu. Jako RP czytanie authenticatorDataumożliwia dostęp do danych widocznych dla dostawcy klucza dostępu i zwracanych w momencie createżądania.

authenticatorDatazawiera niezbędne informacje o danych logowania za pomocą klucza publicznego, które są powiązane z nowo utworzonym kluczem dostępu:

Chociaż authenticatorData jest zagnieżdżony w attestationObject, informacje, które zawiera, są potrzebne do wdrożenia 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 zwykle zajmuje się analizowaniem i dekodowaniem. Jeśli nie używasz biblioteki po stronie serwera, rozważ wykorzystanie getAuthenticatorData()biblioteki po stronie klienta, aby zaoszczędzić sobie pracy związanej z parsowaniem i dekodowaniem po stronie serwera.

Dodatek: weryfikacja odpowiedzi na rejestrację

Weryfikacja odpowiedzi na rejestrację obejmuje te sprawdzenia:

  • Upewnij się, że identyfikator dostawcy tożsamości jest zgodny z Twoją witryną.
  • Upewnij się, że źródło żądania jest oczekiwanym źródłem w przypadku Twojej witryny (główny adres URL witryny, aplikacja na Androida).
  • Jeśli wymagana jest weryfikacja użytkownika, upewnij się, że flaga weryfikacji użytkownika authenticatorData.uv ma wartość true.
  • Oczekuje się, że flaga obecności użytkownika authenticatorData.up będzie miała wartość true, ale jeśli dane logowania zostały utworzone warunkowo, oczekuje się, że będzie miała wartość false.
  • Sprawdź, czy klient był w stanie rozwiązać zadanie, które mu zostało przydzielone. Jeśli nie używasz atestu, to sprawdzenie nie ma znaczenia. Warto jednak wdrożyć to sprawdzenie, aby mieć pewność, że kod jest gotowy, jeśli w przyszłości zdecydujesz się używać atestu.
  • Sprawdź, czy identyfikator danych logowania nie jest jeszcze zarejestrowany dla żadnego użytkownika.
  • Sprawdź, czy algorytm używany przez dostawcę klucza dostępu do utworzenia danych logowania jest algorytmem, który został przez Ciebie wymieniony (w każdym polu algpublicKeyCredentialCreationOptions.pubKeyCredParams, które jest zwykle zdefiniowane w bibliotece po stronie serwera i niewidoczne dla Ciebie). Dzięki temu użytkownicy mogą rejestrować się tylko za pomocą algorytmów, które zezwolisz.

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

Następny krok

Uwierzytelnianie za pomocą klucza dostępu po stronie serwera