Uwierzytelnianie klucza po stronie serwera

.

Omówienie

Oto ogólny przegląd najważniejszych kroków związanych z uwierzytelnianiem za pomocą klucza dostępu:

Proces uwierzytelniania klucza dostępu

  • Zdefiniuj test zabezpieczający i inne opcje wymagane do uwierzytelnienia za pomocą klucza dostępu. Wyślij je do klienta, aby przekazać je do wywołania uwierzytelniania klucza dostępu (navigator.credentials.get w przeglądarce). Gdy użytkownik potwierdzi uwierzytelnianie przy użyciu klucza dostępu, wywołanie uwierzytelniania klucza dostępu zostanie zakończone i zwrócone dane logowania (PublicKeyCredential). Dane logowania zawierają potwierdzenie uwierzytelniania.
.
  • Zweryfikuj potwierdzenie uwierzytelniania.
  • Jeśli potwierdzenie uwierzytelniania jest prawidłowe, uwierzytelnij użytkownika.

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

Utwórz wyzwanie

W praktyce wyzwanie to tablica losowych bajtów, reprezentowana jako obiekt ArrayBuffer.

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

Aby mieć pewność, że wyzwanie spełni swoje zadanie, musisz:

  1. Upewnij się, że to samo wyzwanie nie jest używane więcej niż raz. Wygeneruj nowe zadanie przy każdej próbie logowania. Odrzuć test zabezpieczający po każdej próbie zalogowania się niezależnie od tego, czy się udało. Możesz też odrzucić wyzwanie po określonym czasie. Nigdy nie podejmuj tego samego wyzwania w odpowiedzi więcej niż raz.
  2. Upewnij się, że test jest zabezpieczony kryptograficznie. Wyzwanie powinno być praktycznie niemożliwe do odgadnięcia. Aby utworzyć kryptograficznie bezpieczną bibliotekę wyzwania po stronie serwera, najlepiej jest korzystać z zaufanej biblioteki FIDO po stronie serwera. Jeśli zamiast tego tworzysz własne wyzwania, skorzystaj z wbudowanych funkcji kryptograficznych dostępnych w Twoim stosie technologicznym lub poszukaj bibliotek zaprojektowanych na potrzeby zastosowań kryptograficznych. Mogą to być na przykład iso-crypto w Node.js lub obiekty tajne w Pythonie. Zgodnie ze specyfikacją test zabezpieczający musi mieć co najmniej 16 bajtów, aby został uznany za bezpieczny.

Po utworzeniu wyzwania zapisz je w sesji użytkownika, aby móc je później zweryfikować.

Opcje tworzenia żądania danych logowania

Tworzenie opcji żądań danych logowania jako obiektu publicKeyCredentialRequestOptions.

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. generateAuthenticationOptions.

W polu publicKeyCredentialRequestOptions powinny znajdować się wszystkie informacje potrzebne do uwierzytelnienia za pomocą klucza dostępu. Przekaż te informacje do funkcji w bibliotece FIDO po stronie serwera, która odpowiada za utworzenie obiektu publicKeyCredentialRequestOptions.

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

  • rpId: z którym identyfikatorem grupy objętej ograniczeniami, z którym dane logowania mają być powiązane, np. example.com. Uwierzytelnianie się powiedzie tylko wtedy, gdy podany tutaj identyfikator grupy objętej ograniczeniami jest zgodny z identyfikatorem RP powiązanym z danymi uwierzytelniającymi. Aby uzupełnić identyfikator grupy objętej ograniczeniami, użyj tej samej wartości co identyfikator RP ustawiony w publicKeyCredentialCreationOptions podczas rejestracji danych logowania.
  • challenge: dane, które dostawca klucza podpisuje, aby potwierdzić, że użytkownik posiada klucz dostępu w momencie wysłania prośby o uwierzytelnienie. Sprawdź szczegóły w sekcji Utwórz wyzwanie.
  • allowCredentials: tablica z danymi uwierzytelniającymi akceptowalnymi podczas tego uwierzytelniania. Przekaż pustą tablicę, aby użytkownik mógł wybrać dostępny klucz dostępu z listy wyświetlanej przez przeglądarkę. Więcej informacji znajdziesz w artykułach Pobieranie wyzwania z serwera RP i Szczegółowe omówienie danych logowania z możliwością wykrywania.
  • userVerification: wskazuje, czy weryfikacja użytkownika przy użyciu blokady ekranu urządzenia jest „wymagana”, „preferowana”. czy „odradzane”. Przeczytaj artykuł Pobieranie wyzwania z serwera RP.
  • timeout: czas (w milisekundach) potrzebny użytkownikowi na ukończenie uwierzytelniania. Powinien być odpowiednio obszerny i krótszy niż cykl życia challenge. Zalecana wartość domyślna to 5 minut, ale możesz ją zwiększyć do maksymalnie 10 minut, czyli nadal mieści się w zalecanym zakresie. Długie limity czasu są przydatne, jeśli spodziewasz się, że użytkownicy będą korzystać z hybrydowego przepływu pracy, który zwykle trwa nieco dłużej. W przypadku przekroczenia limitu czasu operacji jest zgłaszany błąd NotAllowedError.

Po utworzeniu zadania publicKeyCredentialRequestOptions wyślij go do klienta.

publicKeyCredentialCreationOptions wysłana przez serwer
Opcje wysłane przez serwer. Dekodowanie challenge odbywa się po stronie klienta.

Przykładowy kod: opcje żądania utworzenia danych logowania

W przykładach używamy biblioteki SimpleWebAuthn. Przekazujemy tutaj możliwość tworzenia opcji żądań danych logowania do funkcji generateAuthenticationOptions.

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

router.post('/signinRequest', csrfCheck, async (req, res) => {

  // Ensure you nest 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 {
    // Use the generateAuthenticationOptions function from SimpleWebAuthn
    const options = await generateAuthenticationOptions({
      rpID: process.env.HOSTNAME,
      allowCredentials: [],
    });
    // Save the challenge in the user session
    req.session.challenge = options.challenge;

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

Potwierdź i zaloguj użytkownika

Gdy usługa navigator.credentials.get zostanie pomyślnie rozwiązana na kliencie, zwraca obiekt PublicKeyCredential.

Obiekt PublicKeyCredential wysłany przez serwer
navigator.credentials.get zwraca wartość PublicKeyCredential.

response to AuthenticatorAssertionResponse. Reprezentuje odpowiedź dostawcy klucza dostępu na instrukcję klienta dotyczącą utworzenia elementów niezbędnych do uwierzytelnienia za pomocą klucza dostępu w RP. Zawiera ona:

  • response.authenticatorDataorazresponse.clientDataJSON, np. na etapie rejestracji klucza dostępu.
  • response.signature, który zawiera podpis nad tymi wartościami.

Wyślij obiekt PublicKeyCredential na serwer.

Na serwerze wykonaj te czynności:

Schemat bazy danych
Sugerowany schemat bazy danych. Więcej informacji o tym projekcie znajdziesz w artykule Rejestracja kluczy dostępu po stronie serwera.
  • Zbierz informacje potrzebne do zweryfikowania potwierdzenia i uwierzytelnienia użytkownika:
    • Wyświetlenie oczekiwanego testu zabezpieczającego zapisanego w sesji podczas generowania opcji uwierzytelniania.
    • Uzyskaj oczekiwany pochodzenie i identyfikator RP.
    • Znajdź w bazie danych tego użytkownika. W przypadku możliwych do znalezienia danych logowania nie wiadomo, kto jest użytkownikiem wysyłającym żądanie uwierzytelnienia. Możesz to sprawdzić na 2 sposoby:
      • Opcja 1. Użyj response.userHandle w obiekcie PublicKeyCredential. W tabeli Użytkownicy poszukaj wartości passkey_user_id odpowiadającej wartości userHandle.
      • Opcja 2. Użyj danych logowania id obecnych w obiekcie PublicKeyCredential. W tabeli Dane logowania klucza publicznego znajdź dane logowania id, które są zgodne z danymi id dostępnymi w obiekcie PublicKeyCredential. Następnie znajdź odpowiedniego użytkownika, używając klucza obcego passkey_user_id w tabeli Users (Użytkownicy).
    • Znajdź w swojej bazie danych informacje o danych logowania klucza publicznego zgodne z otrzymanym potwierdzeniem uwierzytelniania. Aby to zrobić, w tabeli Dane logowania klucza publicznego znajdź dane logowania id, które są zgodne z danymi iddanymi w obiekcie PublicKeyCredential.
  • Sprawdź potwierdzenie uwierzytelniania. Przekaż ten etap weryfikacji do biblioteki FIDO po stronie serwera, która zwykle oferuje funkcję narzędziową do tego celu. SimpleWebAuthn oferuje np. verifyAuthenticationResponse. Więcej informacji o tym, co dzieje się za maskami, znajdziesz w Załączniku: weryfikacja odpowiedzi na potrzeby uwierzytelniania.

  • Usuń test zabezpieczający bez względu na to, czy weryfikacja się uda, czy nie, aby zapobiec atakom metodą powtórzenia.

  • Zaloguj użytkownika. Jeśli weryfikacja się udała, zaktualizuj informacje o sesji, by oznaczyć użytkownika jako zalogowanego. Możesz też zwrócić klientowi obiekt user, aby frontend mógł użyć informacji powiązanych z nowo zalogowanym użytkownikiem.

Przykładowy kod: weryfikacja i logowanie użytkownika

W przykładach używamy biblioteki SimpleWebAuthn. W tym przypadku przekazujemy weryfikację odpowiedzi uwierzytelniania jej funkcji verifyAuthenticationResponse.

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

router.post('/signinResponse', csrfCheck, async (req, res) => {
  const response = req.body;
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;

  // 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 {
    // Find the credential stored to the database by the credential ID
    const cred = Credentials.findById(response.id);
    if (!cred) {
      throw new Error('Credential not found.');
    }
    // Find the user - Here alternatively we could look up the user directly
    // in the Users table via userHandle
    const user = Users.findByPasskeyUserId(cred.passkey_user_id);
    if (!user) {
      throw new Error('User not found.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      requireUserVerification: false,
    });

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

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

    req.session.username = user.username;
    req.session['signed-in'] = 'yes';

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

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

Załącznik: weryfikacja odpowiedzi na uwierzytelnianie

Weryfikacja odpowiedzi uwierzytelniania obejmuje te procesy:

  • Upewnij się, że identyfikator grupy objętej ograniczeniami jest zgodny z identyfikatorem Twojej witryny.
  • Upewnij się, że źródło żądania jest zgodne ze źródłem logowania w Twojej witrynie. W przypadku aplikacji na Androida przeczytaj artykuł Weryfikowanie pochodzenia.
  • Sprawdź, czy urządzenie było w stanie wyświetlić podane przez Ciebie wyzwanie.
  • Sprawdź, czy podczas uwierzytelniania użytkownik spełniał wymagania określone przez Ciebie jako RP. Jeśli wymagasz weryfikacji użytkownika, upewnij się, że flaga uv (zweryfikowane przez użytkownika) w systemie authenticatorData ma wartość true. Sprawdź, czy flaga up (obecny użytkownik) w systemie authenticatorData ma wartość true, ponieważ w przypadku kluczy dostępu obecność użytkownika jest zawsze wymagana.
  • Sprawdź podpis. Aby potwierdzić podpis, potrzebne będą:
    • Podpis, który jest podpisanym wyzwaniem: response.signature
    • klucz publiczny do weryfikacji podpisu,
    • Oryginalne podpisane dane. Są to dane, których podpis ma zostać zweryfikowany.
    • algorytm kryptograficzny użyty do utworzenia podpisu,
.

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