Compila tu primera app de WebAuthn

1. Antes de comenzar

La API de Web Authentication, también conocida como WebAuthn, te permite crear y utilizar credenciales de clave pública limitadas al origen para autenticar usuarios.

La API admite el uso de autenticadores U2F o FIDO2 con BLE, NFC y roaming de USB, también conocidos como llaves de seguridad, así como un autenticador de plataforma, que les permite a los usuarios autenticarse con sus huellas dactilares o bloqueos de pantalla.

En este codelab, compilarás un sitio web con una funcionalidad de reautenticación sencilla que utiliza un sensor de huellas dactilares. La reautenticación protege los datos de la cuenta porque obliga a los usuarios que ya accedieron a un sitio web a autenticarse nuevamente cuando intentan ingresar a secciones importantes del sitio web o volver a visitarlo después de un tiempo determinado.

Requisitos previos

  • Comprensión de los aspectos básicos del funcionamiento de WebAuthn
  • Habilidades básicas de programación con JavaScript

Actividades

  • Compila un sitio web con una funcionalidad de reautenticación sencilla que utiliza un sensor de huellas dactilares

Requisitos

  • Uno de los siguientes dispositivos:
    • Un dispositivo Android, preferentemente con un sensor biométrico
    • Un iPhone o iPad con ID táctil o ID facial en iOS 14 o versiones posteriores
    • Una MacBook Pro o Air con ID táctil en macOS Big Sur o versiones posteriores
    • Windows 10 19H1 o versiones posteriores con Windows Hello configurado
  • Uno de los siguientes navegadores:
    • Google Chrome 67 o versiones posteriores
    • Microsoft Edge 85 o versiones posteriores
    • Safari 14 o versiones posteriores

2. Prepárate

En este codelab, usarás un servicio llamado glitch. Aquí puedes editar el código del cliente y del servidor con JavaScript, además de implementarlos al instante.

Ve a https://glitch.com/edit=\"_/webauthn-codelab-start.

Ve cómo funciona

Sigue estos pasos para ver el estado inicial del sitio web:

  1. Haz clic en 62bb7a6aac381af8.png Mostrar > 3343769d04c09851.png En una ventana nueva para ver el sitio web publicado.
  2. Ingresa un nombre de usuario y haz clic en Siguiente.
  3. Ingresa una contraseña y haz clic en Acceder.

La contraseña se ignora, pero aún tienes la autenticación. De esta forma, llegas a la página principal.

  1. Haz clic en Reintentar autenticación y repite los pasos del dos al cuatro.
  2. Haz clic en Cerrar sesión.

Ten en cuenta que debes ingresar la contraseña cada vez que intentes acceder. De esta forma, se emula un usuario que debe volver a autenticarse antes de poder acceder a una sección importante de un sitio web.

Crea un remix del código

  1. Navega al Codelab de API de WebAuthn/FIDO2.
  2. Haz clic en el nombre de tu proyecto > Remix Project 306122647ce93305.png para bifurcarlo y continuar con tu propia versión en una URL nueva.

8d42bd24f0fd185c.png

3. Registra una credencial con una huella digital

Debes registrar una credencial generada por un UVPA, un autenticador integrado en el dispositivo que verifica la identidad del usuario. Aunque depende del dispositivo del usuario, por lo general, se ve como un sensor de huellas dactilares.

Agrega esta función a la página /home:

260aab9f1a2587a7.png

Crea la función registerCredential()

Crea una función registerCredential(), que registra una credencial nueva.

public/client.js

export const registerCredential = async () => {

};

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

Antes de solicitarle al usuario que registre una credencial nueva, pídele al servidor que muestre los parámetros para pasar en WebAuthn, incluido un desafío. Afortunadamente, ya tienes un extremo de servidor que cumple con esos parámetros.

Agrega el siguiente código a registerCredential().

public/client.js

const opts = {
  attestation: 'none',
  authenticatorSelection: {
    authenticatorAttachment: 'platform',
    userVerification: 'required',
    requireResidentKey: false
  }
};

const options = await _fetch('/auth/registerRequest', opts);

El protocolo entre un servidor y un cliente no forma parte de la especificación de WebAuthn. Sin embargo, este codelab está diseñado para cumplir con la especificación de WebAuthn y el objeto JSON que pasas al servidor es muy similar a PublicKeyCredentialCreationOptions, por lo que te resultará intuitivo. La siguiente tabla contiene los parámetros importantes que puedes pasar al servidor y, además, explica sus funciones:

Parámetros

Descripciones

attestation

Preferencia de transmisión de certificación: none, indirect o direct. Elige none, a menos que necesites uno.

excludeCredentials

Array de PublicKeyCredentialDescriptor para que el autenticador evite crear duplicados.

authenticatorSelection

authenticatorAttachment

Filtra los autenticadores disponibles. Si quieres adjuntar un autenticador al dispositivo, usa "platform". Para los autenticadores de roaming, usa "cross-platform".

userVerification

Determina si la verificación del usuario local del autenticador es "required", "preferred" o "discouraged". Si quieres usar la autenticación con huella digital o bloqueo de pantalla, usa "required".

requireResidentKey

Usa true si la credencial creada debe estar disponible para futuras UX del selector de cuentas.

Para obtener más información sobre estas opciones, consulta 5.4. Opciones para la creación de credenciales (diccionario PublicKeyCredentialCreationOptions).

Las siguientes son opciones de ejemplo que recibes del servidor.

{
  "rp": {
    "name": "WebAuthn Codelab",
    "id": "webauthn-codelab.glitch.me"
  },
  "user": {
    "displayName": "User Name",
    "id": "...",
    "name": "test"
  },
  "challenge": "...",
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    }, {
      "type": "public-key",
      "alg": -257
    }
  ],
  "timeout": 1800000,
  "attestation": "none",
  "excludeCredentials": [
    {
      "id": "...",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ],
  "authenticatorSelection": {
    "authenticatorAttachment": "platform",
    "userVerification": "required"
  }
}

Crea una credencial

  1. Dado que estas opciones se entregan codificadas para pasar por el protocolo HTTP, vuelve a convertir algunos parámetros en objetos binarios, específicamente user.id, challenge y, también, instancias de id incluidas en el array excludeCredentials:

public/client.js

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. Llama al método navigator.credentials.create() para crear una credencial nueva.

Con esta llamada, el navegador interactúa con el autenticador e intenta verificar la identidad del usuario con el UVPA.

public/client.js

const cred = await navigator.credentials.create({
  publicKey: options,
});

Cuando el usuario verifique su identidad, deberías recibir un objeto de credencial que puedes enviar al servidor para registrar el autenticador.

Registra la credencial en el extremo del servidor

Este es un objeto de credencial de ejemplo que deberías haber recibido.

{
  "id": "...",
  "rawId": "...",
  "type": "public-key",
  "response": {
    "clientDataJSON": "...",
    "attestationObject": "..."
  }
}
  1. Del mismo modo que cuando recibes un objeto de opción para registrar una credencial, codifica los parámetros binarios de la credencial para que se pueda entregar al servidor como una string:

public/client.js

const credential = {};
credential.id = cred.id;
credential.rawId = base64url.encode(cred.rawId);
credential.type = cred.type;

if (cred.response) {
  const clientDataJSON =
    base64url.encode(cred.response.clientDataJSON);
  const attestationObject =
    base64url.encode(cred.response.attestationObject);
  credential.response = {
    clientDataJSON,
    attestationObject,
  };
}
  1. Almacena el ID de las credenciales de forma local a fin de usarlo para la autenticación cuando el usuario regrese:

public/client.js

localStorage.setItem(`credId`, credential.id);
  1. Envía el objeto al servidor y, si muestra HTTP code 200, considera que la credencial nueva se registró correctamente.

public/client.js

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

Ahora tienes la función registerCredential() completa.

Código final para esta sección

public/client.js

...
export const registerCredential = async () => {
  const opts = {
    attestation: 'none',
    authenticatorSelection: {
      authenticatorAttachment: 'platform',
      userVerification: 'required',
      requireResidentKey: false
    }
  };

  const options = await _fetch('/auth/registerRequest', opts);

  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);
    }
  }

  const cred = await navigator.credentials.create({
    publicKey: options
  });

  const credential = {};
  credential.id =     cred.id;
  credential.rawId =  base64url.encode(cred.rawId);
  credential.type =   cred.type;

  if (cred.response) {
    const clientDataJSON =
      base64url.encode(cred.response.clientDataJSON);
    const attestationObject =
      base64url.encode(cred.response.attestationObject);
    credential.response = {
      clientDataJSON,
      attestationObject
    };
  }

  localStorage.setItem(`credId`, credential.id);

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

4. Compila la IU para registrar, obtener y quitar credenciales

Es bueno tener una lista de credenciales registradas y botones para quitarlas.

9b5b5ae4a7b316bd.png

Compila el marcador de posición de la IU

Agrega una IU para enumerar credenciales y un botón a fin de registrar una credencial nueva. Dependiendo de si la función está disponible o no, debes quitar la clase hidden del mensaje de advertencia o el botón para registrar una credencial nueva. ul#list es el marcador de posición para agregar una lista de credenciales registradas.

views/home.html

<p id="uvpa_unavailable" class="hidden">
  This device does not support User Verifying Platform Authenticator. You can't register a credential.
</p>
<h3 class="mdc-typography mdc-typography--headline6">
  Your registered credentials:
</h3>
<section>
  <div id="list"></div>
</section>
<mwc-button id="register" class="hidden" icon="fingerprint" raised>Add a credential</mwc-button>

Detección de funciones y disponibilidad de UVPA

Sigue estos pasos para verificar la disponibilidad de UVPA:

  1. Examina window.PublicKeyCredential para verificar si WebAuthn está disponible.
  2. Llama a PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() para verificar si hay un UVPA disponible. En caso afirmativo, verás el botón para registrar una credencial nueva. Si alguno no está disponible, se mostrará el mensaje de advertencia.

views/home.html

const register = document.querySelector('#register');

if (window.PublicKeyCredential) {
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(uvpaa => {
    if (uvpaa) {
      register.classList.remove('hidden');
    } else {
      document
        .querySelector('#uvpa_unavailable')
        .classList.remove('hidden');
    }
  });
} else {
  document
    .querySelector('#uvpa_unavailable')
    .classList.remove('hidden');
}

Obtén y muestra una lista de credenciales

  1. Crea una función getCredentials() para obtener credenciales registradas y mostrarlas en una lista. Afortunadamente, ya tienes un extremo práctico en el servidor /auth/getKeys, desde el que puedes obtener las credenciales registradas para el usuario que accedió.

El JSON que se muestra incluye información de credenciales, como id y publicKey. Puedes compilar HTML para mostrárselo al usuario.

views/home.html

const getCredentials = async () => {
  const res = await _fetch('/auth/getKeys');
  const list = document.querySelector('#list');
  const creds = html`${res.credentials.length > 0 ? res.credentials.map(cred => html`
    <div class="mdc-card credential">
      <span class="mdc-typography mdc-typography--body2">${cred.credId}</span>
      <pre class="public-key">${cred.publicKey}</pre>
      <div class="mdc-card__actions">
        <mwc-button id="${cred.credId}" @click="${removeCredential}" raised>Remove</mwc-button>
      </div>
    </div>`) : html`
    <p>No credentials found.</p>
    `}`;
  render(creds, list);
};
  1. Invoca getCredentials() para mostrar las credenciales disponibles en cuanto el usuario llega a la página /home.

views/home.html

getCredentials();

Quita la credencial

En la lista de credenciales, agregaste un botón para quitar cada credencial. Puedes enviar una solicitud a /auth/removeKey junto con el parámetro de búsqueda credId a fin de quitarlas.

public/client.js

export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
  1. Agrega unregisterCredential a la sentencia import existente.

views/home.html

import { _fetch, unregisterCredential } from '/client.js';
  1. Agrega una función a la que se llame cuando el usuario haga clic en Quitar.

views/home.html

const removeCredential = async e => {
  try {
    await unregisterCredential(e.target.id);
    getCredentials();
  } catch (e) {
    alert(e);
  }
};

Registra una credencial

Puedes llamar a registerCredential() para registrar una credencial nueva cuando el usuario haga clic en Agregar una credencial.

  1. Agrega registerCredential a la sentencia import existente.

views/home.html

import { _fetch, registerCredential, unregisterCredential } from '/client.js';
  1. Invoca registerCredential() con opciones para navigator.credentials.create().

No olvides renovar la lista de credenciales mediante una llamada a getCredentials() después del registro.

views/home.html

register.addEventListener('click', e => {
  registerCredential().then(user => {
    getCredentials();
  }).catch(e => alert(e));
});

Ahora deberías poder registrar una credencial nueva y mostrar la información correspondiente. Puedes probarla en tu sitio web publicado.

Código final para esta sección

views/home.html

...
      <p id="uvpa_unavailable" class="hidden">
        This device does not support User Verifying Platform Authenticator. You can't register a credential.
      </p>
      <h3 class="mdc-typography mdc-typography--headline6">
        Your registered credentials:
      </h3>
      <section>
        <div id="list"></div>
        <mwc-fab id="register" class="hidden" icon="add"></mwc-fab>
      </section>
      <mwc-button raised><a href="/reauth">Try reauth</a></mwc-button>
      <mwc-button><a href="/auth/signout">Sign out</a></mwc-button>
    </main>
    <script type="module">
      import { _fetch, registerCredential, unregisterCredential } from '/client.js';
      import { html, render } from 'https://unpkg.com/lit-html@1.0.0/lit-html.js?module';

      const register = document.querySelector('#register');

      if (window.PublicKeyCredential) {
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(uvpaa => {
          if (uvpaa) {
            register.classList.remove('hidden');
          } else {
            document
              .querySelector('#uvpa_unavailable')
              .classList.remove('hidden');
          }
        });
      } else {
        document
          .querySelector('#uvpa_unavailable')
          .classList.remove('hidden');
      }

      const getCredentials = async () => {
        const res = await _fetch('/auth/getKeys');
        const list = document.querySelector('#list');
        const creds = html`${res.credentials.length > 0 ? res.credentials.map(cred => html`
          <div class="mdc-card credential">
            <span class="mdc-typography mdc-typography--body2">${cred.credId}</span>
            <pre class="public-key">${cred.publicKey}</pre>
            <div class="mdc-card__actions">
              <mwc-button id="${cred.credId}" @click="${removeCredential}" raised>Remove</mwc-button>
            </div>
          </div>`) : html`
          <p>No credentials found.</p>
          `}`;
        render(creds, list);
      };

      getCredentials();

      const removeCredential = async e => {
        try {
          await unregisterCredential(e.target.id);
          getCredentials();
        } catch (e) {
          alert(e);
        }
      };

      register.addEventListener('click', e => {
        registerCredential({
          attestation: 'none',
          authenticatorSelection: {
            authenticatorAttachment: 'platform',
            userVerification: 'required',
            requireResidentKey: false
          }
        })
        .then(user => {
          getCredentials();
        })
        .catch(e => alert(e));
      });
    </script>
...

public/client.js

...
export const unregisterCredential = async (credId) => {
  localStorage.removeItem('credId');
  return _fetch(`/auth/removeKey?credId=${encodeURIComponent(credId)}`);
};
...

5. Autentica el usuario con una huella digital

Ahora tienes una credencial registrada y lista para ser la forma de autenticar al usuario. Lo siguiente es agregar la funcionalidad de reautenticación al sitio web. Esta es la experiencia del usuario:

Cuando un usuario llega a la página /reauth, ve un botón Autenticar siempre que la autenticación biométrica sea posible. La autenticación con una huella digital (UVPA) comienza cuando el usuario presiona Autenticar, luego se autentica y llega a la página /home. Si la autenticación biométrica no está disponible o falla, la IU recurre al uso de la contraseña existente.

b8770c4e7475b075.png

Crea la función authenticate()

Crea una función llamada authenticate(), que verifica la identidad del usuario con una huella digital. Agrega el código JavaScript aquí:

public/client.js

export const authenticate = async () => {

};

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

  1. Antes de la autenticación, examina si el usuario tiene un ID de credencial almacenado y, de ser así, configúralo como un parámetro de búsqueda.

Cuando proporcionas un ID de credencial junto con otras opciones, el servidor puede entregar allowCredentials relevantes para que la verificación del usuario sea confiable.

public/client.js

const opts = {};

let url = '/auth/signinRequest';
const credId = localStorage.getItem(`credId`);
if (credId) {
  url += `?credId=${encodeURIComponent(credId)}`;
}
  1. Antes de pedirle al usuario que se autentique, solicítale al servidor que muestre un desafío y otros parámetros. Llama a _fetch() con opts como un argumento para enviar una solicitud POST al servidor.

public/client.js

const options = await _fetch(url, opts);

A continuación, se muestran opciones de ejemplo que deberías recibir (cumplen con PublicKeyCredentialRequestOptions).

{
  "challenge": "...",
  "timeout": 1800000,
  "rpId": "webauthn-codelab.glitch.me",
  "userVerification": "required",
  "allowCredentials": [
    {
      "id": "...",
      "type": "public-key",
      "transports": [
        "internal"
      ]
    }
  ]
}

La opción más importante es allowCredentials. Cuando recibes opciones del servidor, allowCredentials debe ser un solo objeto en un array o un array vacío. Esto dependerá de que haya o no una credencial con el ID en el parámetro de búsqueda en el servidor.

  1. Resuelve la promesa con null si allowCredentials es un array vacío para que la IU solicite una contraseña.
if (options.allowCredentials.length === 0) {
  console.info('No registered credentials found.');
  return Promise.resolve(null);
}

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

  1. Dado que estas opciones se entregan codificadas para pasar por el protocolo HTTP, vuelve a convertir algunos parámetros en objetos binarios, específicamente challenge y, también, las instancias de id incluidas en el array allowCredentials:

public/client.js

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

for (let cred of options.allowCredentials) {
  cred.id = base64url.decode(cred.id);
}
  1. Llama al método navigator.credentials.get() para verificar la identidad del usuario con un UVPA.

public/client.js

const cred = await navigator.credentials.get({
  publicKey: options
});

Cuando el usuario verifique su identidad, deberías recibir un objeto de credencial que puedes enviar al servidor para autenticarlo.

Verifica la credencial

A continuación, se muestra un objeto de ejemplo PublicKeyCredential (response es AuthenticatorAssertionResponse) que deberías haber recibido:

{
  "id": "...",
  "type": "public-key",
  "rawId": "...",
  "response": {
    "clientDataJSON": "...",
    "authenticatorData": "...",
    "signature": "...",
    "userHandle": ""
  }
}
  1. Codifica los parámetros binarios de la credencial para que se pueda entregar al servidor como una string:

public/client.js

const credential = {};
credential.id = cred.id;
credential.type = cred.type;
credential.rawId = base64url.encode(cred.rawId);

if (cred.response) {
  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 y, si muestra HTTP code 200, considera que el usuario accedió correctamente:

public/client.js

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

Ahora tienes la función authentication() completa.

Código final para esta sección

public/client.js

...
export const authenticate = async () => {
  const opts = {};

  let url = '/auth/signinRequest';
  const credId = localStorage.getItem(`credId`);
  if (credId) {
    url += `?credId=${encodeURIComponent(credId)}`;
  }

  const options = await _fetch(url, opts);

  if (options.allowCredentials.length === 0) {
    console.info('No registered credentials found.');
    return Promise.resolve(null);
  }

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

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

  const cred = await navigator.credentials.get({
    publicKey: options
  });

  const credential = {};
  credential.id = cred.id;
  credential.type = cred.type;
  credential.rawId = base64url.encode(cred.rawId);

  if (cred.response) {
    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. Habilita la experiencia de reautenticación

Compila la IU

Cuando el usuario regrese, te sugerimos que vuelva a autenticarse de la manera más fácil y segura posible. Este es uno de los puntos fuertes de la autenticación biométrica. Sin embargo, hay casos en los que la autenticación biométrica puede no funcionar. Estos son algunos:

  • El UVPA no está disponible.
  • El usuario aún no registró ninguna credencial en su dispositivo.
  • Se libera el almacenamiento y el dispositivo ya no recuerda el ID de la credencial.
  • El usuario no puede verificar su identidad por algún motivo, por ejemplo, cuando tiene las manos mojadas o está usando una mascarilla.

Por eso, siempre es importante que ofrezcas otras opciones de acceso como resguardos. En este codelab, usarás la solución de contraseña basada en formularios.

19da999b0145054.png

  1. Agrega la IU para mostrar un botón de autenticación que invoque la autenticación biométrica además del formulario de contraseñas.

Usa la clase hidden para mostrar y ocultar de forma selectiva una de ellas según el estado del usuario.

views/reauth.html

<div id="uvpa_available" class="hidden">
  <h2>
    Verify your identity
  </h2>
  <div>
    <mwc-button id="reauth" raised>Authenticate</mwc-button>
  </div>
  <div>
    <mwc-button id="cancel">Sign-in with password</mwc-button>
  </div>
</div>
  1. Agrega class="hidden" al formulario:

views/reauth.html

<form id="form" method="POST" action="/auth/password" class="hidden">

Detección de funciones y disponibilidad de UVPA

Los usuarios deben acceder con una contraseña si se cumple una de las siguientes condiciones:

  • WebAuthn no está disponible.
  • UVPA no está disponible.
  • No se puede encontrar un ID de credencial para este UVPA.

Oculta o muestra el botón de autenticación de manera selectiva:

views/reauth.html

if (window.PublicKeyCredential) {
  PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(uvpaa => {
    if (uvpaa && localStorage.getItem(`credId`)) {
      document
        .querySelector('#uvpa_available')
        .classList.remove('hidden');
    } else {
      form.classList.remove('hidden');
    }
  });
} else {
  form.classList.remove('hidden');
}

Resguardo para el formulario de contraseña

El usuario también debería tener la posibilidad de acceder con una contraseña.

Muestra el formulario de contraseña y oculta el botón de autenticación cuando el usuario haga clic en Acceder con contraseña:

views/reauth.html

const cancel = document.querySelector('#cancel');
cancel.addEventListener('click', e => {
  form.classList.remove('hidden');
  document
    .querySelector('#uvpa_available')
    .classList.add('hidden');
});

c4a82800889f078c.png

Invoca la autenticación biométrica

Por último, habilita la autenticación biométrica.

  1. Agrega authenticate a la sentencia import existente:

views/reauth.html

import { _fetch, authenticate } from '/client.js';
  1. Invoca authenticate() cuando el usuario presione Autenticar para iniciar la autenticación biométrica.

Asegúrate de que se produzca un error de autenticación biométrica antes de ofrecer el formulario de contraseña.

views/reauth.html

const button = document.querySelector('#reauth');
button.addEventListener('click', e => {
  authenticate().then(user => {
    if (user) {
      location.href = '/home';
    } else {
      throw 'User not found.';
    }
  }).catch(e => {
    console.error(e.message || e);
    alert('Authentication failed. Use password to sign-in.');
    form.classList.remove('hidden');
    document.querySelector('#uvpa_available').classList.add('hidden');
  });
});

Código final para esta sección

views/reauth.html

...
    <main class="content">
      <div id="uvpa_available" class="hidden">
        <h2>
          Verify your identity
        </h2>
        <div>
          <mwc-button id="reauth" raised>Authenticate</mwc-button>
        </div>
        <div>
          <mwc-button id="cancel">Sign-in with password</mwc-button>
        </div>
      </div>
      <form id="form" method="POST" action="/auth/password" class="hidden">
        <h2>
          Enter a password
        </h2>
        <input type="hidden" name="username" value="{{username}}" />
        <div class="mdc-text-field mdc-text-field--filled">
          <span class="mdc-text-field__ripple"></span>
          <label class="mdc-floating-label" id="password-label">password</label>
          <input type="password" class="mdc-text-field__input" aria-labelledby="password-label" name="password" />
          <span class="mdc-line-ripple"></span>
        </div>
        <input type="submit" class="mdc-button mdc-button--raised" value="Sign-In" />
        <p class="instructions">password will be ignored in this demo.</p>
      </form>
    </main>
    <script src="https://unpkg.com/material-components-web@7.0.0/dist/material-components-web.min.js"></script>
    <script type="module">
      new mdc.textField.MDCTextField(document.querySelector('.mdc-text-field'));
      import { _fetch, authenticate } from '/client.js';
      const form = document.querySelector('#form');
      form.addEventListener('submit', e => {
        e.preventDefault();
        const form = new FormData(e.target);
        const cred = {};
        form.forEach((v, k) => cred[k] = v);
        _fetch(e.target.action, cred)
        .then(user => {
          location.href = '/home';
        })
        .catch(e => alert(e));
      });

      if (window.PublicKeyCredential) {
        PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
        .then(uvpaa => {
          if (uvpaa && localStorage.getItem(`credId`)) {
            document
              .querySelector('#uvpa_available')
              .classList.remove('hidden');
          } else {
            form.classList.remove('hidden');
          }
        });
      } else {
        form.classList.remove('hidden');
      }

      const cancel = document.querySelector('#cancel');
      cancel.addEventListener('click', e => {
        form.classList.remove('hidden');
        document
          .querySelector('#uvpa_available')
          .classList.add('hidden');
      });

      const button = document.querySelector('#reauth');
      button.addEventListener('click', e => {
        authenticate().then(user => {
          if (user) {
            location.href = '/home';
          } else {
            throw 'User not found.';
          }
        }).catch(e => {
          console.error(e.message || e);
          alert('Authentication failed. Use password to sign-in.');
          form.classList.remove('hidden');
          document.querySelector('#uvpa_available').classList.add('hidden');
        });
      });
    </script>
...

7. Felicitaciones.

¡Terminaste este codelab!

Más información

Agradecemos especialmente a Yuriy Ackermann de FIDO Alliance por su ayuda.