Implementa llaves de acceso con el autocompletado de formularios en una app web

1. Antes de comenzar

Utilizar 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, sencillas y fáciles de usar. Con una llave de acceso, los usuarios pueden acceder a un sitio web o a una app mediante una de las funciones de bloqueo de pantalla del dispositivo, como la huella dactilar, el rostro o el PIN. Para hacerlo se debe crear una llave de acceso, asociarla a una cuenta de usuario y almacenar la clave pública en un servidor antes de que un usuario pueda acceder con ella.

En este codelab, convertirás el acceso básico de un nombre de usuario y contraseña basados en formularios en uno que admite llaves de acceso y que incluye los siguientes elementos:

  • Un botón que crea una llave de acceso después de que el usuario accede
  • Una IU que muestra una lista de llaves de acceso registradas
  • El formulario de acceso existente que permite a los usuarios acceder con una llave de acceso registrada a través del autocompletado de formularios

Requisitos previos

Qué aprenderás

  • Cómo crear una llave de acceso
  • Cómo autenticar usuarios con una llave de acceso
  • Cómo permitir que un formulario sugiera una llave como opción para acceder

Requisitos

Una de las siguientes combinaciones de dispositivos:

  • Google Chrome con un dispositivo Android que ejecute Android 9 o versiones posteriores, preferiblemente con sensor biométrico
  • Google Chrome con un dispositivo con Windows que ejecute Windows 10 o versiones posteriores
  • Safari 16 o versiones posteriores con un iPhone que ejecute iOS 16 o versiones posteriores, o un iPad con iPadOS 16 o versiones posteriores
  • Safari 16 o versiones posteriores, o Chrome, con un dispositivo de escritorio Apple que ejecute macOS Ventura o versiones posteriores

2. Prepárate

En este codelab, usarás un servicio llamado Glitch, que te permitirá editar código del cliente y del servidor con JavaScript, además de implementarlo desde el navegador.

Abre el proyecto

  1. Abre el proyecto en Glitch.
  2. Haz clic en Remix para bifurcar el proyecto de Glitch.
  3. En el menú de navegación, en la parte inferior de Glitch, haz clic en Preview > Preview in a new window. Se abrirá otra pestaña en tu navegador.

Botón Preview in a new window, en el menú de navegación, en la parte inferior de Glitch

Examina el estado inicial del sitio web

  1. En la pestaña de vista previa, ingresa un nombre de usuario aleatorio y haz clic en Next.
  2. Ingresa una contraseña aleatoria y haz clic en Sign-in. La contraseña se ignora, pero se te autentica de todas maneras y llegas a la página principal.
  3. Si quieres cambiar tu nombre visible, hazlo. Eso es todo lo que puedes hacer en el estado inicial.
  4. Haz clic en Sign out.

En este estado, los usuarios deben ingresar una contraseña cada vez que accedan. Agrega compatibilidad con llaves de acceso a este formulario para que los usuarios puedan acceder con la función de bloqueo de pantalla del dispositivo. Prueba el estado final en https://passkeys-codelab.glitch.me/.

Para obtener más información sobre cómo funcionan las llaves de acceso, consulta ¿Cómo funcionan las llaves de acceso?.

3. Agrega una habilidad para crear una llave de acceso

Para permitir que los usuarios se autentiquen con una llave de acceso, debes permitirles crearla, registrarla y almacenar su clave pública en el servidor.

Aparece un diálogo de verificación del usuario para la llave de acceso luego de crearla.

Agrega una IU para que los usuarios generen una llave de acceso después de acceder con una contraseña y vean una lista de todas las llaves de acceso registradas en la página /home. En la siguiente sección, crearás una función para generar y registrar una llave de acceso.

Crea la función registerCredential()

  1. En Glitch, navega al archivo public/client.js y, luego, desplázate hasta el final.
  2. Después de encontrar el comentario relevante, agrega la siguiente función registerCredential():

public/client. js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to create a passkey: Create a credential.

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

};

Con esta función, se crea y registra una llave de acceso en el servidor.

Obtén el desafío y otras opciones del extremo del servidor

Antes de crear una llave de acceso, debes solicitar que los parámetros pasen la WebAuthn desde el servidor, incluido un desafío. WebAuthn es una API del navegador que les permite a los usuarios crear una llave de acceso para autenticarse con ella. Afortunadamente, ya tienes un extremo de servidor que responde a esos parámetros en este codelab.

  • Para obtener el desafío y otras opciones del extremo del servidor, agrega el siguiente código al cuerpo de la función registerCredential() después del comentario relevante:

public/client.js

// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/registerRequest');

En el siguiente fragmento de código, se incluyen opciones de muestra que recibes del servidor:

{
  challenge: *****,
  rp: {
    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', 'hybrid'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

El protocolo entre un servidor y un cliente no forma parte de la especificación de WebAuthn. Sin embargo, el servidor de este codelab está diseñado para mostrar un JSON que sea lo más similar posible al diccionario PublicKeyCredentialCreationOptions que se pasa a la API navigator.credentials.create() de WebAuthn.

La siguiente tabla no es exhaustiva, pero contiene los parámetros importantes del diccionario PublicKeyCredentialCreationOptions:

Parámetros

Descripciones

challenge

Un desafío generado por el servidor en un objeto ArrayBuffer para este registro. Aunque es obligatorio, no se usa durante el registro, a menos que se realice una certificación, ya que corresponde a un tema avanzado que no se aborda en este codelab.

user.id

El ID único del usuario. Este valor debe ser un objeto ArrayBuffer que no incluya información de identidad personal, como direcciones de correo electrónico ni nombres de usuario. Un valor aleatorio de 16 bytes generado por cuenta funciona bien.

user.name

Este campo debe contener un identificador único para la cuenta que el usuario pueda reconocer, como su dirección de correo electrónico o nombre de usuario. Se muestra en el selector de cuenta (si utilizas un nombre de usuario, utiliza el mismo valor que usas para la autenticación con contraseña).

user.displayName

Este campo corresponde a un nombre opcional y 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 web no cuenta con un valor adecuado que puedas incluir aquí, pasa una cadena vacía. Esto puede aparecer en el selector de cuentas según el navegador.

rp.id

El ID de un grupo de confianza (RP) es un dominio. Un sitio web puede especificar su dominio o un sufijo que se pueda registrar. 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, los usuarios se pueden autenticar en login.example.com o en cualquier otro subdominio de example.com.

pubKeyCredParams

En este campo, se especifican los algoritmos de clave pública admitidos del RP. Te recomendamos configurarlos en [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. De esta forma, se especifica la compatibilidad con ECDSA con P-256 y RSA PKCS#1, lo que proporciona una cobertura completa.

excludeCredentials

Proporciona una lista de IDs de credenciales ya registrados para evitar que se registre dos veces el mismo dispositivo. Si se proporciona, el miembro transports debe contener el resultado de la llamada a la función getTransports() durante el registro de cada credencial.

authenticatorSelection.authenticatorAttachment

Se establece en un valor "platform". De esta forma, indicas que quieres un autenticador incorporado en el dispositivo de la plataforma para que no se le solicite al usuario insertar un factor como una llave de seguridad USB.

authenticatorSelection.requireResidentKey

Establece en un valor booleano true. Se puede usar una credencial detectable (clave de residente) sin que el servidor tenga que proporcionar el ID de la credencial y, por lo tanto, sea compatible con el autocompletado.

authenticatorSelection.userVerification

Configúralo en un valor de "preferred", o bien omítelo, porque es el valor predeterminado. Este indica si la verificación de un usuario que usa el bloqueo de pantalla del dispositivo es "required", "preferred" o "discouraged". Si se establece un valor de "preferred", se solicitará la verificación del usuario cuando el dispositivo es compatible.

Crea una credencial

  1. En el cuerpo de la función registerCredential() después del comentario relevante, convierte algunos parámetros codificados con Base64URL en un objeto binario, específicamente las cadenas user.id y challenge, y las instancias de la cadena id incluida en el array excludeCredentials:

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);

if (options.excludeCredentials) {
  for (let cred of options.excludeCredentials) {
    cred.id = base64url.decode(cred.id);
  }
}
  1. En la línea siguiente, establece authenticatorSelection.authenticatorAttachment en "platform" y authenticatorSelection.requireResidentKey en true. De esta manera, solo se permite el uso de un autenticador de plataforma (el propio dispositivo) con una función de credencial detectable.

public/client.js

// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
  authenticatorAttachment: 'platform',
  requireResidentKey: true
}
  1. En la línea siguiente, llama al método navigator.credentials.create() para crear una credencial.

public/client.js

// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
  publicKey: options,
});

Con esta llamada, el navegador intenta verificar la identidad del usuario con el bloqueo de pantalla del dispositivo.

Registra la credencial en el extremo del servidor

Después de que el usuario verifica su identidad, se crea y almacena una llave de acceso. El sitio web recibe un objeto de credencial que contiene una clave pública que puedes enviar al servidor para registrar la llave de acceso.

El siguiente fragmento de código contiene un objeto de credencial de ejemplo:

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "attestationObject": *****,
    "transports": ["internal", "hybrid"]
  },
  "authenticatorAttachment": "platform"
}

La siguiente tabla no es exhaustiva, pero contiene los parámetros importantes del objeto PublicKeyCredential:

Parámetros

Descripciones

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 se debe almacenar en la base de datos del backend.

rawId

Una versión del objeto ArrayBuffer del ID de credencial.

response.clientDataJSON

Datos de cliente codificados en un objeto ArrayBuffer.

response.attestationObject

Un objeto de certificación con codificación ArrayBuffer. Contiene información importante, como un ID de RP, marcas y una clave pública.

response.transports

Una lista de transportes que admite el dispositivo: "internal" significa que el dispositivo admite una llave de acceso. "hybrid" significa que también admite la autenticación en otro dispositivo.

authenticatorAttachment

Muestra "platform" cuando se crea esta credencial en un dispositivo compatible con la llave de acceso.

Para enviar el objeto de credencial al servidor, sigue estos pasos:

  1. Codifica los parámetros binarios de la credencial como Base64URL para que se pueda entregar al servidor como una cadena:

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
  credential.authenticatorAttachment = cred.authenticatorAttachment;
}

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject = base64url.encode(cred.response.attestationObject);

// Obtain transports.
const transports = cred.response.getTransports ? cred.response.getTransports() : [];

credential.response = {
  clientDataJSON,
  attestationObject,
  transports
};
  1. En la línea siguiente, envía el objeto al servidor:

public/client.js

return await _fetch('/auth/registerResponse', credential);

Cuando ejecutas el programa, el servidor muestra HTTP code 200, lo que indica que la credencial está registrada.

Ahora tienes la función registerCredential() completa.

Revisa el código de solución para esta sección

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from server endpoint.
  const options = await _fetch('/auth/registerRequest');

  // TODO: Add an ability to create a passkey: Create a credential.
  // Base64URL decode some values.

  options.user.id = base64url.decode(options.user.id);
  options.challenge = base64url.decode(options.challenge);

  if (options.excludeCredentials) {
    for (let cred of options.excludeCredentials) {
      cred.id = base64url.decode(cred.id);
    }
  }

  // Use platform authenticator and discoverable credential.
  options.authenticatorSelection = {
    authenticatorAttachment: 'platform',
    requireResidentKey: true
  }

  // Invoke the WebAuthn create() method.
  const cred = await navigator.credentials.create({
    publicKey: options,
  });

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
  if (cred.authenticatorAttachment) {
    credential.authenticatorAttachment = cred.authenticatorAttachment;
  }

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const attestationObject =
  base64url.encode(cred.response.attestationObject);

  // Obtain transports.
  const transports = cred.response.getTransports ?
  cred.response.getTransports() : [];

  credential.response = {
    clientDataJSON,
    attestationObject,
    transports
  };

  return await _fetch('/auth/registerResponse', credential);
};

4. Compila una IU para registrar y administrar credenciales de llaves de acceso

Ahora que la función registerCredential() está disponible, necesitas un botón para invocarla. Además, debes mostrar una lista de llaves de acceso registradas.

Llaves de acceso registradas en la página principal

Agrega el HTML de marcador de posición

  1. En Glitch, navega al archivo views/home.html.
  2. Después del comentario relevante, agrega un marcador de posición de IU que muestre un botón para registrar una llave de acceso y una lista de llaves de acceso:

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3 class="mdc-typography mdc-typography--headline6"> Your registered
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>

El elemento div#list es el marcador de posición de la lista.

Comprueba la compatibilidad con las llaves de acceso

Para mostrar únicamente la opción de crear una llave de acceso para los usuarios con dispositivos que las admitan, primero debes verificar si WebAuthn está disponible. Si es así, debes quitar la clase hidden para que se muestre el botón Crear una llave de acceso.

Para verificar si un entorno admite llaves de acceso, sigue estos pasos:

  1. Al final del archivo views/home.html, después del comentario relevante, escribe un campo condicional que se ejecute si window.PublicKeyCredential, PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable y PublicKeyCredential.isConditionalMediationAvailable son true.

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  1. En el cuerpo del campo condicional, comprueba si el dispositivo puede crear una llave de acceso y, luego, verifica si se puede sugerir la llave en la función de autocompletado de formularios.

views/home.html

try {
  const results = await Promise.all([

    // Is platform authenticator available in this browser?
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

    // Is conditional UI available in this browser?
    PublicKeyCredential.isConditionalMediationAvailable()
  ]);
  1. Si se cumplen todas las condiciones, aparecerá el botón para crear una llave de acceso. De lo contrario, aparecerá un mensaje de advertencia.

views/home.html

    if (results.every(r => r === true)) {

      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

Renderiza llaves de acceso registradas en una lista

  1. Define una función renderCredentials() que recupere las llaves de acceso registradas del servidor y las renderice en una lista. Afortunadamente, ya tienes el extremo del servidor /auth/getKeys que te permite recuperar las llaves de acceso registradas para el usuario que accedió.

views/home.html

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
    <mwc-list>
      ${res.map(cred => html`
        <mwc-list-item>
          <div class="list-item">
            <div class="entity-name">
              <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}"
            data-name="${cred.name || 'Unnamed' }" @click="${rename}"
            icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}"
            icon="delete"></mwc-icon-button>
          </div>
         </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-list>`}`;
  render(creds, list);
};
  1. En la siguiente línea, invoca la función renderCredentials() para mostrar las llaves de acceso registradas apenas el usuario llega a la página /home como una inicialización.

views/home.html

renderCredentials();

Crea y registra una llave de acceso

Para crear y registrar una llave de acceso, debes llamar a la función registerCredential() que implementaste antes.

Para activar la función registerCredential() cuando haces clic en el botón Crear una llave de acceso, sigue estos pasos:

  1. En el archivo después del marcador de posición HTML, busca la siguiente sentencia import:

views/home.html

import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
} from '/client.js';
  1. Al final del cuerpo de la sentencia import, agrega la función registerCredential().

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
  registerCredential
} from '/client.js';
  1. Al final del archivo después del comentario relevante, define una función register() que invoque la función registerCredential() y una IU de carga, y llame a renderCredentials() después de un registro. De esta forma, el navegador crea una llave de acceso y muestra un mensaje de error cuando algo sale mal.

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  1. En el cuerpo de la función register(), detecta excepciones. El método navigator.credentials.create() muestra un error InvalidStateError cuando ya existe una llave de acceso en el dispositivo. Esto se analiza con el array excludeCredentials. En este caso, se muestra un mensaje relevante para el usuario. También se produce un error NotAllowedError cuando el usuario cancela el diálogo de autenticación. En este caso, ignóralo sin aviso.

views/home.html

  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};
  1. En la línea después de la función register(), adjunta la función register() a un evento click para el botón Crear una llave de acceso.

views/home.html

createPasskey.addEventListener('click', register);

Revisa el código de solución para esta sección

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3 class="mdc-typography mdc-typography--headline6"> Your registered
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
  registerCredential
} from '/client.js';

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');

// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const results = await Promise.all([

      // Is platform authenticator available in this browser?
      PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

      // Is conditional UI available in this browser?
      PublicKeyCredential.isConditionalMediationAvailable()
    ]);
    if (results.every(r => r === true)) {

      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
  <mwc-list>
    ${res.map(cred => html`
      <mwc-list-item>
        <div class="list-item">
          <div class="entity-name">
            <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed' }" @click="${rename}" icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" icon="delete"></mwc-icon-button>
          </div>
        </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-list>`}`;
  render(creds, list);
};

renderCredentials();

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};

createPasskey.addEventListener('click', register);

Pruébalo

Si seguiste todos los pasos hasta ahora, implementaste la capacidad de crear, registrar y mostrar llaves de acceso en el sitio web.

Para probarlo, sigue estos pasos:

  1. En la pestaña de vista previa, accede con un nombre de usuario y una contraseña aleatorios.
  2. Haz clic en Crear una llave de acceso.
  3. Verifica tu identidad con el bloqueo de pantalla del dispositivo.
  4. Confirma que la llave de acceso esté registrada y se muestre en la sección Your registered passkeys de la página web.

Las llaves de acceso registradas que se muestran en la página principal

Cambia el nombre de las llaves de acceso registradas y quítalas

Deberías poder cambiar el nombre de las llaves de acceso registradas de la lista o poder borrarlas. Puedes verificar cómo funciona en el código que incluye el codelab.

En Chrome, puedes quitar las llaves de acceso registradas en chrome://settings/passkeys en computadoras, o en el administrador de contraseñas en la configuración de Android.

Para obtener información sobre cómo cambiar el nombre de las llaves de acceso registradas y quitarlas en otras plataformas, consulta las páginas de asistencia correspondientes a esas plataformas.

5. Agrega la capacidad de autenticarse con una llave de acceso

Los usuarios ahora pueden crear y registrar llaves de acceso, y están listos para utilizarlas como un método que les permitirá autenticarse en tu sitio web de forma segura. Ahora debes agregar una capacidad de autenticación con llave de acceso a tu sitio web.

Crea la función authenticate()

  • En el archivo public/client.js después del comentario relevante, crea una función llamada authenticate() que verifique al usuario de forma local y, luego, en el servidor:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.

};

Obtén el desafío y otras opciones del extremo del servidor

Antes de pedirle al usuario que se autentique, debes solicitar que los parámetros pasen WebAuthn desde el servidor, incluido un desafío.

  • En el cuerpo de la función authenticate(), después del comentario relevante, llama a la función _fetch() para enviar una solicitud POST al servidor:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/signinRequest');

El servidor de este codelab está diseñado para mostrar un JSON que sea lo más similar posible al diccionario PublicKeyCredentialRequestOptions que se pasa a la API navigator.credentials.get() de WebAuthn. El siguiente fragmento de código incluye opciones de ejemplo que deberías recibir:

{
  "challenge": *****,
  "rpId": "passkeys-codelab.glitch.me",
  "allowCredentials": []
}

La siguiente tabla no es exhaustiva, pero contiene los parámetros importantes del diccionario PublicKeyCredentialRequestOptions:

Parámetros

Descripciones

challenge

Un desafío generado por el servidor en un objeto ArrayBuffer. Esta acción es necesaria para evitar ataques de reproducción. Nunca aceptes el mismo desafío en una respuestas dos veces. Considéralo un token de CSRF.

rpId

Un ID de RP es un dominio. Un sitio web puede especificar su dominio o un sufijo que se pueda registrar. Este valor debe coincidir con el parámetro rp.id que se usó cuando se creó la llave de acceso.

allowCredentials

Esta propiedad se usa con el fin de buscar autenticadores aptos para esta autenticación. Pasa un array vacío o déjalo sin especificar para que el navegador muestre un selector de cuentas.

userVerification

Configúralo en un valor de "preferred", o bien omítelo, porque es el valor predeterminado. Indica si una verificación de usuario mediante el bloqueo de pantalla del dispositivo es "required", "preferred" o "discouraged". Si se establece un valor de "preferred", se solicitará la verificación del usuario cuando el dispositivo es compatible.

Verifica el usuario de forma local y obtén una credencial

  1. En el cuerpo de la función authenticate(), después del comentario relevante, vuelve a convertir el parámetro challenge en un objeto binario:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
  1. Pasa un array vacío al parámetro allowCredentials para abrir un selector de cuentas cuando un usuario se autentique:

public/client.js

// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];

El selector de cuentas utiliza la información del usuario almacenada con la llave de acceso.

  1. Llama al método navigator.credentials.get() junto con una opción mediation: 'conditional':

public/client.js

// Invoke the WebAuthn get() method.
const cred = await navigator.credentials.get({
  publicKey: options,

  // Request a conditional UI.
  mediation: 'conditional'
});

Esta opción indica al navegador que sugiera las llaves de acceso de manera condicional como parte del autocompletado del formulario.

Verifica la credencial

Una vez que el usuario verifique su identidad de manera local, recibirás un objeto de credencial con una firma que puedes verificar en el servidor.

En el siguiente fragmento de código, se incluye un objeto PublicKeyCredential de ejemplo:

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "authenticatorData": *****,
    "signature": *****,
    "userHandle": *****
  },
  authenticatorAttachment: "platform"
}

La siguiente tabla no es exhaustiva, pero contiene los parámetros importantes del objeto PublicKeyCredential:

Parámetros

Descripciones

id

El ID codificado en Base64URL de la credencial de llave de acceso autenticada.

rawId

Una versión del objeto ArrayBuffer del ID de credencial.

response.clientDataJSON

Un objeto ArrayBuffer de datos de cliente. Este campo contiene información, como el desafío y el origen, que el servidor de RP debe verificar.

response.authenticatorData

Un objeto ArrayBuffer de datos de autenticador. Este campo contiene información como el ID del RP.

response.signature

Un objeto ArrayBuffer de la firma. Este valor es el núcleo de la credencial y se debe verificar en el servidor.

response.userHandle

Un objeto ArrayBuffer que contiene el ID del usuario establecido en el momento de la creación. Este valor se puede usar en lugar del ID de credencial si el servidor necesita elegir los valores de ID que usa o si el backend desea evitar la creación de un índice en los IDs de credencial.

authenticatorAttachment

Muestra una cadena "platform" cuando esta credencial proviene del dispositivo local. De lo contrario, muestra una cadena "cross-platform", en particular cuando el usuario utiliza un teléfono para acceder. Si el usuario necesita usar un teléfono para acceder, pídele que cree una llave de acceso en el dispositivo local.

Para enviar el objeto de credencial al servidor, sigue estos pasos:

  1. En el cuerpo de la función authenticate() después del comentario relevante, codifica los parámetros binarios de la credencial para que se pueda entregar al servidor como una cadena:

public/client.js

// TODO: Add an ability to authenticate with a passkey: Verify the credential.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const authenticatorData = base64url.encode(cred.response.authenticatorData);
const signature = base64url.encode(cred.response.signature);
const userHandle = base64url.encode(cred.response.userHandle);

credential.response = {
  clientDataJSON,
  authenticatorData,
  signature,
  userHandle,
};
  1. Envía el objeto al servidor:

public/client.js

return await _fetch(`/auth/signinResponse`, credential);

Cuando ejecutas el programa, el servidor muestra HTTP code 200, lo que indica que la credencial está verificada.

Ahora tienes la función authentication() completa.

Revisa el código de solución para esta sección

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the
  challenge and other options from the server endpoint.
  const options = await _fetch('/auth/signinRequest');

  // TODO: Add an ability to authenticate with a passkey: Locally verify
  the user and get a credential.
  // Base64URL decode the challenge.
  options.challenge = base64url.decode(options.challenge);

  // The empty allowCredentials array invokes an account selector
  by discoverable credentials.
  options.allowCredentials = [];

  // Invoke the WebAuthn get() function.
  const cred = await navigator.credentials.get({
    publicKey: options,

    // Request a conditional UI.
    mediation: 'conditional'
  });

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const authenticatorData =
  base64url.encode(cred.response.authenticatorData);
  const signature = base64url.encode(cred.response.signature);
  const userHandle = base64url.encode(cred.response.userHandle);

  credential.response = {
    clientDataJSON,
    authenticatorData,
    signature,
    userHandle,
  };

  return await _fetch(`/auth/signinResponse`, credential);
};

6. Agrega llaves de acceso al elemento de autocompletado del navegador

Cuando el usuario regrese, querrás que acceda con la mayor facilidad y seguridad posible. Si agregas el botón Acceder con una llave de acceso a la página de acceso, el usuario puede presionarlo, seleccionar una llave de acceso en el selector de cuentas del navegador y usar el bloqueo de pantalla para verificar su identidad.

Sin embargo, la transición de una contraseña a una llave de acceso no se produce para todos los usuarios a la vez. Esto significa que no puedes deshacerte de las contraseñas hasta que todos los usuarios realicen la transición a las llaves de acceso, por lo que debes dejar el formulario de acceso basado en contraseñas hasta que eso suceda. Sin embargo, si dejas un formulario de contraseñas y un botón para la llave de acceso, los usuarios tendrán que elegir una opción entre ambas para acceder. Lo ideal es que el proceso de acceso sea sencillo.

Aquí es donde entra en juego la IU condicional. Una IU condicional es una función de WebAuthn en la que puedes crear un campo de entrada de formulario para sugerir una llave de acceso como parte de los elementos de autocompletado, además de las contraseñas. Si un usuario presiona una llave de acceso en las sugerencias para autocompletar, se le pedirá que use el bloqueo de pantalla del dispositivo para verificar su identidad de forma local. Esta es una experiencia del usuario sencilla porque su acción es casi idéntica a la de un acceso basado en contraseña.

Una llave de acceso sugerida como parte del autocompletado de formularios.

Habilita una IU condicional

Para habilitar una IU condicional, solo debes agregar un token webauthn en el atributo autocomplete de un campo de entrada. Con el token establecido, puedes llamar al método navigator.credentials.get() con la cadena mediation: 'conditional' para activar condicionalmente la IU de bloqueo de pantalla.

  • Para habilitar una IU condicional, reemplaza los campos de entrada de nombre de usuario existentes por el siguiente HTML después del comentario relevante en el archivo view/index.html:

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus />

Detecta funciones, invoca WebAuthn y habilita una IU condicional

  1. En el archivo view/index.html, después del comentario relevante, reemplaza la sentencia import existente por el siguiente código:

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
  $,
  _fetch,
  loading,
  authenticate
} from "/client.js";

Este código importa la función authenticate() que implementaste antes.

  1. Confirma que el objeto window.PulicKeyCredential esté disponible y que el método PublicKeyCredential.isConditionalMediationAvailable() muestre un valor true y, luego, llama a la función authenticate():

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
if (
  window.PublicKeyCredential &&
  PublicKeyCredential.isConditionalMediationAvailable
) {
  try {

    // Is conditional UI available in this browser?
    const cma =
      await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $("#username").value = user.username;
        loading.start();
        location.href = "/home";
      } else {
        throw new Error("User not found.");
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== "NotAllowedError") {
      console.error(e);
      alert(e.message);
    }
  }
}

Revisa el código de solución para esta sección

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus
/>

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
  $,
  _fetch,
  loading,
  authenticate
} from '/client.js';

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
// Is WebAuthn avaiable in this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {

    // Is a conditional UI available in this browser?
    const cma= await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If a conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $('#username').value = user.username;
        loading.start();
        location.href = '/home';
      } else {
        throw new Error('User not found.');
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== 'NotAllowedError') {
      console.error(e);
      alert(e.message);
    }
  }
}

Pruébalo

Ya implementaste la creación, el registro, la visualización y la autenticación de las llaves de acceso en tu sitio web.

Para probarlo, sigue estos pasos:

  1. Navega a la pestaña de vista previa.
  2. Si es necesario, sal de tu cuenta.
  3. Haz clic en el cuadro de texto del nombre de usuario. Aparecerá un cuadro de diálogo.
  4. Selecciona la cuenta con la que quieres acceder.
  5. Verifica tu identidad con el bloqueo de pantalla del dispositivo. Se te redireccionará a la página de /home y accederás a ella.

Un cuadro de diálogo en el que se te solicita que verifiques tu identidad con la contraseña o llave de acceso que guardaste.

7. ¡Felicitaciones!

¡Terminaste este codelab! Si tienes alguna pregunta, hazla en la lista de distribución FIDO-DEV o en StackOverflow con una etiqueta passkey.

Más información