Passkeys

Passkeys ersetzen Passwörter durch ein vom Authenticator des Benutzers erstelltes Schlüsselpaar: Windows Hello, iCloud Keychain, Google Password Manager, einen Passwort-Manager oder einen FIDO2-Security-Key. Der private Schlüssel verlässt nie den Authenticator, der Server speichert nur den öffentlichen Schlüssel, und die Signatur ist an die Domäne der Site gebunden, sodass Passkeys nicht per Phishing erlangt, wiederverwendet oder aus der Serverdatenbank geleakt werden können.

Technisch ist ein Passkey ein discoverable WebAuthn credential (ein Resident Key): Der Authenticator hält das User Handle neben dem privaten Schlüssel, sodass sich der Benutzer ohne Eingabe eines Benutzernamens anmelden kann, und der Browser kann die Passkeys der Site in der Autofill-Liste des Benutzernamenfelds anbieten. Der TsgcWSAPIServer_WebAuthn-Server unterstützt Passkeys mit den unten beschriebenen Einstellungen und Ereignissen.

Availability

Der WebAuthn-Server ist in sgcWebSockets Enterprise und im sgcAuth-Pack enthalten. WebAuthn benötigt einen sicheren Kontext: Stellen Sie die Seiten über https (oder localhost) bereit, und setzen Sie WebAuthnOptions.RelyingParty auf den im Browser angezeigten Hostnamen.

Discoverable Credentials registrieren

Die Eigenschaft WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential legt den in den Registrierungsoptionen angeforderten residentKey fest:

WertresidentKeyMeaning
waundcPreferred (default)preferredDer Authenticator erstellt ein discoverable Credential, wenn er kann.
waundcRequiredrequiredEs werden nur discoverable Credentials akzeptiert. Verwenden Sie es für Passkeys und Anmeldungen ohne Benutzernamen.
waundcDiscourageddiscouragedDer Authenticator soll ein serverseitiges Credential erstellen, das zur Anmeldung den Benutzernamen benötigt.

Eine Registrierungsanfrage kann den Standardwert mit dem Feld discoverable_credential ("required", "preferred" oder "discouraged") überschreiben. Die Einstellung wird nur angewendet, wenn die Anfrage nicht selbst einen bestimmten residentKey anfordert.


WebAuthn.WebAuthnOptions.RelyingParty := 'www.example.com';
// ... passkeys are discoverable credentials: require a resident key
WebAuthn.WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential :=
  waundcRequired;
WebAuthn.WebAuthnOptions.DefaultOptions.Registration.UserVerification :=
  waunuvPreferred;
WebAuthn.WebAuthnOptions.DefaultOptions.Authentication.UserVerification :=
  waunuvPreferred;

Mehrere Passkeys pro Benutzer

Ein Benutzer hat üblicherweise mehrere Passkeys, zum Beispiel einen auf dem Computer und einen auf dem Telefon. Registrieren Sie jeden davon mit demselben Benutzernamen: Wenn für den Benutzernamen bereits dem Server bekannte Credentials existieren, verwendet die Registrierung deren User Handle (user.id) erneut, sodass jeder Passkey des Kontos ein gemeinsames User Handle nutzt. Der Server kennt die während seiner Laufzeit registrierten Credentials sowie die mit AddCredential hinzugefügten, wenn Ihre Credentials also in einer Datenbank liegen, fügen Sie sie beim Start hinzu, um pro Konto ein einziges User Handle beizubehalten. Mit einem Benutzernamen enthalten die Authentifizierungsoptionen jeden Passkey des Benutzers in allowCredentials.

Anmeldung ohne Benutzernamen

Fordern Sie die Authentifizierungsoptionen mit einem leeren Benutzernamen an. Die Optionen enthalten dann kein allowCredentials, der Browser zeigt die für den Relying Party vorhandenen Passkeys, und der Benutzer wählt einen aus. Der Server verlangt das userHandle der Antwort und prüft, dass es zum verwendeten Credential gehört, und ein der Anwendung unbekanntes Credential wird abgelehnt.


// ... "Sign in with a passkey" button: the browser lists the passkeys of the site
const options = await post("/sgcWebAuthn/Authentication/Options", {username: ""});
const assertion = await SimpleWebAuthnBrowser.startAuthentication(options);
const result = await post("/sgcWebAuthn/Authentication/Verify", assertion);

Passkey-Autofill

Mit Conditional Mediation erscheinen die Passkeys in der Autofill-Liste des Benutzernamenfelds, neben den gespeicherten Passwörtern. Fügen Sie autocomplete="username webauthn" zum Eingabefeld hinzu, laden Sie /sgcWebAuthn.js (von der Komponente bereitgestellt, Endpunkt EndpointsOptions.WebAuthn) und rufen Sie beim Laden der Seite startAuthentication(options, true) mit Optionen ohne Benutzernamen auf. Das Promise wird aufgelöst, wenn der Benutzer einen Passkey auswählt.


<script src="/sgcWebAuthn.js"></script>
<input id="username" name="username" autocomplete="username webauthn">
<script>
async function post(url, body) {
  const r = await fetch(url, {method: "POST",
    headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)});
  return r.json();
}
async function passkeyAutofill() {
  if (!(await SimpleWebAuthnBrowser.browserSupportsWebAuthnAutofill())) return;
  // ... no user name: the options carry no allowCredentials
  const options = await post("/sgcWebAuthn/Authentication/Options", {username: ""});
  // ... true: conditional mediation, the passkeys appear in the autofill list
  const assertion = await SimpleWebAuthnBrowser.startAuthentication(options, true);
  const result = await post("/sgcWebAuthn/Authentication/Verify", assertion);
  if (result.verified) { location.href = "/"; }
}
passkeyAutofill();
</script>

Autofill erfordert einen Browser, der Conditional Mediation unterstützt (aktuelle Versionen von Chrome, Edge und Safari). browserSupportsWebAuthnAutofill() gibt in den anderen Browsern False zurück, behalten Sie für diese eine Schaltfläche "Sign in with a passkey" bei.

Speichern Sie die Passkeys in Ihrer eigenen Datenbank

Die Komponente speichert Credentials nicht dauerhaft. Vier Ereignisse verbinden sie mit Ihrem Speicher:

EreignisWas zu tun ist
OnWebAuthnRegistrationSuccessfulSpeichern Sie aCredentialRecord, zum Beispiel aCredentialRecord.AsJSON, zusammen mit seiner CredentialId und seinem Username.
OnWebAuthnAuthenticationOptionsRequestFügen Sie bei einem Benutzernamen die Passkeys dieses Benutzers zu CredentialRecords hinzu. Ohne Benutzernamen fügen Sie nichts hinzu.
OnWebAuthnAuthenticationGetCredentialWird ausgelöst, wenn das vom Browser verwendete Credential nicht in der Credential-Liste der Zeremonie enthalten ist, was bei einer Anmeldung ohne Benutzernamen oder per Autofill der Fall ist. Suchen Sie die Credential-ID, füllen Sie aCredentialRecord und setzen Sie Found auf True.
OnWebAuthnAuthenticationSuccessfulSpeichern Sie den aktualisierten SignCount und BackupState von aAuthentication.Credential.CredentialRecord und erstellen Sie die Sitzung.

// ... 1. registration: store the new passkey (the whole record as JSON)
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;

// ... 2. sign in with a user name: the passkeys of that user
procedure TForm1.WebAuthnWebAuthnAuthenticationOptionsRequest(Sender: TObject;
  const aRequest: TsgcWebAuthn_AuthenticationOptions_Request;
  var CredentialRecords: TsgcWebAuthn_CredentialRecords; var Accept: Boolean);
var
  i: Integer;
  oList: TStringList;
begin
  if aRequest.Username = '' then
    Exit; // ... usernameless or autofill: no allowCredentials
  oList := TStringList.Create;
  try
    DBGetPasskeysOfUser(aRequest.Username, oList); // ... one JSON per line
    for i := 0 to oList.Count - 1 do
      CredentialRecords.AddCredentialRecordFromJSON(oList[i]);
    Accept := oList.Count > 0;
  finally
    oList.Free;
  end;
end;

// ... 3. usernameless and autofill: find the passkey by its credential id
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;

// ... 4. signed in: save the new signature counter and backup state
procedure TForm1.WebAuthnWebAuthnAuthenticationSuccessful(Sender: TObject;
  const aRequest: TsgcWebAuthn_AuthenticationVerify_Request;
  const aAuthentication: TsgcWebAuthn_Authentication; var Accept: Boolean);
var
  oRecord: TsgcWebAuthn_CredentialRecord;
begin
  oRecord := aAuthentication.Credential.CredentialRecord;
  DBUpdatePasskey(oRecord.CredentialId, oRecord.AsJSON);
  CreateUserSession(oRecord.Username);
end;

Der von OnWebAuthnAuthenticationGetCredential zurückgegebene Datensatz muss die angeforderte CredentialId haben, und denselben Username, wenn die Zeremonie mit einem Benutzernamen gestartet wurde, andernfalls schlägt die Anmeldung fehl. Behalten Sie die UserId (User Handle) im gespeicherten Datensatz, sie wird mit der vom Authenticator gesendeten userHandle verglichen. Die Ereignisse laufen in den Server-Verbindungsthreads, schützen Sie gemeinsam genutzte Ressourcen entsprechend.

Synced oder gerätegebunden

Die Flags der Authenticator-Daten geben an, welche Art von Passkey der Server erhalten hat. Sie werden im Credential-Datensatz als BackupEligible (BE-Flag) und BackupState (BS-Flag) gespeichert:

BackupEligibleBackupStateMeaning
TrueTrueSynced Passkey, von der Plattform oder dem Passwort-Manager gesichert und auf den anderen Geräten des Benutzers verfügbar.
TrueFalseMulti-Device-Passkey, der noch nicht gesichert ist.
FalseFalseGerätegebundener Passkey, zum Beispiel ein Security Key oder ein TPM-Schlüssel, der das Gerät nie verlässt. Empfehlen Sie dem Benutzer, einen zweiten Passkey zu registrieren.

function GetPasskeyKind(aRecord: TsgcWebAuthn_CredentialRecord): string;
begin
  if aRecord.BackupEligible and aRecord.BackupState then
    Result := 'synced passkey'
  else if aRecord.BackupEligible then
    Result := 'multi-device passkey, not backed up yet'
  else
    Result := 'device-bound passkey';
end;

Bei jeder Anmeldung lehnt der Server eine Antwort ab, bei der das BS-Flag gesetzt, aber das BE-Flag nicht gesetzt ist, sowie eine Antwort, deren BE-Flag von der gespeicherten BackupEligible abweicht, da sich die Eignung eines Credentials nie ändert. Das neue BS-Flag wird nach BackupState kopiert, speichern Sie es in OnWebAuthnAuthenticationSuccessful.

Signaturzähler und geklonte Authenticators

Manche Authenticators erhöhen bei jeder Anmeldung einen Signaturzähler. Wenn der Zähler der Antwort oder der gespeicherte SignCount nicht null ist, muss der Zähler der Antwort größer als der gespeicherte Wert sein, andernfalls wird die Anmeldung abgelehnt, da der Authenticator geklont worden sein könnte. Der gespeicherte Wert bewegt sich nur vorwärts und muss nach jeder Anmeldung gespeichert werden, damit die Erkennung funktioniert. Synced Passkeys melden üblicherweise immer 0, wodurch die Prüfung für sie deaktiviert wird. Setzen Sie WebAuthnOptions.AllowSignCountLessOrEqualStoredValue auf True, um einen Zähler zu akzeptieren, der nicht ansteigt (der gespeicherte Wert wird dennoch nie verringert).

Demo

Die Demo Demos\26.Authentication\01.Passkeys registriert mehrere Passkeys pro Benutzer, meldet sich ohne Benutzernamen und per Autofill an, zeigt, ob jeder Passkey synced oder gerätegebunden ist, und speichert die Credentials über die obigen Ereignisse in einer JSON-Datei der Anwendung. Siehe auch WebAuthn und den JavaScript-Client.