Every password your application accepts can be guessed, reused on another site, typed into a fake login page or leaked from a database backup. Passkeys remove all four problems at once. The user signs in with the fingerprint, face or PIN that already unlocks the phone or the laptop, and there is no secret on your server worth stealing.
In the overview of the new login components passkeys got five bullet points, and the SAML post covered sign-in for companies that run their own identity provider. This post is the passwordless side: how TsgcWSAPIServer_WebAuthn registers passkeys, signs users in without a user name, shows passkeys in the browser autofill list and keeps every credential in your own database.
What a Passkey Is
A passkey is a key pair created by the authenticator of the user: Windows Hello, iCloud Keychain, Google Password Manager, a password manager or a FIDO2 security key. The private key never leaves the authenticator. Your server stores only the public key, so a leaked table of passkeys lets nobody sign in. Every signature is bound to the domain of your site, so a look-alike phishing domain gets nothing it can use. That is what phishing-resistant means here.
In WebAuthn terms, a passkey is a discoverable credential, also called a resident key. The authenticator keeps the user handle next to the private key, which is why the user can sign in without typing a user name and why the browser can list the passkeys of your site on its own.
Ask for Discoverable Credentials
WebAuthn needs a secure context, so serve your pages over https, or from localhost while you develop, and set WebAuthnOptions.RelyingParty to the host name the browser shows. The residentKey that the registration options ask for comes from WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential:
waundcPreferred, the default. The authenticator creates a discoverable credential when it can.waundcRequired. Only discoverable credentials are accepted. Use it for passkeys and usernameless sign-in.waundcDiscouraged. The authenticator should create a server-side credential, which needs the user name to sign in.
A single registration request can still override the default with the field discoverable_credential, set to required, preferred or discouraged.
Sign In Without a User Name
Ask for the authentication options with an empty user name. The options then carry no allowCredentials list, the browser shows the passkeys it holds for your relying party, and the user picks one. There is nothing to type and nothing to mistype.
Because the server did not choose the credential this time, it checks more. The response must include the userHandle, the userHandle must belong to the credential that signed, and a credential your application does not know is rejected.
Passkeys in the Autofill List
Autofill, or conditional mediation, puts the passkeys of your site in the suggestion list of the user name field, next to the saved passwords. That is the gentle way to move users over: the login page keeps working for people with a password, and people with a passkey pick it from the list.
- Add
autocomplete="username webauthn"to the user name input. - Load
/sgcWebAuthn.jsin the page. The component serves it itself, from the endpoint set inEndpointsOptions.WebAuthn. - When the page loads, request usernameless options and call
startAuthentication(options, true). The promise resolves when the user picks a passkey from the list.
Autofill needs a browser with conditional mediation, which today means current Chrome, Edge and Safari. browserSupportsWebAuthnAutofill() returns False in the others, so keep a “Sign in with a passkey” button on the page as well.
Several Passkeys, One User Handle
Real users have more than one passkey: one on the laptop, one on the phone, maybe a security key for the day both are lost. Register each of them with the same user name. When that user name already has credentials the server knows, the new registration reuses their user handle (user.id), so every passkey of the account shares one user handle and all of them lead to the same user.
The server knows the credentials registered while it runs and the ones you add with AddCredential. When your passkeys live in a database, add them at startup so every account keeps a single user handle. When a user does type a user name, the authentication options list every passkey of that user in allowCredentials, and the authenticator uses whichever one it holds.
Keep the Passkeys in Your Own Database
The component does not persist credentials. Your passkeys belong next to your users table, and four events connect the two:
OnWebAuthnRegistrationSuccessful. Save the new credential record, for exampleaCredentialRecord.AsJSON, together with itsCredentialIdandUsername.OnWebAuthnAuthenticationOptionsRequest. With a user name, add the passkeys of that user toCredentialRecords. Without a user name, add nothing.OnWebAuthnAuthenticationGetCredential. Fires when the credential chosen by the browser is not in the list of the ceremony, which is the case of every usernameless and autofill sign-in. Look the credential up, fillaCredentialRecordand setFound.OnWebAuthnAuthenticationSuccessful. Save the newSignCountandBackupStateofaAuthentication.Credential.CredentialRecord, then create the session.
Saving a passkey and loading it back are two short handlers:
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 and DBFindPasskey stand for your own data access code. The record you return must carry the requested CredentialId, and the same Username when the ceremony started with a user name, otherwise the sign-in fails. Keep the UserId in the stored record too, because the server compares it with the userHandle the authenticator sends. The events run in the server connection threads, so protect shared resources the way you protect them anywhere else in the server.
Synced or Device-Bound
The flags in the authenticator data tell you what kind of passkey you received, and the credential record keeps them as BackupEligible (the BE flag) and BackupState (the BS flag):
- BackupEligible True, BackupState True. A synced passkey, backed up by the platform or the password manager and available on the other devices of the user.
- BackupEligible True, BackupState False. A multi-device passkey that is not backed up yet.
- BackupEligible False, BackupState False. A device-bound passkey, such as a security key or a TPM key that never leaves the device. Suggest that the user registers a second passkey, because losing that device means losing the credential.
Your policy can treat them differently, for example accepting only device-bound security keys on administrator accounts. At every sign-in the server rejects a response with the BS flag set and the BE flag clear, and a response whose BE flag differs from the stored BackupEligible, because the eligibility of a credential never changes. The new BS flag is copied to BackupState, and OnWebAuthnAuthenticationSuccessful is the place to save it.
The Signature Counter and Cloned Authenticators
Some authenticators increase a signature counter at every sign-in. When the counter in the response or the stored SignCount is not zero, the counter in the response must be greater than the stored value. If it is not, the sign-in is rejected, because two authenticators answering with the same key is what a cloned authenticator looks like.
The check only works if you save the counter after every sign-in, and the stored value only ever moves forward. Synced passkeys usually report 0 every time, which switches the check off for them. If you have authenticators that do not increase the counter reliably, set WebAuthnOptions.AllowSignCountLessOrEqualStoredValue to True to accept them. The stored value is still never lowered.
Try the Demo
The demo Demos\26.Authentication\01.Passkeys is a complete relying party: a TsgcWebSocketHTTPServer with a TsgcWSAPIServer_WebAuthn attached, serving a small login page on https://localhost:5443. It stores every passkey in a passkeys.json file of its own, through the four events above.
- Build the demo and keep libcrypto-3.dll and libssl-3.dll next to the executable. They are in the demo folder.
- Keep host 127.0.0.1, port 5443 and relying party localhost, then click Start.
- Click Open Browser and accept the self-signed test certificate.
- Type a user name and click Register passkey. Register a second passkey for the same user name. The form lists every passkey with its type, synced or device-bound, and its signature counter.
- Click Sign in without user name and choose a passkey. The server finds it through
OnWebAuthnAuthenticationGetCredentialand the log shows the user. - Reload the page and click the user name box. The passkeys appear in the autofill list. Pick one to sign in.
- Restart the application. The passkeys load again from passkeys.json, which shows that the storage belongs to your application and not to the component.
Documentation
Where to Get It
TsgcWSAPIServer_WebAuthn is included in the Enterprise and All-Access editions of sgcWebSockets, for Delphi and C++ Builder, and the same component is part of sgcWebSockets .NET. If you only need authentication, the sgcAuth pack has it with the other login components. You find it on the SGC Auth palette, and nothing changes in an existing application until you drop it on a form.
Read Next
- Delphi Login With Passkeys, SAML SSO, LDAP and TOTP 2FA
- SAML Single Sign-On in Delphi With Entra ID, Okta and AD FS
- WebAuthn, Passkeys, and the End of Passwords
Watch It
There is a short video, “Passkeys in Delphi: passwordless login with WebAuthn”, on the eSeGeCe channel. It shows the code in the IDE, what the backup flags and the signature counter tell you, and the demo running on https://localhost: a user registers two passkeys, then signs in without typing a user name.
Questions, feedback or help adding passkeys to your login page? Get in touch. You will get a reply from the people who wrote the code.
