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 sin contraseñas. Con una llave de acceso, un usuario puede acceder a un sitio web o una app con solo usar la huella dactilar, el rostro o el PIN del dispositivo.

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

Cómo funciona

En una de las siguientes situaciones, se le puede pedir a un usuario que cree una llave de acceso:

  • 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 dedicada en la que los usuarios puedan 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 el 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: El frontend que se comunica con el navegador y envía solicitudes de recuperación al backend
  • Navegador: Es el navegador del usuario que ejecuta 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 se usa 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, cuando presiona el botón “Crear una llave de acceso”.
  3. El frontend solicita información del backend para crear una llave de acceso, como 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. Se crea una llave de acceso después de que el usuario da su consentimiento mediante 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 autenticación futuras.

Compatibilidades

WebAuthn es compatible con la mayoría de los navegadores, pero hay pequeñas brechas. Consulta Asistencia para dispositivos: llaves de acceso.dev para obtener información sobre qué combinación de navegadores y sistemas operativos admite la creación de llaves de acceso.

Crea una nueva llave de acceso

A continuación, se muestra cómo debe operar 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  
    }  
  });  
}  

Este navegador no admitirá las llaves de acceso hasta que se cumplan todas las condiciones. El botón "Crear una llave de acceso nueva" no debería mostrarse hasta entonces.

Recupera 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. Esto es obligatorio, pero no se usa durante el registro, a menos que se realice una certificación, ya que se trata de un tema avanzado que no se aborda 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 cuenta. (si usas un nombre de usuario, utiliza 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 nueva llave de acceso. La API muestra una promesa, a la espera de la interacción del usuario, con 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 del 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 que admite el RP. Te recomendamos establecerla en [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. De esta manera, se especifica la compatibilidad con ECDSA con P-256 y RSA PKCS#1, que brinda una cobertura completa.

  • authenticatorSelection.authenticatorAttachment: Configúralo como "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 acceder. "platform" indica que el RP quiere un autenticador de plataforma (un autenticador incorporado en el dispositivo de la plataforma) que no solicitará insertar, p.ej., una llave de seguridad USB. El usuario tiene una opción más simple de crear una llave de acceso.

  • authenticatorSelection.requireResidentKey: Configúralo en un valor booleano "verdadero". Una credencial detectable (clave de residente) almacena la información del usuario en la llave de acceso y les permite seleccionar la cuenta cuando se realiza la autenticación.

  • 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. Configura esto 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 da su consentimiento con el bloqueo de pantalla del dispositivo, se crea una llave de acceso y se resuelve la promesa, lo que muestra un objeto PublicKeyCredential al frontend.

La promesa se puede rechazar por diferentes motivos. Para manejar estos errores, puedes verificar 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 tratar esto como un error, ya que el usuario quería que se registrara el dispositivo local y así se lo hizo.
  • NotAllowedError: El usuario canceló la operación.
  • Otras excepciones: Se produjo un error inesperado. El navegador le muestra un diálogo de error al usuario.

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

  • id: 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 de ArrayBuffer del ID de credencial.
  • response.clientDataJSON: Datos del cliente codificados en ArrayBuffer.
  • response.attestationObject: Es un objeto de certificación con codificación ArrayBuffer. 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 la llave 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

Una vez que 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 para 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, lee Cómo acceder con una llave de acceso a través del autocompletado de formularios.

Recursos