Passkeys sostituiscono le password con una coppia di chiavi creata dall'autenticatore dell'utente: Windows Hello, iCloud Keychain, Google Password Manager, un password manager o una security key FIDO2. La chiave privata non lascia mai l'autenticatore, il server memorizza solo la chiave pubblica, e la firma è vincolata al dominio del sito, così le passkey non possono essere sottratte con phishing, riutilizzate o trapelare dal database del server.
Tecnicamente una passkey è una credenziale WebAuthn discoverable (una resident key): l'autenticatore mantiene lo user handle accanto alla chiave privata, così l'utente può accedere senza digitare un nome utente, e il browser può offrire le passkey del sito nell'elenco autofill del campo nome utente. Il server TsgcWSAPIServer_WebAuthn supporta le passkey con le impostazioni e gli eventi descritti di seguito.
Il server WebAuthn è incluso in sgcWebSockets Enterprise e nel pack sgcAuth. WebAuthn richiede un contesto sicuro: servire le pagine su https (o localhost) e impostare WebAuthnOptions.RelyingParty sul nome host mostrato nel browser.
La proprietà WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential imposta il residentKey richiesto nelle opzioni di registrazione:
| Valore | residentKey | Meaning |
|---|---|---|
waundcPreferred (default) | preferred | L'autenticatore crea una credenziale discoverable quando può. |
waundcRequired | required | Sono accettate solo credenziali discoverable. Usarlo per le passkey e l'accesso senza nome utente. |
waundcDiscouraged | discouraged | L'autenticatore dovrebbe creare una credenziale lato server, che richiede il nome utente per l'accesso. |
Una richiesta di registrazione può sovrascrivere il valore predefinito con il campo discoverable_credential ("required", "preferred" o "discouraged"). L'impostazione viene applicata solo quando la richiesta stessa non chiede uno specifico residentKey.
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;
Un utente ha di solito diverse passkey, per esempio una sul computer e una sul telefono. Registrarle tutte con lo stesso nome utente: quando il nome utente ha già credenziali note al server, la registrazione riutilizza il loro user handle (user.id), cosicché ogni passkey dell'account condivide un unico user handle. Il server conosce le credenziali registrate durante la sua esecuzione e quelle aggiunte con AddCredential, quindi quando le credenziali risiedono in un database, aggiungerle all'avvio per mantenere un solo user handle per account. Con un nome utente, le opzioni di autenticazione contengono ogni passkey dell'utente in allowCredentials.
Richiedere le opzioni di autenticazione con un nome utente vuoto. Le opzioni non contengono allora alcun allowCredentials, il browser mostra le passkey che possiede per la relying party e l'utente ne sceglie una. Il server richiede il userHandle della risposta e verifica che appartenga alla credenziale usata, e una credenziale non nota all'applicazione viene rifiutata.
// ... "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);
Con la conditional mediation le passkey compaiono nell'elenco autofill del campo nome utente, accanto alle password salvate. Aggiungere autocomplete="username webauthn" all'input, caricare /sgcWebAuthn.js (servito dal componente, endpoint EndpointsOptions.WebAuthn) e chiamare startAuthentication(options, true) con opzioni senza nome utente al caricamento della pagina. La promise si risolve quando l'utente sceglie una passkey.
<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>
L'autofill richiede un browser che supporti la conditional mediation (le versioni attuali di Chrome, Edge e Safari). browserSupportsWebAuthnAutofill() restituisce False negli altri browser, mantenere per essi un pulsante "Accedi con una passkey".
Il componente non persiste le credenziali. Quattro eventi lo collegano al proprio storage:
| Evento | Cosa fare |
|---|---|
| OnWebAuthnRegistrationSuccessful | Salvare aCredentialRecord, per esempio aCredentialRecord.AsJSON, insieme al suo CredentialId e Username. |
| OnWebAuthnAuthenticationOptionsRequest | Con un nome utente, aggiungere le passkey di quell'utente a CredentialRecords. Senza un nome utente, non aggiungere nulla. |
| OnWebAuthnAuthenticationGetCredential | Si attiva quando la credenziale usata dal browser non è nell'elenco delle credenziali della ceremony, il che accade in un accesso senza nome utente o con autofill. Cercare l'id della credenziale, compilare aCredentialRecord e impostare Found a True. |
| OnWebAuthnAuthenticationSuccessful | Salvare SignCount e BackupState aggiornati di aAuthentication.Credential.CredentialRecord e creare la sessione. |
// ... 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;
Il record restituito da OnWebAuthnAuthenticationGetCredential deve avere il CredentialId richiesto, e lo stesso Username quando la ceremony è stata avviata con un nome utente, altrimenti l'accesso fallisce. Mantenere lo UserId (user handle) nel record memorizzato, viene confrontato con quello inviato dall'autenticatore (userHandle). Gli eventi vengono eseguiti nei thread di connessione del server, proteggere di conseguenza le risorse condivise.
I flag dei dati dell'autenticatore indicano il tipo di passkey ricevuta dal server. Vengono memorizzati nel record della credenziale come BackupEligible (flag BE) e BackupState (flag BS):
| BackupEligible | BackupState | Meaning |
|---|---|---|
| True | True | Passkey sincronizzata, sottoposta a backup dalla piattaforma o dal password manager e disponibile sugli altri dispositivi dell'utente. |
| True | False | Passkey multi-dispositivo non ancora sottoposta a backup. |
| False | False | Passkey vincolata al dispositivo, per esempio una security key o una chiave TPM che non lascia mai il dispositivo. Consigliare all'utente di registrare una seconda passkey. |
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;
A ogni accesso il server rifiuta una risposta con il flag BS impostato e il flag BE non impostato, e una risposta il cui flag BE differisce dal BackupEligible memorizzato, perché l'idoneità di una credenziale non cambia mai. Il nuovo flag BS viene copiato in BackupState, salvarlo in OnWebAuthnAuthenticationSuccessful.
Alcuni autenticatori incrementano un contatore di firma a ogni accesso. Quando il contatore della risposta o il SignCount memorizzato non è zero, il contatore della risposta deve essere maggiore del valore memorizzato, altrimenti l'accesso viene rifiutato perché l'autenticatore potrebbe essere stato clonato. Il valore memorizzato avanza solo in avanti, e deve essere salvato dopo ogni accesso perché il rilevamento funzioni. Le passkey sincronizzate riportano di solito sempre 0, il che disabilita il controllo per esse. Impostare WebAuthnOptions.AllowSignCountLessOrEqualStoredValue a True per accettare un contatore che non aumenta (il valore memorizzato non viene comunque mai abbassato).
La demo Demos\26.Authentication\01.Passkeys registra più passkey per utente, effettua l'accesso senza nome utente e con autofill, mostra se ogni passkey è sincronizzata o vincolata al dispositivo, e memorizza le credenziali in un file JSON dell'applicazione tramite gli eventi sopra descritti. Vedere anche WebAuthn e il client JavaScript.