Tworzenie klucza do logowania się bez hasła

Dzięki kluczom dostępu konta użytkowników są bezpieczniejsze i prostsze w obsłudze.

Używanie kluczy dostępu zamiast haseł to świetny sposób na to, aby witryny były bezpieczniejsze, prostsze w obsłudze i bez hasła. Dzięki kluczowi dostępu użytkownik może logować się na stronie lub w aplikacji przy użyciu odcisku palca, wizerunku twarzy lub kodu PIN urządzenia.

Aby użytkownik mógł się zalogować przy użyciu klucza dostępu, musi zostać utworzony klucz dostępu powiązany z kontem użytkownika. Klucz publiczny musi być przechowywany na Twoim serwerze.

Jak to działa

Użytkownik może otrzymać prośbę o utworzenie klucza dostępu w jednej z tych sytuacji:

  • Gdy użytkownik loguje się przy użyciu hasła.
  • Gdy użytkownik loguje się przy użyciu klucza z innego urządzenia (authenticatorAttachment to cross-platform).
  • Na specjalnej stronie, na której użytkownicy mogą zarządzać swoimi kluczami dostępu.

Aby utworzyć klucz dostępu, użyj interfejsu WebAuthn API.

4 komponenty procesu rejestracji klucza dostępu to:

  • Backend: serwer backendu, w którym znajduje się baza danych kont przechowująca klucz publiczny i inne metadane klucza dostępu.
  • Frontend: frontend, który komunikuje się z przeglądarką i wysyła żądania pobierania do backendu.
  • Przeglądarka: przeglądarka użytkownika, która używa Twojego JavaScriptu.
  • Authenticator: mechanizm uwierzytelniający użytkownika, który tworzy i przechowuje klucz dostępu. Może być na tym samym urządzeniu co przeglądarka (np. w Windows Hello) lub na innym urządzeniu, np. telefonie.
Schemat rejestracji klucza

Procedura dodawania nowego klucza do dotychczasowego konta użytkownika przebiega w ten sposób:

  1. Użytkownik loguje się w witrynie.
  2. Po zalogowaniu się użytkownik prosi o utworzenie klucza dostępu w interfejsie, na przykład naciskając przycisk „Utwórz klucz dostępu”.
  3. Frontend żąda od backendu informacji o utworzeniu klucza dostępu, takich jak informacje o użytkowniku, test zabezpieczający i identyfikatory danych logowania do wykluczenia.
  4. Frontend wywołuje polecenie navigator.credentials.create(), aby utworzyć klucz dostępu. To wywołanie zwraca obietnicę.
  5. Klucz jest tworzony, gdy użytkownik wyrazi zgodę przy użyciu blokady ekranu urządzenia. Obietnica zostanie zrealizowana, a dane uwierzytelniające klucza publicznego zostaną zwrócone do frontendu.
  6. Frontend wysyła do backendu dane uwierzytelniające klucza publicznego i przechowuje na potrzeby przyszłych uwierzytelniania identyfikator danych logowania i klucz publiczny powiązany z kontem użytkownika.

Zgodność

Większość przeglądarek obsługuje WebAuthn, ale występują drobne luki. Informacje o tym, która kombinacja przeglądarek i systemów operacyjnych obsługuje tworzenie klucza dostępu, znajdziesz w tej dokumentacji.

Utwórz nowy klucz dostępu

Oto jak powinien działać frontend w odpowiedzi na żądanie utworzenia nowego klucza dostępu.

Wykrywanie funkcji

Zanim wyświetli się przycisk „Utwórz nowy klucz dostępu”, sprawdź, czy:

  • Przeglądarka obsługuje WebAuthn.
  • Urządzenie obsługuje mechanizm uwierzytelniania platformy (może utworzyć klucz dostępu i uwierzytelnić go za pomocą klucza dostępu).
  • Przeglądarka obsługuje interfejs warunkowy WebAuthn.
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.  
// `​​isConditionalMediationAvailable` means the feature detection is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&  
    PublicKeyCredential.​​isConditionalMediationAvailable) {  
  // Check if user verifying platform authenticator is available.  
  Promise.all([  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),  
    PublicKeyCredential.​​isConditionalMediationAvailable(),  
  ]).then(results => {  
    if (results.every(r => r === true)) {  
      // Display "Create a new passkey" button  
    }  
  });  
}  

Dopóki nie zostaną spełnione wszystkie warunki, klucze dostępu nie będą obsługiwane w tej przeglądarce. Do tego czasu przycisk „Utwórz nowy klucz dostępu” nie powinien być widoczny.

Pobieraj ważne informacje z backendu

Gdy użytkownik kliknie przycisk, pobierze ważne informacje, aby wywołać navigator.credentials.create() z backendu:

  • challenge: wygenerowany przez serwer test w funkcji ArrayBuffer na potrzeby tej rejestracji. Jest ono wymagane, ale nie jest używane podczas rejestracji, chyba że przeprowadzasz atest – nie jest to zagadnienie zaawansowane, które nie zostało tu omówione.
  • user.id: unikalny identyfikator użytkownika. Ta wartość musi być wartością ArrayBuffer, która nie zawiera danych umożliwiających identyfikację, takich jak adresy e-mail czy nazwy użytkowników. Dobrze jest wygenerować losową wartość 16-bajtową wygenerowaną dla każdego konta.
  • user.name: w tym polu powinien znajdować się unikalny identyfikator konta rozpoznawany przez użytkownika, taki jak adres e-mail lub nazwa użytkownika. Ta opcja wyświetli się w selektorze kont. Jeśli używasz nazwy użytkownika, użyj tej samej wartości co przy uwierzytelnianiu hasła.
  • user.displayName: To pole jest wymagane i łatwiejsze w użyciu. Nie musi być unikalna i może być nazwą wybrana przez użytkownika. Jeśli Twoja witryna nie ma odpowiedniej wartości, podaj pusty ciąg znaków. W zależności od przeglądarki ta opcja może się wyświetlić w selektorze konta.
  • excludeCredentials: umożliwia zarejestrowanie tego samego urządzenia przez udostępnienie listy już zarejestrowanych identyfikatorów danych logowania. Element transports (jeśli został podany) powinien zawierać wynik wywołania getTransports() podczas rejestracji poszczególnych danych logowania.

Wywołaj interfejs WebAuthn API, aby utworzyć klucz dostępu

Zadzwoń pod numer navigator.credentials.create(), aby utworzyć nowy klucz dostępu. Interfejs API zwraca obietnicę w oczekiwaniu na interakcję użytkownika, wyświetlając okno modalne.

const publicKeyCredentialCreationOptions = {
  challenge: *****,
  rp: {
    name: "Example",
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },
  pubKeyCredParams: [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
};

const credential = await navigator.credentials.create({
  publicKey: publicKeyCredentialCreationOptions
});

// Encode and send the credential to the server for verification.  

Parametry, które nie zostały opisane powyżej:

  • rp.id: Identyfikator RP to domena, a witryna może określać swoją domenę lub sufiks dostępny do zarejestrowania. Jeśli na przykład źródłem grupy objętej ograniczeniami jest https://login.example.com:1337, identyfikatorem może być login.example.com lub example.com. Jeśli identyfikator RP jest określony jako example.com, użytkownik może uwierzytelniać się w login.example.com lub w dowolnej subdomenie example.com.

  • rp.name: Nazwa grupy objętej ograniczeniami.

  • pubKeyCredParams: To pole określa algorytmy klucza publicznego dla grupy objętej ograniczeniami. Zalecamy ustawienie go na [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. Określa to obsługę ECDSA z P-256 i RSA PKCS#1, a obsługa tych adresów zapewnia pełne pokrycie.

  • authenticatorSelection.authenticatorAttachment: Ustaw tę wartość na "platform", jeśli tworzenie klucza dostępu polega na uaktualnieniu z hasła, np. w promocji po zalogowaniu. "platform" wskazuje, że strona RP wymaga modułu uwierzytelniającego platformy (umieszczonego na platformie), który nie wyświetla prośby o podanie np. klucza bezpieczeństwa USB. Użytkownik ma prostszą opcję utworzenia klucza dostępu.

  • authenticatorSelection.requireResidentKey: Ustaw wartość logiczną „true” (prawda). Wykrywalne dane logowania (klucz rezydenta) przechowują informacje o użytkowniku w kluczu dostępu i umożliwiają im wybranie konta podczas uwierzytelniania.

  • authenticatorSelection.userVerification: Wskazuje, czy weryfikacja użytkownika przy użyciu blokady ekranu urządzenia to "required", "preferred" czy "discouraged". Wartość domyślna to "preferred", co oznacza, że mechanizm uwierzytelniania może pominąć weryfikację użytkownika. Ustaw tę wartość na "preferred" lub pomiń tę właściwość.

Wyślij zwrócone dane uwierzytelniające klucza publicznego do backendu

Gdy użytkownik wyrazi zgodę przy użyciu blokady ekranu urządzenia, zostanie utworzony klucz dostępu, a obietnica zostanie zrealizowana i zwróci do frontendu obiekt PublicKeyCredential.

Obietnica może zostać odrzucona z różnych powodów. Aby wyeliminować te błędy, sprawdź właściwość name obiektu Error:

  • InvalidStateError: klucz dostępu już znajduje się na urządzeniu. Użytkownik nie zobaczy okna o błędzie, a witryna nie powinna traktować tego jako błędu – użytkownik chce zarejestrować urządzenie lokalne – i tak właśnie jest.
  • NotAllowedError: użytkownik anulował operację.
  • Inne wyjątki: wystąpiło coś nieoczekiwanego. Przeglądarka wyświetli użytkownikowi okno z błędem.

Obiekt danych logowania klucza publicznego zawiera te właściwości:

  • id: zakodowany w formacie Base64URL identyfikator utworzonego klucza dostępu. Pomaga on przeglądarce określić w trakcie uwierzytelniania, czy zgodny klucz dostępu znajduje się na urządzeniu. Ta wartość musi być przechowywana w bazie danych w backendzie.
  • rawId: wersja identyfikatora danych logowania w formacie ArrayBuffer.
  • response.clientDataJSON: dane klienta zakodowane w formacie ArrayBuffer.
  • response.attestationObject: obiekt atestu zakodowany w formacie ArrayBuffer. Zawiera on ważne informacje, takie jak identyfikator RP, flagi i klucz publiczny.
  • authenticatorAttachment: Zwraca "platform", gdy te dane logowania zostaną utworzone na urządzeniu obsługującym klucze dostępu.
  • type: to pole ma zawsze wartość "public-key".

Jeśli do obsługi obiektu danych logowania klucza publicznego w backendzie używasz biblioteki, zalecamy wysłanie całego obiektu do backendu po jego częściowym zakodowaniu za pomocą base64url.

Zapisz dane logowania

Po otrzymaniu danych logowania klucza publicznego z backendu przekaż je do biblioteki FIDO w celu przetworzenia obiektu.

Informacje pobrane z tych danych możesz potem przechowywać w bazie danych do wykorzystania w przyszłości. Na liście poniżej znajdziesz typowe właściwości, które warto zapisać:

  • Identyfikator danych logowania (klucz podstawowy)
  • User-ID
  • Klucz publiczny

Dane logowania klucza publicznego zawierają też te informacje, które warto zapisać w bazie danych:

Informacje o uwierzytelnianiu użytkownika znajdziesz w artykule Logowanie się przy użyciu klucza dostępu w autouzupełnianiu formularza.

Zasoby