Implementación de Passkeys en Spring Boot para Mejorar la Seguridad

Cómo implementar Passkeys en aplicaciones Spring Boot para mejorar la seguridad

1. Introducción

Los formularios de inicio de sesión han sido, y siguen siendo, una característica común de cualquier servicio web que requiere autenticación para proporcionar sus servicios. Sin embargo, a medida que las preocupaciones de seguridad comenzaron a ser una cuestión prioritaria, se hizo evidente que las simples contraseñas de texto son un punto débil: pueden ser adivinadas, interceptadas o filtradas, lo que lleva a incidentes de seguridad que pueden resultar en daños financieros y/o de reputación.

Los intentos anteriores de reemplazar las contraseñas con soluciones alternativas (mTLS, tarjetas de seguridad, etc.) intentaron abordar este problema, pero llevaron a una experiencia de usuario deficiente y costos adicionales.

En este tutorial, exploraremos los Passkeys, también conocidos como WebAuthn, un estándar que proporciona una alternativa segura a las contraseñas. En particular, demostraremos cómo agregar rápidamente soporte para este mecanismo de autenticación a una aplicación de Spring Boot con Spring Security.

2. ¿Qué es un Passkey?

Los Passkeys o WebAuthn son una API estándar definida por el W3C que permite a las aplicaciones que se ejecutan en un navegador web gestionar claves públicas y registrarlas para su uso con un proveedor de servicios determinado.

El escenario típico de registro se desarrolla de la siguiente manera:

  1. El usuario crea una nueva cuenta en el servicio. Las credenciales iniciales suelen ser el familiar nombre de usuario/contraseña.
  2. Una vez registrado, el usuario se dirige a su página de perfil y selecciona “crear passkey”.
  3. El sistema muestra un formulario de registro de passkey.
  4. El usuario llena el formulario con la información requerida, por ejemplo, la etiqueta de clave que ayudará al usuario a seleccionar la clave adecuada más tarde, y lo envía.
  5. El sistema guarda el passkey en su base de datos y lo asocia con la cuenta del usuario. Al mismo tiempo, una parte privada de esta clave se guardará en el dispositivo del usuario.
  6. El registro del passkey está completo.

Una vez que se completa el registro de la clave, el usuario puede utilizar el passkey almacenado para acceder al servicio. Dependiendo de la configuración de seguridad del navegador y del dispositivo del usuario, el inicio de sesión requerirá una verificación de huella digital, desbloqueo de un smartphone, o una acción similar.

Un passkey consiste en dos partes: la clave pública que el navegador envía al proveedor de servicios y una parte privada que permanece en el dispositivo local.

Además, la implementación de la API del lado del cliente asegura que un passkey dado sea utilizable solo con el mismo sitio que lo registró.

3. Agregar Passkeys a aplicaciones Spring Boot

Vamos a crear una aplicación Spring Boot simple para probar los passkeys. Nuestra aplicación tendrá solo una página de bienvenida que muestra el nombre del usuario actual y un enlace a la página de registro de passkey.

El primer paso es añadir las dependencias necesarias al proyecto:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.4.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>3.4.3</version>
</dependency>
<dependency>
    <groupId>com.webauthn4j</groupId>
    <artifactId>webauthn4j-core</artifactId>
    <version>0.28.5.RELEASE</version>
</dependency>

Las últimas versiones de estas dependencias están disponibles en Maven Central:

IMPORTANTE: el soporte para WebAuthn requiere Spring Boot versión 3.4.0 o superior.

4. Configuración de Spring Security

A partir de Spring Security 6.4, que es la versión predeterminada incluida a través de la dependencia spring-boot-starter-security, la DSL de configuración viene con soporte nativo para passkeys a través del método webAuthn().

@Bean
SecurityFilterChain webauthnFilterChain(HttpSecurity http, WebAuthNProperties webAuthNProperties) {
    return http.authorizeHttpRequests(ht -> ht.anyRequest().authenticated())
      .formLogin(withDefaults())
      .webAuthn(webauth -> 
          webauth.allowedOrigins(webAuthNProperties.getAllowedOrigins())
            .rpId(webAuthNProperties.getRpId())
            .rpName(webAuthNProperties.getRpName())
      )
      .build();
}

Esto es lo que obtenemos con esta configuración:

  • Se presentará un botón de “iniciar sesión con passkey” en la página de inicio de sesión.
  • Habrá una página de registro disponible en /webauthn/register.

Para un funcionamiento adecuado, debemos proporcionar al menos los siguientes atributos de configuración al configurador de webauthn:

  • allowedOrigins: URL externa del sitio, que DEBE utilizar HTTPS, a menos que utilice localhost.
  • rpId: Identificador de la aplicación, que DEBE ser un nombre de dominio válido que coincida con la parte de hostname del atributo allowedOrigin.
  • rpName: Un nombre amigable que el navegador puede usar durante el proceso de registro y/o inicio de sesión.

Sin embargo, esta configuración carece de un aspecto crítico del soporte para passkey: las claves registradas se pierden tras reiniciar la aplicación. Esto se debe a que, por defecto, Spring Security utiliza un almacenamiento de credenciales basado en memoria, que no está destinado a uso en producción.

Veremos cómo solucionar esto más adelante.

5. Recorrido por Passkey

Con la configuración del passkey en su lugar, es hora de realizar un rápido recorrido por nuestra aplicación. Una vez que la iniciamos usando mvn spring-boot:run o el IDE, podemos abrir nuestro navegador y navegar a http://localhost:8080:

La página de inicio de sesión estándar para aplicaciones Spring incluirá ahora el botón “Iniciar sesión con un passkey”. Dado que no hemos registrado ninguna clave aún, debemos iniciar sesión utilizando las credenciales de nombre de usuario/contraseña, que hemos configurado en nuestro archivo application.yaml: alice/changeit.

Como era de esperar, hemos iniciado sesión como Alice. Ahora podemos continuar hacia la página de registro haciendo clic en el enlace Registrar PassKey:

Aquí, solo proporcionaremos una etiqueta: baeldung-demo, y haremos clic en Registrar. Lo que sucede a continuación depende del tipo de dispositivo (escritorio, móvil, tableta) y del sistema operativo (Windows, Linux, Mac, Android), pero al final, resultará en una nueva clave que se añadirá a la lista:

Por ejemplo, en Chrome en Windows, el diálogo dará la opción de crear una nueva clave y almacenarla con el administrador de contraseñas nativo del navegador o utilizar la funcionalidad Windows Hello disponible en el sistema operativo.

A continuación, cerraremos la sesión de la aplicación e intentaremos usar nuestra nueva clave. Primero, nos dirigimos a http://localhost:8080/logout y confirmamos que queremos salir. Luego, en el formulario de inicio de sesión, hacemos clic en “Iniciar sesión con un passkey”. El navegador mostrará un diálogo que permitirá seleccionar un passkey:

Una vez que seleccionamos una de las claves disponibles, el dispositivo realizará un desafío adicional de autenticación. Para la autenticación de “Windows Hello”, esto puede ser una verificación de huella dactilar, reconocimiento facial, etc.

Si la autenticación es exitosa, la clave privada del usuario se utilizará para firmar un desafío y enviarlo al servidor, donde se validará utilizando la clave pública almacenada anteriormente. Finalmente, si todo verifica, el inicio de sesión se completa y se muestra la página de bienvenida como antes.

6. Repositorios de Passkey

Como se mencionó anteriormente, la configuración de passkey predeterminada creada por Spring Security no proporciona persistencia para las claves registradas. Para solucionar esto, necesitamos proporcionar implementaciones para las siguientes interfaces:

  • PublicKeyCredentialUserEntityRepository
  • UserCredentialRepository

6.1. PublicKeyCredentialUserEntityRepository

Este servicio gestiona instancias de PublicKeyCredentialUserEntity y asigna cuentas de usuario gestionadas por el estándar UserDetailsService a identificadores de cuentas de usuario. Esta entidad tiene los siguientes atributos:

  • name: Un identificador de nombre amigable para la cuenta.
  • id: Un identificador opaco para la cuenta del usuario.
  • displayName: Una versión alternativa del nombre de la cuenta, destinada a usarse con fines de visualización.

Es importante notar que la implementación actual asume que tanto name como id son únicos dentro de un dominio de autenticación dado.

En general, podemos asumir que las entradas en esta tabla tienen una relación 1:1 con las cuentas gestionadas por el estándar UserDetailsService.

La implementación, disponible en línea, utiliza Spring Data JDBC para almacenar esos campos en la tabla PASSKEY_USERS.

6.2. UserCredentialRepository

Gestiona instancias de CredentialRecord, que almacenan la clave pública real recibida del navegador como parte del proceso de registro. Esta entidad incluye todas las propiedades recomendadas especificadas en la documentación del W3C, junto con algunas adicionales:

  • userEntityUserId: Identificador de la PublicKeyCredentialUserEntity que posee esta credencial.
  • label: Etiqueta definida por el usuario para esta credencial, asignada en el momento del registro.
  • lastUsed: Fecha del último uso de esta credencial.
  • created: Fecha de creación de esta credencial.

Nota que CredentialRecord tiene una relación N:1 con PublicKeyCredentialUserEntity, lo cual se refleja en los métodos del repositorio. Por ejemplo, el método findByUserId() devuelve una lista de instancias de CredentialRecord.

Nuestra implementación tiene esto en cuenta y utiliza una clave externa en la tabla PASSKEY_CREDENTIALS para garantizar la integridad referencial.

7. Pruebas

Si bien es posible probar aplicaciones basadas en passkey utilizando solicitudes simuladas, el valor de esas pruebas es algo limitado. La mayoría de los escenarios de fallo están relacionados con problemas del lado del cliente, lo que requiere pruebas de integración que utilicen un navegador real impulsado por una herramienta de automatización.

Aquí, utilizaremos Selenium para implementar un escenario de “camino feliz” solo para ilustrar la técnica. En particular, utilizaremos la función VirtualAuthenticator para configurar el WebDriver, permitiéndonos simular interacciones entre la página de registro y la de inicio de sesión con este mecanismo.

Por ejemplo, así es como podemos crear un nuevo driver con un VirtualAuthenticator:

@BeforeEach
void setupTest() {
    VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions()
      .setIsUserVerified(true)
      .setIsUserConsenting(true)
      .setProtocol(VirtualAuthenticatorOptions.Protocol.CTAP2)
      .setHasUserVerification(true)
      .setHasResidentKey(true);

    driver = new ChromeDriver();
    authenticator = ((HasVirtualAuthenticator) driver).addVirtualAuthenticator(options);
}

Una vez que obtenemos la instancia de authenticator, podemos usarla para simular diferentes escenarios, como un inicio de sesión exitoso o fallido, registro, etc. Nuestra prueba en vivo abarca un ciclo completo, que consta de los siguientes pasos:

  1. Inicio de sesión inicial utilizando las credenciales de nombre de usuario/contraseña.
  2. Registro del passkey.
  3. Cierre de sesión.
  4. Inicio de sesión utilizando el passkey.

8. Conclusión

En este tutorial, hemos mostrado cómo usar Passkeys en una aplicación web de Spring Boot, incluida la configuración de Spring Security y la adición del soporte de persistencia de claves necesario para aplicaciones del mundo real.

Implementar soluciones de autenticación como Passkeys no solo mejora la seguridad de las aplicaciones, sino que también proporciona una experiencia más fluida para los usuarios. Como consejos prácticos, considere siempre mantener su aplicación actualizada con las últimas versiones de dependencias y librerías, y realice pruebas exhaustivas antes de implementar nuevas características en un entorno de producción. Esto no solo ayudará a mitigar riesgos de seguridad, sino que también facilitará la implementación de nuevos estándares como el de WebAuthn en sus proyectos futuros.