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.
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.
Die Eigenschaft WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential legt den in den Registrierungsoptionen angeforderten residentKey fest:
| Wert | residentKey | Meaning |
|---|---|---|
waundcPreferred (default) | preferred | Der Authenticator erstellt ein discoverable Credential, wenn er kann. |
waundcRequired | required | Es werden nur discoverable Credentials akzeptiert. Verwenden Sie es für Passkeys und Anmeldungen ohne Benutzernamen. |
waundcDiscouraged | discouraged | Der 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;
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.
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);
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.
Die Komponente speichert Credentials nicht dauerhaft. Vier Ereignisse verbinden sie mit Ihrem Speicher:
| Ereignis | Was zu tun ist |
|---|---|
| OnWebAuthnRegistrationSuccessful | Speichern Sie aCredentialRecord, zum Beispiel aCredentialRecord.AsJSON, zusammen mit seiner CredentialId und seinem Username. |
| OnWebAuthnAuthenticationOptionsRequest | Fügen Sie bei einem Benutzernamen die Passkeys dieses Benutzers zu CredentialRecords hinzu. Ohne Benutzernamen fügen Sie nichts hinzu. |
| OnWebAuthnAuthenticationGetCredential | Wird 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. |
| OnWebAuthnAuthenticationSuccessful | Speichern 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.
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:
| BackupEligible | BackupState | Meaning |
|---|---|---|
| True | True | Synced Passkey, von der Plattform oder dem Passwort-Manager gesichert und auf den anderen Geräten des Benutzers verfügbar. |
| True | False | Multi-Device-Passkey, der noch nicht gesichert ist. |
| False | False | Gerä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.
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).
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.