Análisis detallado de las credenciales detectables

Si bien las credenciales FIDO, como las llaves de acceso, tienen como objetivo reemplazar las contraseñas, la mayoría de ellas también pueden liberar al usuario de escribir un nombre de usuario. Esto permite que los usuarios se autentiquen seleccionando una cuenta de una lista de llaves de acceso que tienen para el sitio web actual.

Las versiones anteriores de las llaves de seguridad se diseñaron como métodos de autenticación en 2 pasos y requerían los ID de posibles credenciales, por lo que se requería el ingreso de un nombre de usuario. Las credenciales que una llave de seguridad puede encontrar sin conocer sus IDs se denominan credenciales detectables. La mayoría de las credenciales FIDO creadas actualmente son credenciales detectables, en especial las llaves de acceso almacenadas en un administrador de contraseñas o en una llave de seguridad moderna.

Para asegurarte de que las credenciales sean detectables, especifica residentKey y requireResidentKey cuando se cree la llave de acceso.

Las partes de confianza (RP) pueden usar credenciales detectables si omiten allowCredentials durante la autenticación con llave de acceso. En estos casos, el navegador o el sistema le muestran al usuario una lista de llaves de acceso disponibles, identificadas por la propiedad user.name establecida en el momento de la creación. Si el usuario selecciona una, se incluirá el valor user.id en la firma resultante. Luego, el servidor puede usar ese o el ID de credencial que se muestra para buscar la cuenta en lugar de un nombre de usuario escrito.

Las IU del selector de cuentas, como las que mencionamos anteriormente, nunca muestran credenciales no detectables.

requireResidentKey y residentKey

Para crear una credencial detectable, especifica authenticatorSelection.residentKey y authenticatorSelection.requireResidentKey en navigator.credentials.create() con los valores que se indican a continuación.

async function register () {
  // ...

  const publicKeyCredentialCreationOptions = {
    // ...
    authenticatorSelection: {
      authenticatorAttachment: 'platform',
      residentKey: 'required',
      requireResidentKey: true,
    }
  };

  const credential = await navigator.credentials.create({
    publicKey: publicKeyCredentialCreationOptions
  });

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // ...
}

residentKey:

  • 'required': Se debe crear una credencial detectable. Si no se puede crear, se muestra NotSupportedError.
  • 'preferred': El RP prefiere crear una credencial detectable, pero acepta una no detectable.
  • 'discouraged': El RP prefiere crear una credencial no detectable, pero acepta una credencial detectable.

requireResidentKey:

  • Esta propiedad se conserva para brindar retrocompatibilidad con el nivel 1 de WebAuthn, una versión anterior de la especificación. Establece esto en true si residentKey es 'required'; de lo contrario, configúralo como false.

allowCredentials

Los RP pueden usar allowCredentials en navigator.credentials.get() para controlar la experiencia de autenticación con llave de acceso. Por lo general, hay tres tipos de experiencias de autenticación con llaves de acceso:

Con credenciales detectables, las RP pueden mostrar un selector modal de cuentas para que el usuario elija una cuenta con la que acceder y, luego, la verificación del usuario. Esto es adecuado para el flujo de autenticación con llave de acceso que se inicia cuando se presiona un botón dedicado a la autenticación con llave de acceso.

Para lograr esta experiencia del usuario, omite o pasa un array vacío al parámetro allowCredentials en navigator.credentials.get().

async function authenticate() {
  // ...

  const publicKeyCredentialRequestOptions = {
    // Server generated challenge:
    challenge: ****,
    // The same RP ID as used during registration:
    rpId: 'example.com',
    // You can omit `allowCredentials` as well:
    allowCredentials: []
  };

  const credential = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions,
    signal: abortController.signal
  });

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;
  
  // ...
}

Cómo mostrar el autocompletado de un formulario de llave de acceso

El selector modal de cuentas descrito anteriormente funciona bien si la mayoría de los usuarios usan llaves de acceso y las tienen disponibles en el dispositivo local. Para un usuario que no tenga llaves de acceso locales, seguirá apareciendo el diálogo modal y le ofrecerá presentar una llave de acceso desde otro dispositivo. Durante la transición de tus usuarios a llaves de acceso, te recomendamos evitar esa IU para los usuarios que no configuraron una.

En su lugar, la selección de una llave de acceso se puede integrar en solicitudes de autocompletado para los campos en un formulario de acceso tradicional, junto con nombres de usuario y contraseñas guardados. De esta manera, un usuario con llaves de acceso puede "completar" el formulario de acceso seleccionando su llave de acceso; los usuarios con pares de nombre de usuario y contraseña guardados pueden seleccionarlas, y los usuarios que no tengan ninguna de ellas podrán escribir su nombre de usuario y contraseña.

Esta experiencia del usuario es ideal cuando el RP se encuentra en una migración con un uso mixto de contraseñas y llaves de acceso.

Para lograr esta experiencia del usuario, además de pasar un array vacío a la propiedad allowCredentials o de omitir el parámetro, especifica mediation: 'conditional' en navigator.credentials.get() y anota un campo de entrada HTML username con autocomplete="username webauthn" o un campo de entrada password con autocomplete="password webauthn".

La llamada a navigator.credentials.get() no hará que se muestre ninguna IU, pero si el usuario enfoca el campo de entrada con anotaciones, todas las llaves de acceso disponibles se incluirán en las opciones de autocompletado. Si el usuario selecciona uno, se someterá a la verificación normal del desbloqueo del dispositivo y solo entonces la promesa que devuelve .get() se resolverá con un resultado. Si el usuario no selecciona una llave de acceso, la promesa nunca se resuelve.

async function authenticate() {
  // ...

  const publicKeyCredentialRequestOptions = {
    // Server generated challenge:
    challenge: ****,
    // The same RP ID as used during registration:
    rpId: 'example.com',
    // You can omit `allowCredentials` as well:
    allowCredentials: []
  };

  const cred = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions,
    signal: abortController.signal,
    // Specify 'conditional' to activate conditional UI
    mediation: 'conditional'
  });

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;
  
  // ...
}
<input type="text" name="username" autocomplete="username webauthn" ...>

Puedes aprender a crear esta experiencia del usuario en Accede con una llave de acceso mediante el autocompletado de formularios, así como en el codelab Cómo implementar llaves de acceso con el autocompletado de formularios en una app web.

Reautenticación

En algunos casos, como cuando se usan llaves de acceso para la reautenticación, ya se conoce el identificador del usuario. En este caso, nos gustaría usar una llave de acceso sin que el navegador o el SO muestren ningún selector de cuentas. Para ello, se debe pasar una lista de IDs de credenciales en el parámetro allowCredentials.

En ese caso, si alguna de las credenciales con nombre está disponible de forma local, se le solicita al usuario que desbloquee el dispositivo de inmediato. De lo contrario, se le solicita que presente otro dispositivo (un teléfono o una llave de seguridad) que tenga una credencial válida.

Para lograr esta experiencia del usuario, proporciona una lista de IDs de credenciales para el usuario que accede. El RP debería poder consultarlos porque el usuario ya es conocido. Proporciona IDs de credenciales como objetos PublicKeyCredentialDescriptor en la propiedad allowCredentials de navigator.credentials.get().

async function authenticate() {
  // ...

  const publicKeyCredentialRequestOptions = {
    // Server generated challenge:
    challenge: ****,
    // The same RP ID as used during registration:
    rpId: 'example.com',
    // Provide a list of PublicKeyCredentialDescriptors:
    allowCredentials: [{
      id: ****,
      type: 'public-key',
      transports: [
        'internal',
        'hybrid'
      ]
    }, {
      id: ****,
      type: 'public-key',
      transports: [
        'internal',
        'hybrid'
      ]
    }, ...]
  };

  const credential = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions,
    signal: abortController.signal
  });

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;
  
  // ...
}

Un objeto PublicKeyCredentialDescriptor consta de lo siguiente:

  • id: Es un ID de la credencial de clave pública que el RP obtuvo en el registro de la llave de acceso.
  • type: Por lo general, este campo es 'public-key'.
  • transports: Es una sugerencia de transportes compatible con el dispositivo que contiene esta credencial y que los navegadores usan para optimizar la IU que le solicita al usuario que presente un dispositivo externo. Esta lista, si se proporciona, debe contener el resultado de la llamada a getTransports() durante el registro de cada credencial.

Resumen

Las credenciales detectables hacen que la experiencia de acceso con llave de acceso sea mucho más fácil de usar, ya que les permiten omitir el ingreso de un nombre de usuario. Con la combinación de residentKey, requireResidentKey y allowCredentials, los RP pueden lograr experiencias de acceso con las siguientes características:

  • Mostrar un selector modal de cuentas.
  • Muestra un formulario de llave de acceso para autocompletar.
  • Reautenticación.

Usa las credenciales detectables con inteligencia. De esta manera, puedes diseñar experiencias sofisticadas de acceso con llaves de acceso que los usuarios puedan acceder sin problemas y con las que es más probable que interactúen.