Uwierzytelnianie klucza po stronie serwera

Przegląd

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

Proces uwierzytelniania klucza dostępu

  • Określ test zabezpieczający logowanie 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 (w przeglądarce navigator.credentials.get). 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 na temat każdego kroku.

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. Dopilnuj, aby to samo wyzwanie nie było nigdy używane więcej niż raz. Generuj nowe wyzwanie przy każdej próbie logowania. Odrzuć test po każdej próbie logowania, niezależnie od tego, czy się udała czy nie. Odrzuć wyzwanie również po określonym czasie. Nigdy nie podejmuj tego samego wyzwania w odpowiedzi więcej niż raz.
  2. Upewnij się, że test zabezpieczający jest zabezpieczony kryptograficznie. Zadanie powinno być praktycznie niemożliwe do odgadnięcia. Do utworzenia kryptograficznie bezpiecznej testów zabezpieczających po stronie serwera najlepiej polegać zaufana biblioteka FIDO. Jeśli tworzysz własne wyzwania, skorzystaj z wbudowanych funkcji kryptograficznych dostępnych w Twoim stosie technologicznym lub poszukaj bibliotek przeznaczonych do zastosowań kryptograficznych. Przykładem może być iso-crypto w Node.js i secrets w Pythonie. Zgodnie ze specyfikacją wyzwanie musi mieć co najmniej 16 bajtów, aby zostało uznane za bezpieczne.

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

Utwórz opcje żądania danych logowania

Utwórz opcje żądania danych logowania w postaci obiektu publicKeyCredentialRequestOptions.

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

Plik publicKeyCredentialRequestOptions powinien zawierać wszystkie informacje wymagane do uwierzytelniania kluczem. Przekaż te informacje do funkcji w bibliotece po stronie serwera FIDO, która odpowiada za utworzenie obiektu publicKeyCredentialRequestOptions.

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

  • rpId: identyfikator RP, z którym mają być powiązane dane logowania, np. example.com. Uwierzytelnianie uda się tylko wtedy, gdy podany tutaj identyfikator RP będzie zgodny z identyfikatorem RP powiązanym z danymi logowania. Aby wypełnić identyfikator RP, użyj tej samej wartości co identyfikator RP ustawiony w publicKeyCredentialCreationOptions podczas rejestracji danych logowania.
  • challenge: dane, które dostawca klucza dostępu podpisuje, aby udowodnić, że użytkownik posiada klucz dostępu w momencie żądania uwierzytelnienia. Sprawdź szczegóły w sekcji Tworzenie wyzwania.
  • allowCredentials: tablica akceptowanych danych uwierzytelniających 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 Wykrywalne dane logowania.
  • userVerification: wskazuje, czy weryfikacja użytkownika przy użyciu blokady ekranu urządzenia jest „wymagana”, „preferowana” czy „odradzana”. Przeczytaj artykuł Pobieranie wyzwania z serwera RP.
  • timeout: czas (w milisekundach) potrzebny użytkownikowi na uwierzytelnienie. Kwota powinna być wystarczająca i krótsza niż w okresie challenge. Zalecana wartość domyślna to 5 minut, ale możesz ją wydłużyć – do 10 minut, co mieści się w zalecanym zakresie. Długie limity czasu sprawdzają się, jeśli spodziewasz się, że użytkownicy będą korzystać z przepływu pracy hybrydowego, który zwykle zajmuje trochę więcej czasu. Jeśli operacja przekroczy limit czasu, zostanie zgłoszony NotAllowedError.

Po utworzeniu pliku publicKeyCredentialRequestOptions wyślij go do klienta.

Parametr publicKeyCredentialCreationOptions wysyłany przez serwer
Opcje wysyłane przez serwer. Dekodowanie challenge odbywa się po stronie klienta.

Przykładowy kod: tworzenie opcji żądania danych logowania

W naszych przykładach używamy biblioteki SimpleWebAuthn. Tutaj przekazujemy opcje tworzenia żą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 });
  }
});

Weryfikacja i logowanie użytkownika

Jeśli navigator.credentials.get zakończy się pomyślnie po stronie klienta, zwróci 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 przeprowadzenia próby uwierzytelnienia za pomocą klucza dostępu w RP. Zawiera ona:

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

Wyślij obiekt PublicKeyCredential do serwera.

Na serwerze wykonaj te czynności:

Schemat bazy danych
Sugerowany schemat bazy danych. Więcej informacji o tym projekcie znajdziesz w artykule o rejestrowaniu kluczy dostępu po stronie serwera.
  • Zbierz informacje potrzebne do zweryfikowania twierdzenia i uwierzytelnienia użytkownika:
    • uzyskać oczekiwany test zapisany w sesji podczas generowania opcji uwierzytelniania;
    • Uzyskaj oczekiwaną wartość origin i identyfikator RP.
    • Sprawdź w bazie danych, kto jest użytkownikiem. W przypadku możliwych do znalezienia danych logowania nie wiadomo, kto wysyła żądanie uwierzytelniania. Możesz to sprawdzić na 2 sposoby:
      • Opcja 1. Użyj response.userHandle w obiekcie PublicKeyCredential. W tabeli Użytkownicy znajdź passkey_user_id z wartością userHandle.
      • Opcja 2. Użyj danych logowania id znajdujących się w obiekcie PublicKeyCredential. W tabeli Dane logowania klucza publicznego znajdź dane logowania id, które odpowiadają danym logowania id w obiekcie PublicKeyCredential. Następnie wyszukaj odpowiedniego użytkownika za pomocą klucza obcego passkey_user_id do tabeli Użytkownicy.
    • Znajdź w swojej bazie danych dane logowania klucza publicznego, które pasują do otrzymanego potwierdzenia uwierzytelniania. Aby to zrobić, w tabeli Dane logowania klucza publicznego znajdź dane logowania id zgodne z danymi logowania idwidocznymi w obiekcie PublicKeyCredential.
  • Zweryfikuj potwierdzenie uwierzytelniania. Przekaż ten etap weryfikacji do swojej biblioteki po stronie serwera FIDO, która zwykle udostępnia do tego celu funkcję narzędzia. Oferta SimpleWebAuthn, to na przykład verifyAuthenticationResponse. Więcej informacji znajdziesz w artykule Dodatek: weryfikacja odpowiedzi uwierzytelniającej.

  • Usuń test niezależnie od tego, czy weryfikacja się udała, czy nie, aby zapobiec atakom metodą powtórzenia.

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

Przykładowy kod: weryfikowanie i logowanie użytkownika

W naszych przykładach używamy biblioteki SimpleWebAuthn. Tutaj przekazujemy weryfikację odpowiedzi uwierzytelniającej 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 });
  }
});

Dodatek: weryfikacja odpowiedzi uwierzytelniającej

Weryfikacja odpowiedzi uwierzytelniającej obejmuje następujące kontrole:

  • Upewnij się, że identyfikator RP jest zgodny z identyfikatorem w witrynie.
  • Upewnij się, że źródło żądania jest zgodne z miejscem logowania w Twojej witrynie. W przypadku aplikacji na Androida zapoznaj się z artykułem Weryfikowanie pochodzenia.
  • Sprawdź, czy urządzenie mogło wykonać zadanie.
  • Sprawdź, czy podczas uwierzytelniania użytkownik spełnia wymagania określone przez Ciebie jako grupę z ograniczonym dostępem. Jeśli wymagasz weryfikacji użytkownika, upewnij się, że flaga uv (zweryfikowany przez użytkownika) w authenticatorData ma wartość true. Sprawdź, czy flaga up (obecny użytkownik) w authenticatorData ma wartość true, ponieważ obecność użytkownika jest zawsze wymagana w przypadku kluczy dostępu.
  • Sprawdź podpis. Aby zweryfikować podpis, musisz mieć:
    • Podpis, który jest podpisanym wyzwaniem: response.signature
    • Klucz publiczny do weryfikacji podpisu.
    • Pierwotnie podpisane dane. To są dane, których podpis ma zostać zweryfikowany.
    • Algorytm kryptograficzny, który został użyty do utworzenia podpisu.

Aby dowiedzieć się więcej o tych czynnościach, zajrzyj do kodu źródłowego aplikacji SimpleWebAuthn w domenie verifyAuthenticationResponse lub zapoznaj się z pełną listą weryfikacji w specyfikacji.