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
- Conocimientos básicos sobre JavaScript
- Conocimientos básicos sobre las llaves de acceso
- Conocimientos básicos sobre la API de Web Authentication (WebAuthn)
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
- Abre el proyecto en Glitch.
- Haz clic en Remix para bifurcar el proyecto de Glitch.
- 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.
Examina el estado inicial del sitio web
- En la pestaña de vista previa, ingresa un nombre de usuario aleatorio y haz clic en Next.
- 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.
- Si quieres cambiar tu nombre visible, hazlo. Eso es todo lo que puedes hacer en el estado inicial.
- 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.
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()
- En Glitch, navega al archivo
public/client.js
y, luego, desplázate hasta el final. - 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 |
Un desafío generado por el servidor en un objeto | |
El ID único del usuario. Este valor debe ser un objeto | |
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). | |
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. | |
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 | |
En este campo, se especifican los algoritmos de clave pública admitidos del RP. Te recomendamos configurarlos en | |
Proporciona una lista de IDs de credenciales ya registrados para evitar que se registre dos veces el mismo dispositivo. Si se proporciona, el miembro | |
Se establece en un valor | |
Establece en un valor booleano | |
Configúralo en un valor de |
Crea una credencial
- 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 cadenasuser.id
ychallenge
, y las instancias de la cadenaid
incluida en el arrayexcludeCredentials
:
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);
}
}
- En la línea siguiente, establece
authenticatorSelection.authenticatorAttachment
en"platform"
yauthenticatorSelection.requireResidentKey
entrue
. 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
}
- 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 |
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. | |
Una versión del objeto | |
Datos de cliente codificados en un objeto | |
Un objeto de certificación con codificación | |
Una lista de transportes que admite el dispositivo: | |
Muestra |
Para enviar el objeto de credencial al servidor, sigue estos pasos:
- 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
};
- 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.
Agrega el HTML de marcador de posición
- En Glitch, navega al archivo
views/home.html
. - 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:
- Al final del archivo
views/home.html
, después del comentario relevante, escribe un campo condicional que se ejecute siwindow.PublicKeyCredential
,PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable
yPublicKeyCredential.isConditionalMediationAvailable
sontrue
.
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) {
- 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()
]);
- 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
- 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);
};
- 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:
- 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';
- Al final del cuerpo de la sentencia
import
, agrega la funciónregisterCredential()
.
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';
- Al final del archivo después del comentario relevante, define una función
register()
que invoque la funciónregisterCredential()
y una IU de carga, y llame arenderCredentials()
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();
- En el cuerpo de la función
register()
, detecta excepciones. El métodonavigator.credentials.create()
muestra un errorInvalidStateError
cuando ya existe una llave de acceso en el dispositivo. Esto se analiza con el arrayexcludeCredentials
. En este caso, se muestra un mensaje relevante para el usuario. También se produce un errorNotAllowedError
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);
}
}
};
- En la línea después de la función
register()
, adjunta la funciónregister()
a un eventoclick
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:
- En la pestaña de vista previa, accede con un nombre de usuario y una contraseña aleatorios.
- Haz clic en Crear una llave de acceso.
- Verifica tu identidad con el bloqueo de pantalla del dispositivo.
- Confirma que la llave de acceso esté registrada y se muestre en la sección Your registered passkeys de la página web.
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 llamadaauthenticate()
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 solicitudPOST
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 |
Un desafío generado por el servidor en un objeto | |
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 | |
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. | |
Configúralo en un valor de |
Verifica el usuario de forma local y obtén una credencial
- En el cuerpo de la función
authenticate()
, después del comentario relevante, vuelve a convertir el parámetrochallenge
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);
- 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.
- Llama al método
navigator.credentials.get()
junto con una opciónmediation: '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 |
El ID codificado en Base64URL de la credencial de llave de acceso autenticada. | |
Una versión del objeto | |
Un objeto | |
Un objeto | |
Un objeto | |
Un objeto | |
Muestra una cadena |
Para enviar el objeto de credencial al servidor, sigue estos pasos:
- 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,
};
- 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.
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
- En el archivo
view/index.html
, después del comentario relevante, reemplaza la sentenciaimport
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.
- Confirma que el objeto
window.PulicKeyCredential
esté disponible y que el métodoPublicKeyCredential.isConditionalMediationAvailable()
muestre un valortrue
y, luego, llama a la funciónauthenticate()
:
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:
- Navega a la pestaña de vista previa.
- Si es necesario, sal de tu cuenta.
- Haz clic en el cuadro de texto del nombre de usuario. Aparecerá un cuadro de diálogo.
- Selecciona la cuenta con la que quieres acceder.
- Verifica tu identidad con el bloqueo de pantalla del dispositivo. Se te redireccionará a la página de
/home
y accederás a ella.
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
.