Passkeys vervangen wachtwoorden door een sleutelpaar dat wordt aangemaakt door de authenticator van de gebruiker: Windows Hello, iCloud Keychain, Google Password Manager, een wachtwoordmanager of een FIDO2-beveiligingssleutel. De privésleutel verlaat de authenticator nooit, de server slaat alleen de publieke sleutel op, en de handtekening is gebonden aan het domein van de site, zodat passkeys niet kunnen worden gephisht, hergebruikt of uit de serverdatabase gelekt.
Technisch gezien is een passkey een discoverable WebAuthn credential (een resident key): de authenticator houdt de user handle naast de privésleutel, zodat de gebruiker zich kan aanmelden zonder een gebruikersnaam te typen, en de browser kan de passkeys van de site aanbieden in de autofill-lijst van het gebruikersnaamveld. De TsgcWSAPIServer_WebAuthn-server ondersteunt passkeys met de hieronder beschreven instellingen en events.
De WebAuthn-server is inbegrepen in sgcWebSockets Enterprise en in het sgcAuth-pack. WebAuthn heeft een secure context nodig: serveer de pagina's via https (of localhost) en stel WebAuthnOptions.RelyingParty in op de hostnaam die in de browser wordt weergegeven.
De eigenschap WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential stelt de residentKey in die in de registratieopties wordt aangevraagd:
| Waarde | residentKey | Meaning |
|---|---|---|
waundcPreferred (default) | preferred | De authenticator maakt een discoverable credential aan wanneer dit mogelijk is. |
waundcRequired | required | Alleen discoverable credentials worden geaccepteerd. Gebruik dit voor passkeys en gebruikersnaamloze aanmelding. |
waundcDiscouraged | discouraged | De authenticator moet een serverzijdige credential aanmaken, waarvoor de gebruikersnaam nodig is om aan te melden. |
Een registratieaanvraag kan de standaardwaarde overschrijven met het veld discoverable_credential ("required", "preferred" of "discouraged"). De instelling wordt alleen toegepast wanneer de aanvraag zelf niet om een specifieke residentKey vraagt.
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;
Een gebruiker heeft meestal meerdere passkeys, bijvoorbeeld een op de computer en een op de telefoon. Registreer elke passkey met dezelfde gebruikersnaam: wanneer de gebruikersnaam al credentials heeft die bekend zijn bij de server, hergebruikt de registratie hun user handle (user.id), zodat elke passkey van het account dezelfde user handle deelt. De server kent de credentials die zijn geregistreerd terwijl deze actief is, en de credentials die zijn toegevoegd met AddCredential, dus wanneer uw credentials in een database staan, voeg deze dan bij het opstarten toe om één user handle per account te behouden. Met een gebruikersnaam bevatten de authenticatieopties elke passkey van de gebruiker in allowCredentials.
Vraag de authenticatieopties op met een lege gebruikersnaam. De opties bevatten dan geen allowCredentials, de browser toont de passkeys die deze heeft voor de relying party en de gebruiker kiest er een. De server vereist de userHandle van de respons en controleert dat deze bij de gebruikte credential hoort, en een credential die niet bekend is bij de toepassing wordt geweigerd.
// ... "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);
Met conditional mediation verschijnen de passkeys in de autofill-lijst van het gebruikersnaamveld, naast de opgeslagen wachtwoorden. Voeg autocomplete="username webauthn" toe aan de input, laad /sgcWebAuthn.js (geserveerd door het component, endpoint EndpointsOptions.WebAuthn) en roep startAuthentication(options, true) aan met gebruikersnaamloze opties wanneer de pagina laadt. De promise wordt vervuld wanneer de gebruiker een passkey kiest.
<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 vereist een browser die conditional mediation ondersteunt (huidige Chrome, Edge en Safari). browserSupportsWebAuthnAutofill() retourneert False in de andere browsers, houd voor deze browsers een knop "Aanmelden met een passkey" beschikbaar.
Het component slaat credentials niet op. Vier events verbinden het met uw opslag:
| Gebeurtenis | Wat te doen |
|---|---|
| OnWebAuthnRegistrationSuccessful | Sla aCredentialRecord op, bijvoorbeeld aCredentialRecord.AsJSON, samen met de CredentialId en Username ervan. |
| OnWebAuthnAuthenticationOptionsRequest | Voeg bij een gebruikersnaam de passkeys van die gebruiker toe aan CredentialRecords. Zonder gebruikersnaam voegt u niets toe. |
| OnWebAuthnAuthenticationGetCredential | Treedt op wanneer de credential die door de browser wordt gebruikt niet in de credentiallijst van de ceremonie staat, zoals bij een gebruikersnaamloze of autofill-aanmelding. Zoek het credential-ID op, vul aCredentialRecord in en stel Found in op True. |
| OnWebAuthnAuthenticationSuccessful | Sla de bijgewerkte SignCount en BackupState van aAuthentication.Credential.CredentialRecord op en maak de sessie aan. |
// ... 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;
De record die door OnWebAuthnAuthenticationGetCredential wordt geretourneerd, moet de aangevraagde CredentialId hebben, en dezelfde Username wanneer de ceremonie met een gebruikersnaam is gestart, anders mislukt de aanmelding. Houd de UserId (user handle) in de opgeslagen record, deze wordt vergeleken met de userHandle die door de authenticator wordt verzonden. De events worden uitgevoerd in de serververbindingsthreads, bescherm gedeelde resources dienovereenkomstig.
De authenticator-datavlaggen geven aan welk type passkey de server heeft ontvangen. Ze worden in de credentialrecord opgeslagen als BackupEligible (BE-vlag) en BackupState (BS-vlag):
| BackupEligible | BackupState | Meaning |
|---|---|---|
| True | True | Gesynchroniseerde passkey, geback-upt door het platform of de wachtwoordmanager en beschikbaar op de andere apparaten van de gebruiker. |
| True | False | Multi-device-passkey waarvan nog geen back-up is gemaakt. |
| False | False | Apparaatgebonden passkey, bijvoorbeeld een beveiligingssleutel of een TPM-sleutel die het apparaat nooit verlaat. Raad de gebruiker aan een tweede passkey te registreren. |
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;
Bij elke aanmelding wijst de server een respons af waarbij de BS-vlag is ingesteld en de BE-vlag niet, en een respons waarvan de BE-vlag afwijkt van de opgeslagen BackupEligible, omdat de geschiktheid van een credential nooit verandert. De nieuwe BS-vlag wordt gekopieerd naar BackupState, sla deze op in OnWebAuthnAuthenticationSuccessful.
Sommige authenticators verhogen bij elke aanmelding een handtekeningteller. Wanneer de teller van de respons of de opgeslagen SignCount niet nul is, moet de teller van de respons groter zijn dan de opgeslagen waarde, anders wordt de aanmelding geweigerd omdat de authenticator mogelijk is gekloond. De opgeslagen waarde beweegt alleen vooruit, en moet na elke aanmelding worden opgeslagen wil de detectie werken. Gesynchroniseerde passkeys melden meestal altijd 0, waardoor de controle voor hen wordt uitgeschakeld. Stel WebAuthnOptions.AllowSignCountLessOrEqualStoredValue in op True om een teller te accepteren die niet toeneemt (de opgeslagen waarde wordt nog steeds nooit verlaagd).
De demo Demos\26.Authentication\01.Passkeys registreert meerdere passkeys per gebruiker, meldt aan zonder gebruikersnaam en met autofill, toont of elke passkey gesynchroniseerd of apparaatgebonden is, en slaat de credentials op in een JSON-bestand van de toepassing via de bovenstaande events. Zie ook WebAuthn en de JavaScript-client.