Crea una llave de acceso para los accesos sin contraseña

Las llaves de acceso permiten que las cuentas de usuario sean más seguras, sencillas y fáciles de usar.

El uso de llaves de acceso en lugar de contraseñas es una excelente manera para que los sitios web logren que sus cuentas de usuario sean más seguras, simples, fáciles de usar y no tengan contraseña. Con una llave de acceso, un usuario puede acceder a un sitio web o una app con solo usar su huella dactilar, rostro o PIN del dispositivo.

Se debe crear una llave de acceso, asociarla a una cuenta de usuario y almacenar su clave pública en tu servidor antes de que un usuario pueda acceder con ella.

Cómo funcionan

Se puede pedir a un usuario que cree una llave de acceso en una de las siguientes situaciones:

  • Cuando un usuario accede con una contraseña
  • Cuando un usuario accede con una llave de acceso desde otro dispositivo (es decir, el authenticatorAttachment es cross-platform).
  • En una página exclusiva en la que los usuarios pueden administrar sus llaves de acceso

Para crear una llave de acceso, usa la API de WebAuthn.

Los cuatro componentes del flujo de registro de la llave de acceso son los siguientes:

  • Backend: Es tu servidor de backend que contiene la base de datos de cuentas que almacena la clave pública y otros metadatos sobre la llave de acceso.
  • Frontend: Es el frontend que se comunica con el navegador y envía solicitudes de recuperación al backend.
  • Navegador: El navegador del usuario que ejecuta tu JavaScript.
  • Autenticador: Es el autenticador del usuario que crea y almacena la llave de acceso. Puede estar en el mismo dispositivo que el navegador (por ejemplo, cuando usas Windows Hello) o en otro dispositivo, como un teléfono.
Diagrama de registro de la llave de acceso

El proceso para agregar una nueva llave de acceso a una cuenta de usuario existente es el siguiente:

  1. Un usuario accede al sitio web.
  2. Una vez que el usuario accede, solicita crear una llave de acceso en el frontend, por ejemplo, presionando el botón “Crear una llave de acceso”.
  3. El frontend solicita información al backend para crear una llave de acceso, como la información del usuario, un desafío y los IDs de credenciales que se excluirán.
  4. El frontend llama a navigator.credentials.create() para crear una llave de acceso. Esta llamada muestra una promesa.
  5. Una llave de acceso se crea después de que el usuario otorga su consentimiento con el bloqueo de pantalla del dispositivo. La promesa se resuelve y se muestra una credencial de clave pública al frontend.
  6. El frontend envía la credencial de clave pública al backend y almacena el ID de credencial y la clave pública asociada con la cuenta de usuario para autenticaciones futuras.

Compatibilidades

WebAuthn es compatible con la mayoría de los navegadores, pero hay pequeñas brechas. Consulta Asistencia de dispositivos: llaves de acceso.dev para saber qué combinación de navegadores y sistemas operativos permite crear una llave de acceso.

Crea una nueva llave de acceso

A continuación, se muestra cómo debe funcionar un frontend cuando se solicita crear una llave de acceso nueva.

Detección de funciones

Antes de mostrar el botón "Crear una llave de acceso nueva", verifica lo siguiente:

  • El navegador es compatible con WebAuthn.
  • El dispositivo admite un autenticador de plataforma (puede crear una llave de acceso y autenticarse con ella).
  • El navegador admite la IU condicional de WebAuthn.
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.  
// `​​isConditionalMediationAvailable` means the feature detection is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&  
    PublicKeyCredential.​​isConditionalMediationAvailable) {  
  // Check if user verifying platform authenticator is available.  
  Promise.all([  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),  
    PublicKeyCredential.​​isConditionalMediationAvailable(),  
  ]).then(results => {  
    if (results.every(r => r === true)) {  
      // Display "Create a new passkey" button  
    }  
  });  
}  

Hasta que se cumplan todas las condiciones, las llaves de acceso no serán compatibles con este navegador. Hasta entonces, no se debería mostrar el botón "Crear una llave de acceso nueva".

Cómo recuperar información importante del backend

Cuando el usuario haga clic en el botón, recupera información importante para llamar a navigator.credentials.create() desde el backend:

  • challenge: Es un desafío generado por el servidor en ArrayBuffer para este registro. Este paso es obligatorio, pero no se usa durante el registro, a menos que se realice una certificación, ya que es un tema avanzado que no se trata aquí.
  • user.id: Es el ID único de un usuario. Este valor debe ser un ArrayBuffer que no incluya información de identificación personal, como direcciones de correo electrónico o nombres de usuario. Un valor aleatorio de 16 bytes generado por cuenta funcionará bien.
  • user.name: Este campo debe contener un identificador único para la cuenta que el usuario reconocerá, como su dirección de correo electrónico o nombre de usuario. Esto se mostrará en el selector de cuentas. (Si usas un nombre de usuario, usa el mismo valor que en la autenticación con contraseña).
  • user.displayName: Este campo es un nombre obligatorio y más fácil de usar para la cuenta. No es necesario que sea único y puede ser el nombre que eligió el usuario. Si tu sitio no tiene un valor adecuado para incluir aquí, pasa una cadena vacía. Esto puede aparecer en el selector de cuentas según el navegador.
  • excludeCredentials: Impide que se registre el mismo dispositivo, ya que proporciona una lista de IDs de credenciales ya registrados. El miembro transports, si se proporciona, debe contener el resultado de la llamada a getTransports() durante el registro de cada credencial.

Llama a la API de WebAuthn para crear una llave de acceso

Llama a navigator.credentials.create() para crear una llave de acceso nueva. La API muestra una promesa, a la que espera la interacción del usuario para mostrar un diálogo modal.

const publicKeyCredentialCreationOptions = {
  challenge: *****,
  rp: {
    name: "Example",
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },
  pubKeyCredParams: [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
};

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

// Encode and send the credential to the server for verification.  

Los parámetros que no se explicaron anteriormente son los siguientes:

  • rp.id: Un ID de RP es un dominio, y un sitio web puede especificar su dominio o un sufijo registrable. Por ejemplo, si el origen de un RP es https://login.example.com:1337, su ID puede ser login.example.com o example.com. Si el ID del RP se especifica como example.com, el usuario puede autenticarse en login.example.com o en cualquier subdominio de example.com.

  • rp.name: Es el nombre del RP.

  • pubKeyCredParams: Este campo especifica los algoritmos de clave pública compatibles con el RP. Te recomendamos configurarlos en [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. Esto especifica la compatibilidad con ECDSA con P-256 y RSA PKCS#1, y la compatibilidad con estas funciones brinda una cobertura completa.

  • authenticatorSelection.authenticatorAttachment: Establece este parámetro en "platform" si la creación de esta llave de acceso es una actualización de una contraseña, p.ej., en una promoción después de un acceso. "platform" indica que la parte restringida requiere un autenticador de plataforma (un autenticador incorporado en el dispositivo de la plataforma) que no solicitará su inserción, p.ej., una llave de seguridad USB. El usuario tiene una opción más sencilla para crear una llave de acceso.

  • authenticatorSelection.requireResidentKey: Establécelo en un valor booleano "verdadero". Una credencial detectable (clave de residente) almacena la información del usuario en la llave de acceso y permite que los usuarios seleccionen la cuenta cuando se autentica.

  • authenticatorSelection.userVerification: Indica si una verificación de usuario mediante el bloqueo de pantalla del dispositivo es "required", "preferred" o "discouraged". El valor predeterminado es "preferred", lo que significa que el autenticador puede omitir la verificación del usuario. Configúralo como "preferred", o bien omite la propiedad.

Envía la credencial de clave pública que se muestra al backend

Después de que el usuario otorga su consentimiento con el bloqueo de pantalla del dispositivo, se crea una llave de acceso y se resuelve la promesa, y se muestra un objeto PublicKeyCredential al frontend.

La promesa se puede rechazar por diferentes motivos. Para controlar estos errores, verifica la propiedad name del objeto Error:

  • InvalidStateError: Ya existe una llave de acceso en el dispositivo. No se mostrará ningún diálogo de error al usuario, y el sitio no debe considerarlo como un error: el usuario quería que se registrara el dispositivo local y así es.
  • NotAllowedError: El usuario canceló la operación.
  • Otras excepciones: Se produjo un error inesperado. El navegador le muestra al usuario un diálogo de error.

El objeto de credencial de clave pública contiene las siguientes propiedades:

  • id: Es un ID codificado en Base64URL de la llave de acceso creada. Este ID ayuda al navegador a determinar si una llave de acceso coincide en el dispositivo después de la autenticación. Este valor debe almacenarse en la base de datos del backend.
  • rawId: Es una versión ArrayBuffer del ID de la credencial.
  • response.clientDataJSON: Datos de cliente codificados en ArrayBuffer.
  • response.attestationObject: Es un objeto de certificación con codificación ArrayBuffer. Esto contiene información importante, como un ID de RP, marcas y una clave pública.
  • authenticatorAttachment: Muestra "platform" cuando se crea esta credencial en un dispositivo compatible con llaves de acceso.
  • type: Este campo siempre se establece en "public-key".

Si usas una biblioteca para controlar el objeto de credencial de clave pública en el backend, te recomendamos que envíes el objeto completo al backend después de codificarlo parcialmente con base64url.

Guarda la credencial

Cuando recibas la credencial de clave pública en el backend, pásala a la biblioteca FIDO para procesar el objeto.

Luego, puedes almacenar la información recuperada de la credencial en la base de datos para usarla más adelante. En la siguiente lista, se incluyen algunas propiedades típicas que se deben guardar:

  • ID de credencial (clave primaria)
  • ID de usuario
  • Clave pública

La credencial de clave pública también incluye la siguiente información que puedes guardar en la base de datos:

Para autenticar al usuario, consulta Accede con una llave de acceso mediante el autocompletado de formularios.

Recursos