Passkeys w Delphi: logowanie bez hasła z WebAuthn

· Komponenty
Passkeys w Delphi: logowanie bez hasła z WebAuthn

Każde hasło, które akceptuje twoja aplikacja, można odgadnąć, użyć ponownie na innej stronie, wpisać na sfałszowanej stronie logowania albo wykraść z kopii zapasowej bazy danych. Passkeys eliminują wszystkie cztery problemy naraz. Użytkownik loguje się odciskiem palca, twarzą albo kodem PIN, który już odblokowuje telefon lub laptop, więc na twoim serwerze nie ma żadnego sekretu wartego kradzieży.

W przeglądzie nowych komponentów logowania passkeys dostały pięć punktów, a wpis o SAML omawiał logowanie dla firm prowadzących własnego dostawcę tożsamości. Ten wpis dotyczy strony bez hasła: jak TsgcWSAPIServer_WebAuthn rejestruje passkeys, loguje użytkowników bez nazwy użytkownika, pokazuje passkeys na liście autouzupełniania przeglądarki i przechowuje każdy credential we własnej bazie danych.

Czym jest passkey

Passkey to para kluczy utworzona przez authenticator użytkownika: Windows Hello, pęk kluczy iCloud, Menedżer haseł Google, menedżer haseł albo klucz bezpieczeństwa FIDO2. Klucz prywatny nigdy nie opuszcza authenticatora. Twój serwer przechowuje tylko klucz publiczny, więc wyciek tabeli passkeys nie pozwala nikomu się zalogować. Każdy podpis jest powiązany z domeną twojej witryny, więc podobna domena phishingowa nie zdobywa niczego, co dałoby się wykorzystać. Właśnie to oznacza tu odporność na phishing.

W terminologii WebAuthn passkey jest wykrywalnym credentialem, nazywanym też resident key. Authenticator przechowuje userHandle obok klucza prywatnego, dlatego użytkownik może się zalogować bez wpisywania nazwy użytkownika, a przeglądarka może samodzielnie wylistować passkeys twojej witryny.

Żądanie wykrywalnych credentiali

WebAuthn wymaga bezpiecznego kontekstu, więc serwuj swoje strony przez https albo z localhost podczas prac programistycznych, i ustaw WebAuthnOptions.RelyingParty na nazwę hosta pokazywaną przez przeglądarkę. residentKey, o który proszą opcje rejestracji, pochodzi z WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential:

Pojedyncze żądanie rejestracji może nadal nadpisać wartość domyślną polem discoverable_credential, ustawionym na required, preferred albo discouraged.

Logowanie bez nazwy użytkownika

Zażądaj opcji uwierzytelniania z pustą nazwą użytkownika. Opcje nie niosą wtedy żadnej listy allowCredentials, przeglądarka pokazuje passkeys, które ma dla twojego relying party, a użytkownik wybiera jeden z nich. Nie ma nic do wpisania, więc nie ma jak się pomylić.

Ponieważ tym razem to nie serwer wybrał credential, sprawdza więcej. Odpowiedź musi zawierać userHandle, userHandle musi należeć do credentiala, który złożył podpis, a credential, którego twoja aplikacja nie zna, zostaje odrzucony.

Passkeys na liście autouzupełniania

Autouzupełnianie, czyli conditional mediation, umieszcza passkeys twojej witryny na liście podpowiedzi pola nazwy użytkownika, obok zapisanych haseł. To łagodny sposób na przeniesienie użytkowników: strona logowania nadal działa dla osób z hasłem, a osoby z passkey wybierają go z listy.

Autouzupełnianie wymaga przeglądarki z obsługą conditional mediation, co dziś oznacza aktualne wersje Chrome, Edge i Safari. browserSupportsWebAuthnAutofill() zwraca w pozostałych False, więc trzymaj na stronie także przycisk „Zaloguj się passkeyem”.

Kilka passkeys, jeden userHandle

Prawdziwi użytkownicy mają więcej niż jeden passkey: jeden na laptopie, jeden na telefonie, może klucz bezpieczeństwa na dzień, w którym oba zginą. Zarejestruj każdy z nich pod tą samą nazwą użytkownika. Gdy ta nazwa użytkownika ma już credentiale znane serwerowi, nowa rejestracja ponownie wykorzystuje ich userHandle (user.id), więc każdy passkey konta dzieli jeden userHandle i wszystkie prowadzą do tego samego użytkownika.

Serwer zna credentiale zarejestrowane podczas swojego działania oraz te, które dodajesz przez AddCredential. Gdy twoje passkeys żyją w bazie danych, dodaj je przy starcie, aby każde konto zachowało jeden userHandle. Gdy użytkownik rzeczywiście wpisze nazwę użytkownika, opcje uwierzytelniania wylistują każdy passkey tego użytkownika w allowCredentials, a authenticator użyje tego, który posiada.

Przechowuj passkeys we własnej bazie danych

Komponent nie utrwala credentiali. Twoje passkeys należą obok tabeli użytkowników, a łączą je cztery zdarzenia:

Zapis passkeya i jego ponowne wczytanie to dwa krótkie handlery:

uses
  sgcWebAuthn_Classes;

// registration: store the whole record, keyed by its credential id
procedure TForm1.WebAuthnWebAuthnRegistrationSuccessful(Sender: TObject;
  const aRegistration: TsgcWebAuthn_Registration;
  const aCredentialRecord: TsgcWebAuthn_CredentialRecord; var Accept: Boolean);
begin
  DBInsertPasskey(aCredentialRecord.CredentialId, aCredentialRecord.Username,
    aCredentialRecord.AsJSON);
end;

// usernameless and autofill sign-in: the browser chose the passkey,
// find it by its credential id and hand it back to the server
procedure TForm1.WebAuthnWebAuthnAuthenticationGetCredential(Sender: TObject;
  const aCredentialId: string;
  const aCredentialRecord: TsgcWebAuthn_CredentialRecord; var Found: Boolean);
var
  vJSON: string;
begin
  Found := DBFindPasskey(aCredentialId, vJSON);
  if Found then
    aCredentialRecord.ReadJSON(vJSON);
end;

DBInsertPasskey i DBFindPasskey reprezentują twój własny kod dostępu do danych. Zwracany rekord musi nieść żądany CredentialId oraz tę samą Username, gdy ceremony zaczęła się od nazwy użytkownika, w przeciwnym razie logowanie się nie powiedzie. Przechowuj też UserId w zapisanym rekordzie, ponieważ serwer porównuje go z userHandle wysyłanym przez authenticator. Zdarzenia działają w wątkach połączeń serwera, więc chroń współdzielone zasoby tak, jak chronisz je w każdym innym miejscu serwera.

Synchronizowane czy powiązane z urządzeniem

Flagi w danych authenticatora mówią, jaki typ passkeya otrzymałeś, a rekord credentiala przechowuje je jako BackupEligible (flaga BE) i BackupState (flaga BS):

Twoja polityka może traktować je odmiennie, na przykład akceptując na kontach administratorów wyłącznie klucze bezpieczeństwa powiązane z urządzeniem. Przy każdym logowaniu serwer odrzuca odpowiedź z ustawioną flagą BS i wyczyszczoną flagą BE, a także odpowiedź, której flaga BE różni się od zapisanego BackupEligible, ponieważ eligibility credentiala nigdy się nie zmienia. Nowa flaga BS jest kopiowana do BackupState, a OnWebAuthnAuthenticationSuccessful to miejsce, gdzie należy ją zapisać.

Licznik podpisów i sklonowane authenticatory

Niektóre authenticatory zwiększają licznik podpisów przy każdym logowaniu. Gdy licznik w odpowiedzi lub zapisany SignCount nie jest zerem, licznik w odpowiedzi musi być większy od zapisanej wartości. Jeśli nie jest, logowanie zostaje odrzucone, ponieważ dwa authenticatory odpowiadające tym samym kluczem to dokładnie to, jak wygląda sklonowany authenticator.

Ta kontrola działa tylko wtedy, gdy zapisujesz licznik po każdym logowaniu, a zapisana wartość tylko rośnie. Synchronizowane passkeys zwykle za każdym razem zgłaszają 0, co wyłącza dla nich tę kontrolę. Jeśli masz authenticatory, które nie zwiększają licznika w niezawodny sposób, ustaw WebAuthnOptions.AllowSignCountLessOrEqualStoredValue na True, aby je akceptować. Zapisana wartość nadal nigdy nie jest obniżana.

Wypróbuj demo

Demo Demos\26.Authentication\01.Passkeys to kompletny relying party: TsgcWebSocketHTTPServer z podłączonym TsgcWSAPIServer_WebAuthn, serwujący niewielką stronę logowania pod adresem https://localhost:5443. Przechowuje każdy passkey we własnym pliku passkeys.json, za pomocą czterech powyższych zdarzeń.

  1. Skompiluj demo i trzymaj libcrypto-3.dll oraz libssl-3.dll obok pliku wykonywalnego. Znajdziesz je w folderze demo.
  2. Zostaw host 127.0.0.1, port 5443 i relying party localhost, a następnie kliknij Start.
  3. Kliknij Open Browser i zaakceptuj samopodpisany certyfikat testowy.
  4. Wpisz nazwę użytkownika i kliknij Register passkey. Zarejestruj drugi passkey dla tej samej nazwy użytkownika. Formularz wylistuje każdy passkey z jego typem, synchronizowany albo powiązany z urządzeniem, oraz jego licznikiem podpisów.
  5. Kliknij Sign in without user name i wybierz passkey. Serwer znajduje go przez OnWebAuthnAuthenticationGetCredential, a log pokazuje użytkownika.
  6. Przeładuj stronę i kliknij pole nazwy użytkownika. Passkeys pojawiają się na liście autouzupełniania. Wybierz jeden, aby się zalogować.
  7. Zrestartuj aplikację. Passkeys wczytują się ponownie z passkeys.json, co pokazuje, że przechowywanie należy do twojej aplikacji, a nie do komponentu.

Dokumentacja

Gdzie go zdobyć

TsgcWSAPIServer_WebAuthn jest częścią edycji Enterprise i All-Access sgcWebSockets, dla Delphi i C++ Builder, a ten sam komponent jest częścią sgcWebSockets .NET. Jeśli potrzebujesz tylko uwierzytelniania, pakiet sgcAuth zawiera go razem z pozostałymi komponentami logowania. Znajdziesz go na palecie SGC Auth, a w istniejącej aplikacji nic się nie zmienia, dopóki nie przeciągniesz go na formularz.

Czytaj dalej

Zobacz wideo

Na kanale eSeGeCe jest krótkie wideo, „Passkeys in Delphi: passwordless login with WebAuthn”, na kanale eSeGeCe. Pokazuje kod w IDE, co mówią flagi kopii zapasowej i licznik podpisów, oraz demo działające pod https://localhost: użytkownik rejestruje dwa passkeys, a następnie loguje się bez wpisywania nazwy użytkownika.

Masz pytania, uwagi albo potrzebujesz pomocy przy dodawaniu passkeys do swojej strony logowania? Skontaktuj się z nami. Odpowiedź otrzymasz od osób, które napisały ten kod.