Passkeys en Delphi: inicio de sesión sin contraseña con WebAuthn

· Componentes
Passkeys en Delphi: inicio de sesión sin contraseña con WebAuthn

Cualquier contraseña que acepte tu aplicación puede adivinarse, reutilizarse en otro sitio, escribirse en una página de inicio de sesión falsa o filtrarse desde una copia de seguridad de la base de datos. Las passkeys eliminan los cuatro problemas a la vez. El usuario inicia sesión con la huella dactilar, el rostro o el PIN que ya desbloquea el teléfono o el portátil, y no hay ningún secreto en tu servidor que merezca la pena robar.

En la visión general de los nuevos componentes de inicio de sesión las passkeys recibieron cinco puntos, y la entrada sobre SAML cubrió el inicio de sesión para empresas que gestionan su propio proveedor de identidad. Esta entrada es la parte sin contraseña: cómo TsgcWSAPIServer_WebAuthn registra passkeys, inicia sesión sin nombre de usuario, muestra las passkeys en la lista de autocompletado del navegador y guarda cada credencial en tu propia base de datos.

Qué es una passkey

Una passkey es un par de claves creado por el autenticador del usuario: Windows Hello, el llavero de iCloud, el Administrador de contraseñas de Google, un gestor de contraseñas o una llave de seguridad FIDO2. La clave privada nunca sale del autenticador. Tu servidor solo guarda la clave pública, así que una tabla de passkeys filtrada no permite iniciar sesión a nadie. Cada firma está vinculada al dominio de tu sitio, así que un dominio de phishing parecido no obtiene nada que le sirva. Eso es lo que aquí significa resistente al phishing.

En términos de WebAuthn, una passkey es una credencial detectable, también llamada resident key. El autenticador guarda el userHandle junto a la clave privada, y por eso el usuario puede iniciar sesión sin escribir un nombre de usuario y el navegador puede listar por sí solo las passkeys de tu sitio.

Solicitar credenciales detectables

WebAuthn necesita un contexto seguro, así que sirve tus páginas por https, o desde localhost mientras desarrollas, y define WebAuthnOptions.RelyingParty con el nombre de host que muestra el navegador. El residentKey que piden las opciones de registro procede de WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential:

Una solicitud de registro concreta puede seguir anulando el valor predeterminado con el campo discoverable_credential, establecido en required, preferred o discouraged.

Iniciar sesión sin nombre de usuario

Solicita las opciones de autenticación con el nombre de usuario vacío. Las opciones entonces no llevan lista allowCredentials, el navegador muestra las passkeys que tiene guardadas para tu relying party y el usuario elige una. No hay nada que escribir ni nada que escribir mal.

Como esta vez el servidor no eligió la credencial, comprueba más cosas. La respuesta debe incluir el userHandle, el userHandle debe pertenecer a la credencial que firmó, y se rechaza una credencial que tu aplicación no conoce.

Passkeys en la lista de autocompletado

El autocompletado, o mediación condicional, coloca las passkeys de tu sitio en la lista de sugerencias del campo de nombre de usuario, junto a las contraseñas guardadas. Es la forma suave de hacer que los usuarios migren: la página de inicio de sesión sigue funcionando para quienes tienen contraseña, y quienes tienen una passkey la eligen de la lista.

El autocompletado necesita un navegador con mediación condicional, lo que hoy significa las versiones actuales de Chrome, Edge y Safari. browserSupportsWebAuthnAutofill() devuelve False en los demás, así que mantén también un botón “Iniciar sesión con una passkey” en la página.

Varias passkeys, un userHandle

Los usuarios reales tienen más de una passkey: una en el portátil, otra en el teléfono, quizá una llave de seguridad para el día en que se pierdan las dos. Registra cada una con el mismo nombre de usuario. Cuando ese nombre de usuario ya tiene credenciales que el servidor conoce, el nuevo registro reutiliza su userHandle (user.id), de modo que todas las passkeys de la cuenta comparten un único userHandle y todas llevan al mismo usuario.

El servidor conoce las credenciales registradas mientras está en ejecución y las que añades con AddCredential. Cuando tus passkeys viven en una base de datos, añádelas al iniciar para que cada cuenta mantenga un único userHandle. Cuando un usuario sí escribe un nombre de usuario, las opciones de autenticación listan todas las passkeys de ese usuario en allowCredentials, y el autenticador usa la que tenga.

Guarda las passkeys en tu propia base de datos

El componente no persiste las credenciales. Tus passkeys pertenecen junto a tu tabla de usuarios, y cuatro eventos conectan ambas cosas:

Guardar una passkey y volver a cargarla son dos manejadores breves:

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 y DBFindPasskey representan tu propio código de acceso a datos. El registro que devuelves debe llevar el CredentialId solicitado, y el mismo Username cuando la ceremonia empezó con un nombre de usuario, o el inicio de sesión falla. Guarda también el UserId en el registro almacenado, porque el servidor lo compara con el userHandle que envía el autenticador. Los eventos se ejecutan en los hilos de conexión del servidor, así que protege los recursos compartidos igual que los proteges en cualquier otro punto del servidor.

Sincronizadas o vinculadas al dispositivo

Los indicadores en los datos del autenticador indican qué tipo de passkey has recibido, y el registro de credencial los guarda como BackupEligible (el indicador BE) y BackupState (el indicador BS):

Tu política puede tratarlas de forma distinta, por ejemplo aceptando solo llaves de seguridad vinculadas al dispositivo en las cuentas de administrador. En cada inicio de sesión, el servidor rechaza una respuesta con el indicador BS activado y el BE desactivado, y una respuesta cuyo indicador BE difiera del BackupEligible almacenado, porque la elegibilidad de una credencial nunca cambia. El nuevo indicador BS se copia en BackupState, y OnWebAuthnAuthenticationSuccessful es el lugar donde guardarlo.

El contador de firmas y los autenticadores clonados

Algunos autenticadores incrementan un contador de firmas 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. Si no lo es, el inicio de sesión se rechaza, porque dos autenticadores que responden con la misma clave es justo el aspecto de un autenticador clonado.

La comprobación solo funciona si guardas el contador después de cada inicio de sesión, y el valor almacenado únicamente avanza. Las passkeys sincronizadas suelen informar 0 siempre, lo que desactiva la comprobación para ellas. Si tienes autenticadores que no incrementan el contador de forma fiable, define WebAuthnOptions.AllowSignCountLessOrEqualStoredValue en True para aceptarlos. El valor almacenado sigue sin bajar nunca.

Prueba la demo

La demo Demos\26.Authentication\01.Passkeys es un relying party completo: un TsgcWebSocketHTTPServer con un TsgcWSAPIServer_WebAuthn conectado, que sirve una pequeña página de inicio de sesión en https://localhost:5443. Guarda cada passkey en un archivo passkeys.json propio, mediante los cuatro eventos anteriores.

  1. Compila la demo y mantén libcrypto-3.dll y libssl-3.dll junto al ejecutable. Están en la carpeta de la demo.
  2. Deja el host 127.0.0.1, el puerto 5443 y el relying party localhost, y luego haz clic en Start.
  3. Haz clic en Open Browser y acepta el certificado de prueba autofirmado.
  4. Escribe un nombre de usuario y haz clic en Register passkey. Registra una segunda passkey para el mismo nombre de usuario. El formulario lista todas las passkeys con su tipo, sincronizada o vinculada al dispositivo, y su contador de firmas.
  5. Haz clic en Sign in without user name y elige una passkey. El servidor la encuentra mediante OnWebAuthnAuthenticationGetCredential y el registro muestra el usuario.
  6. Recarga la página y haz clic en el campo de nombre de usuario. Las passkeys aparecen en la lista de autocompletado. Elige una para iniciar sesión.
  7. Reinicia la aplicación. Las passkeys se cargan de nuevo desde passkeys.json, lo que demuestra que el almacenamiento pertenece a tu aplicación y no al componente.

Documentación

Dónde conseguirlo

TsgcWSAPIServer_WebAuthn está incluido en las ediciones Enterprise y All-Access de sgcWebSockets, para Delphi y C++ Builder, y el mismo componente forma parte de sgcWebSockets .NET. Si solo necesitas autenticación, el paquete sgcAuth lo incluye junto con los demás componentes de inicio de sesión. Lo encuentras en la paleta SGC Auth, y nada cambia en una aplicación existente hasta que lo colocas en un formulario.

Sigue leyendo

Míralo en vídeo

Hay un vídeo breve, “Passkeys in Delphi: passwordless login with WebAuthn”, en el canal de eSeGeCe. Muestra el código en el IDE, lo que indican los indicadores de respaldo y el contador de firmas, y la demo funcionando en https://localhost: un usuario registra dos passkeys y luego inicia sesión sin escribir un nombre de usuario.

¿Preguntas, comentarios o ayuda para añadir passkeys a tu página de inicio de sesión? Ponte en contacto. Recibirás respuesta de las personas que escribieron el código.