Passkeys sustituyen las contraseñas por un par de claves creado por el autenticador del usuario: Windows Hello, iCloud Keychain, Google Password Manager, un gestor de contraseñas o una llave de seguridad FIDO2. La clave privada nunca sale del autenticador, el servidor solo almacena la clave pública, y la firma está vinculada al dominio del sitio, de modo que las passkeys no pueden sufrir phishing, reutilizarse ni filtrarse desde la base de datos del servidor.
Técnicamente una passkey es una credencial WebAuthn discoverable (una resident key): el autenticador guarda el user handle junto a la clave privada, de modo que el usuario puede iniciar sesión sin escribir un nombre de usuario, y el navegador puede ofrecer las passkeys del sitio en la lista de autofill del campo de nombre de usuario. El servidor TsgcWSAPIServer_WebAuthn admite passkeys con los ajustes y eventos descritos a continuación.
El servidor WebAuthn se incluye en sgcWebSockets Enterprise y en el pack sgcAuth. WebAuthn necesita un contexto seguro: sirva las páginas por https (o localhost) y establezca WebAuthnOptions.RelyingParty con el nombre de host que muestra el navegador.
La propiedad WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential establece el residentKey solicitado en las opciones de registro:
| Valor | residentKey | Meaning |
|---|---|---|
waundcPreferred (default) | preferred | El autenticador crea una credencial discoverable cuando puede. |
waundcRequired | required | Solo se aceptan credenciales discoverable. Úselo para passkeys e inicio de sesión sin nombre de usuario. |
waundcDiscouraged | discouraged | El autenticador debe crear una credencial del lado del servidor, que necesita el nombre de usuario para iniciar sesión. |
Una solicitud de registro puede sobrescribir el valor predeterminado con el campo discoverable_credential ("required", "preferred" o "discouraged"). El ajuste solo se aplica cuando la propia solicitud no pide un residentKey específico.
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 usuario suele tener varias passkeys, por ejemplo una en el ordenador y otra en el teléfono. Registre cada una con el mismo nombre de usuario: cuando el nombre de usuario ya tiene credenciales conocidas por el servidor, el registro reutiliza su user handle (user.id), de modo que todas las passkeys de la cuenta comparten un único user handle. El servidor conoce las credenciales registradas mientras se ejecuta y las añadidas con AddCredential, así que cuando sus credenciales residen en una base de datos, añádalas al arrancar para mantener un único user handle por cuenta. Con un nombre de usuario, las opciones de autenticación incluyen todas las passkeys del usuario en allowCredentials.
Solicite las opciones de autenticación con un nombre de usuario vacío. Las opciones entonces no llevan allowCredentials, el navegador muestra las passkeys que tiene para el relying party y el usuario elige una. El servidor exige el userHandle de la respuesta y comprueba que pertenece a la credencial usada, y una credencial que la aplicación no conoce se rechaza.
// ... "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 conditional mediation, las passkeys aparecen en la lista de autofill del campo de nombre de usuario, junto a las contraseñas guardadas. Añada autocomplete="username webauthn" al input, cargue /sgcWebAuthn.js (servido por el componente, endpoint EndpointsOptions.WebAuthn) y llame a startAuthentication(options, true) con opciones sin nombre de usuario cuando se cargue la página. La promesa se resuelve cuando el usuario elige 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>
El autofill necesita un navegador que admita conditional mediation (versiones actuales de Chrome, Edge y Safari). browserSupportsWebAuthnAutofill() devuelve False en los demás navegadores, mantenga para ellos un botón "Iniciar sesión con una passkey".
El componente no persiste las credenciales. Cuatro eventos lo conectan a su almacenamiento:
| Evento | Qué hacer |
|---|---|
| OnWebAuthnRegistrationSuccessful | Guarde aCredentialRecord, por ejemplo aCredentialRecord.AsJSON, junto con su CredentialId y Username. |
| OnWebAuthnAuthenticationOptionsRequest | Con un nombre de usuario, añada las passkeys de ese usuario a CredentialRecords. Sin un nombre de usuario, no añada nada. |
| OnWebAuthnAuthenticationGetCredential | Se dispara cuando la credencial usada por el navegador no está en la lista de credenciales de la ceremonia, lo que ocurre en un inicio de sesión sin nombre de usuario o con autofill. Busque el credential id, rellene aCredentialRecord y establezca Found en True. |
| OnWebAuthnAuthenticationSuccessful | Guarde el SignCount y el BackupState actualizados de aAuthentication.Credential.CredentialRecord y cree la sesión. |
// ... 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;
El registro devuelto por OnWebAuthnAuthenticationGetCredential debe tener el CredentialId solicitado, y el mismo Username cuando la ceremonia se inició con un nombre de usuario, en caso contrario el inicio de sesión falla. Mantenga el UserId (user handle) en el registro almacenado, se compara con el userHandle enviado por el autenticador. Los eventos se ejecutan en los threads de conexión del servidor, proteja los recursos compartidos en consecuencia.
Los flags de datos del autenticador indican qué tipo de passkey recibió el servidor. Se almacenan en el registro de la credencial como BackupEligible (flag BE) y BackupState (flag BS):
| BackupEligible | BackupState | Meaning |
|---|---|---|
| True | True | Passkey sincronizada, con copia de seguridad de la plataforma o del gestor de contraseñas, y disponible en los demás dispositivos del usuario. |
| True | False | Passkey multidispositivo que todavía no tiene copia de seguridad. |
| False | False | Passkey vinculada al dispositivo, por ejemplo una llave de seguridad o una clave TPM que nunca sale del dispositivo. Recomiende al usuario registrar una segunda 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;
En cada inicio de sesión el servidor rechaza una respuesta con el flag BS activado y el flag BE desactivado, y una respuesta cuyo flag BE difiera del BackupEligible almacenado, porque la elegibilidad de una credencial nunca cambia. El nuevo flag BS se copia en BackupState, guárdelo en OnWebAuthnAuthenticationSuccessful.
Algunos autenticadores incrementan un contador de firma en cada inicio de sesión. Cuando el contador de la respuesta o el SignCount almacenado no es cero, el contador de la respuesta debe ser mayor que el valor almacenado, en caso contrario el inicio de sesión se rechaza porque el autenticador podría haber sido clonado. El valor almacenado solo avanza, y debe guardarse después de cada inicio de sesión para que la detección funcione. Las passkeys sincronizadas normalmente siempre informan 0, lo que desactiva la comprobación para ellas. Establezca WebAuthnOptions.AllowSignCountLessOrEqualStoredValue en True para aceptar un contador que no aumenta (el valor almacenado nunca se reduce).
La demo Demos\26.Authentication\01.Passkeys registra varias passkeys por usuario, inicia sesión sin nombre de usuario y con autofill, muestra si cada passkey está sincronizada o vinculada al dispositivo, y almacena las credenciales en un archivo JSON de la aplicación mediante los eventos anteriores. Consulte también WebAuthn y el cliente JavaScript.