Passkeys in Delphi: passwortlose Anmeldung mit WebAuthn

· Komponenten
Passkeys in Delphi: passwortlose Anmeldung mit WebAuthn

Jedes Passwort, das Ihre Anwendung akzeptiert, kann erraten, auf einer anderen Seite wiederverwendet, in eine gefälschte Anmeldeseite eingegeben oder aus einem Datenbank-Backup geleakt werden. Passkeys beseitigen alle vier Probleme auf einmal. Der Benutzer meldet sich mit dem Fingerabdruck, dem Gesicht oder der PIN an, die ohnehin schon das Telefon oder den Laptop entsperrt, und auf Ihrem Server gibt es kein Geheimnis mehr, das sich zu stehlen lohnt.

Im Überblick über die neuen Login-Komponenten bekamen Passkeys fünf Stichpunkte, und der SAML-Beitrag behandelte die Anmeldung für Unternehmen mit eigenem Identity Provider. Dieser Beitrag ist die passwortlose Seite: wie TsgcWSAPIServer_WebAuthn Passkeys registriert, Benutzer ohne Benutzername anmeldet, Passkeys in der Autofill-Liste des Browsers anzeigt und jede Credential in Ihrer eigenen Datenbank speichert.

Was ein Passkey ist

Ein Passkey ist ein Schlüsselpaar, das vom Authenticator des Benutzers erzeugt wird: Windows Hello, iCloud-Schlüsselbund, Google Passwortmanager, ein Passwort-Manager oder ein FIDO2-Sicherheitsschlüssel. Der private Schlüssel verlässt den Authenticator nie. Ihr Server speichert nur den öffentlichen Schlüssel, sodass eine geleakte Passkey-Tabelle niemandem eine Anmeldung erlaubt. Jede Signatur ist an die Domain Ihrer Site gebunden, sodass eine ähnlich aussehende Phishing-Domain nichts erhält, das sie verwenden könnte. Genau das bedeutet hier phishing-resistent.

In WebAuthn-Begriffen ist ein Passkey eine erkennbare Credential, auch resident key genannt. Der Authenticator speichert das userHandle neben dem privaten Schlüssel, weshalb sich der Benutzer ohne Eingabe eines Benutzernamens anmelden kann und der Browser die Passkeys Ihrer Site selbstständig auflisten kann.

Erkennbare Credentials anfordern

WebAuthn benötigt einen sicheren Kontext, liefern Sie Ihre Seiten also über https aus, oder während der Entwicklung von localhost, und setzen Sie WebAuthnOptions.RelyingParty auf den Hostnamen, den der Browser anzeigt. Der residentKey, den die Registrierungsoptionen anfordern, stammt aus WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential:

Eine einzelne Registrierungsanfrage kann die Vorgabe weiterhin mit dem Feld discoverable_credential überschreiben, gesetzt auf required, preferred oder discouraged.

Anmeldung ohne Benutzername

Fordern Sie die Authentifizierungsoptionen mit leerem Benutzernamen an. Die Optionen enthalten dann keine allowCredentials-Liste, der Browser zeigt die Passkeys, die er für Ihre Relying Party besitzt, und der Benutzer wählt eine aus. Es gibt nichts einzutippen und nichts zu vertippen.

Weil der Server diesmal die Credential nicht ausgewählt hat, prüft er mehr. Die Antwort muss das userHandle enthalten, das userHandle muss zur Credential gehören, die signiert hat, und eine Credential, die Ihre Anwendung nicht kennt, wird abgelehnt.

Passkeys in der Autofill-Liste

Autofill, oder conditional mediation, setzt die Passkeys Ihrer Site in die Vorschlagsliste des Benutzernamensfelds, neben die gespeicherten Passwörter. Das ist der sanfte Weg, Benutzer umzustellen: Die Anmeldeseite funktioniert weiter für Personen mit Passwort, und Personen mit Passkey wählen ihn aus der Liste.

Autofill benötigt einen Browser mit conditional mediation, was heute aktuelle Versionen von Chrome, Edge und Safari bedeutet. browserSupportsWebAuthnAutofill() liefert in den anderen False zurück, halten Sie also zusätzlich einen “Mit Passkey anmelden”-Button auf der Seite bereit.

Mehrere Passkeys, ein userHandle

Reale Benutzer haben mehr als einen Passkey: einen auf dem Laptop, einen auf dem Telefon, vielleicht einen Sicherheitsschlüssel für den Tag, an dem beide verloren gehen. Registrieren Sie jeden von ihnen mit demselben Benutzernamen. Wenn dieser Benutzername bereits Credentials hat, die der Server kennt, verwendet die neue Registrierung deren userHandle (user.id) weiter, sodass sich jeder Passkey des Kontos ein einziges userHandle teilt und alle zum selben Benutzer führen.

Der Server kennt die Credentials, die während seiner Laufzeit registriert wurden, sowie die, die Sie mit AddCredential hinzufügen. Wenn Ihre Passkeys in einer Datenbank liegen, fügen Sie sie beim Start hinzu, damit jedes Konto ein einziges userHandle behält. Wenn ein Benutzer tatsächlich einen Benutzernamen eingibt, listen die Authentifizierungsoptionen jeden Passkey dieses Benutzers in allowCredentials auf, und der Authenticator verwendet den, den er besitzt.

Passkeys in Ihrer eigenen Datenbank speichern

Die Komponente persistiert keine Credentials. Ihre Passkeys gehören neben Ihre Benutzertabelle, und vier Events verbinden beides:

Einen Passkey zu speichern und wieder zu laden sind zwei kurze Handler:

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 und DBFindPasskey stehen für Ihren eigenen Datenzugriffscode. Der zurückgegebene Datensatz muss die angeforderte CredentialId tragen, und denselben Username, wenn die Ceremony mit einem Benutzernamen begann, sonst schlägt die Anmeldung fehl. Bewahren Sie auch die UserId im gespeicherten Datensatz auf, weil der Server sie mit dem vom Authenticator gesendeten userHandle vergleicht. Die Events laufen in den Verbindungs-Threads des Servers, schützen Sie gemeinsam genutzte Ressourcen also so, wie Sie sie überall sonst im Server schützen.

Synchronisiert oder gerätegebunden

Die Flags in den Authenticator-Daten verraten, welche Art von Passkey Sie erhalten haben, und der Credential-Datensatz speichert sie als BackupEligible (das BE-Flag) und BackupState (das BS-Flag):

Ihre Richtlinie kann sie unterschiedlich behandeln, etwa indem sie bei Administratorkonten nur gerätegebundene Sicherheitsschlüssel akzeptiert. Bei jeder Anmeldung lehnt der Server eine Antwort mit gesetztem BS-Flag und nicht gesetztem BE-Flag ab, sowie eine Antwort, deren BE-Flag vom gespeicherten BackupEligible abweicht, weil sich die Eligibility einer Credential nie ändert. Das neue BS-Flag wird in BackupState kopiert, und OnWebAuthnAuthenticationSuccessful ist der Ort, es zu speichern.

Der Signaturzähler und geklonte Authenticators

Manche Authenticators erhöhen bei jeder Anmeldung einen Signaturzähler. Wenn der Zähler in der Antwort oder der gespeicherte SignCount nicht null ist, muss der Zähler in der Antwort größer sein als der gespeicherte Wert. Ist er das nicht, wird die Anmeldung abgelehnt, weil zwei Authenticators, die mit demselben Schlüssel antworten, genau so aussehen wie ein geklonter Authenticator.

Die Prüfung funktioniert nur, wenn Sie den Zähler nach jeder Anmeldung speichern, und der gespeicherte Wert sich immer nur vorwärts bewegt. Synchronisierte Passkeys melden meist jedes Mal 0, was die Prüfung für sie abschaltet. Wenn Sie Authenticators haben, die den Zähler nicht zuverlässig erhöhen, setzen Sie WebAuthnOptions.AllowSignCountLessOrEqualStoredValue auf True, um sie zu akzeptieren. Der gespeicherte Wert wird trotzdem nie herabgesetzt.

Die Demo ausprobieren

Die Demo Demos\26.Authentication\01.Passkeys ist eine vollständige Relying Party: ein TsgcWebSocketHTTPServer mit angehängtem TsgcWSAPIServer_WebAuthn, der eine kleine Anmeldeseite unter https://localhost:5443 ausliefert. Sie speichert jeden Passkey in einer eigenen passkeys.json-Datei, über die vier oben genannten Events.

  1. Erstellen Sie die Demo und halten Sie libcrypto-3.dll und libssl-3.dll neben der ausführbaren Datei bereit. Sie befinden sich im Demo-Ordner.
  2. Belassen Sie Host 127.0.0.1, Port 5443 und Relying Party localhost, und klicken Sie dann auf Start.
  3. Klicken Sie auf Open Browser und akzeptieren Sie das selbstsignierte Testzertifikat.
  4. Geben Sie einen Benutzernamen ein und klicken Sie auf Register passkey. Registrieren Sie einen zweiten Passkey für denselben Benutzernamen. Das Formular listet jeden Passkey mit seinem Typ, synchronisiert oder gerätegebunden, und seinem Signaturzähler auf.
  5. Klicken Sie auf Sign in without user name und wählen Sie einen Passkey aus. Der Server findet ihn über OnWebAuthnAuthenticationGetCredential, und das Log zeigt den Benutzer.
  6. Laden Sie die Seite neu und klicken Sie in das Benutzernamensfeld. Die Passkeys erscheinen in der Autofill-Liste. Wählen Sie einen zur Anmeldung aus.
  7. Starten Sie die Anwendung neu. Die Passkeys werden wieder aus passkeys.json geladen, was zeigt, dass die Speicherung zu Ihrer Anwendung gehört und nicht zur Komponente.

Dokumentation

Wo Sie es bekommen

TsgcWSAPIServer_WebAuthn ist in den Editionen Enterprise und All-Access von sgcWebSockets enthalten, für Delphi und C++ Builder, und dieselbe Komponente ist Teil von sgcWebSockets .NET. Wenn Sie nur Authentifizierung benötigen, hat das sgcAuth-Paket sie zusammen mit den anderen Login-Komponenten. Sie finden sie auf der SGC-Auth-Palette, und in einer bestehenden Anwendung ändert sich nichts, bis Sie sie auf ein Formular ziehen.

Weiterlesen

Video ansehen

Es gibt ein kurzes Video, “Passkeys in Delphi: passwordless login with WebAuthn”, auf dem eSeGeCe-Kanal. Es zeigt den Code in der IDE, was die Backup-Flags und der Signaturzähler verraten, und die Demo, die unter https://localhost läuft: Ein Benutzer registriert zwei Passkeys und meldet sich dann an, ohne einen Benutzernamen einzutippen.

Fragen, Feedback oder Hilfe beim Hinzufügen von Passkeys zu Ihrer Anmeldeseite? Kontaktieren Sie uns. Sie erhalten eine Antwort von den Leuten, die den Code geschrieben haben.