Descripción general
A continuación, se incluye una descripción general de los pasos clave que se deben seguir para registrar una llave de acceso:

- Define opciones para crear una llave de acceso. Envíalos al cliente para que puedas pasarlos a tu llamada de creación de llaves de acceso: la llamada a la API de WebAuthn
navigator.credentials.createen la Web ycredentialManager.createCredentialen Android. Después de que el usuario confirma la creación de la llave de acceso, se resuelve la llamada de creación de la llave de acceso y se devuelve una credencialPublicKeyCredential. - Verifica la credencial y almacénala en el servidor.
En las siguientes secciones, se analizan los detalles de cada paso.
Crea opciones de creación de credenciales
El primer paso que debes realizar en el servidor es crear un objeto PublicKeyCredentialCreationOptions.
Para ello, usa tu biblioteca de FIDO del servidor. Por lo general, ofrecerá una función de utilidad que puede crear estas opciones por ti. SimpleWebAuthn ofrece, por ejemplo, generateRegistrationOptions.
PublicKeyCredentialCreationOptions debe incluir todo lo necesario para la creación de llaves de acceso: información sobre el usuario, sobre la RP y una configuración para las propiedades de la credencial que estás creando. Una vez que hayas definido todos estos elementos, pásalos según sea necesario a la función de tu biblioteca del servidor de FIDO que se encarga de crear el objeto PublicKeyCredentialCreationOptions.
Algunos de los campos de PublicKeyCredentialCreationOptions pueden ser constantes. Otros deben definirse de forma dinámica en el servidor:
rpId: Para propagar el ID de RP en el servidor, usa funciones o variables del servidor que te proporcionen el nombre de host de tu aplicación web, comoexample.com.user.nameyuser.displayName: Para propagar estos campos, usa la información de la sesión del usuario que accedió (o la información de la cuenta del usuario nuevo, si el usuario está creando una llave de acceso durante el registro).user.namesuele ser una dirección de correo electrónico y es única para la RP.user.displayNamees un nombre fácil de usar. Ten en cuenta que no todas las plataformas usarándisplayName.user.id: Es una cadena aleatoria y única que se genera cuando se crea la cuenta. Debe ser permanente, a diferencia de un nombre de usuario, que puede ser editable. El ID de usuario identifica una cuenta, pero no debe contener información de identificación personal (PII). Es probable que ya tengas un ID de usuario en tu sistema, pero, si es necesario, crea uno específicamente para las llaves de acceso para que no contenga ninguna IIP.excludeCredentials: Es una lista de IDs de credenciales existentes para evitar que se duplique una llave de acceso del proveedor de llaves de acceso. Para completar este campo, busca en tu base de datos las credenciales existentes de este usuario. Revisa los detalles en Evita la creación de una llave de acceso nueva si ya existe una.challenge: Para el registro de credenciales, el desafío no es relevante, a menos que uses la certificación, una técnica más avanzada para verificar la identidad de un proveedor de llaves de acceso y los datos que emite. Sin embargo, incluso si no usas la certificación, el desafío sigue siendo un campo obligatorio. Las instrucciones para crear un desafío seguro para la autenticación están disponibles en Autenticación de llaves de acceso del servidor.
Codificación y decodificación
PublicKeyCredentialCreationOptions enviado por el servidor. Los campos binarios, como challenge y user.id, se codifican en el servidor en base64URL para que las opciones se puedan entregar como JSON a través de HTTPS.El objeto PublicKeyCredentialCreationOptions estándar de WebAuthn incluye campos que son ArrayBuffer, que no se pueden serializar directamente con JSON.stringify(). Para enviar estas opciones a través de HTTPS, los campos binarios se deben codificar como cadenas base64URL:
- En el servidor, tu biblioteca del servidor de FIDO (como SimpleWebAuthn) produce automáticamente un objeto de opciones serializable en JSON (p.ej.,
PublicKeyCredentialCreationOptionsJSON) con campos codificados en Base64URL. - En el cliente, los navegadores actualizados admiten
PublicKeyCredential.parseCreationOptionsFromJSON()para convertir las opciones de JSON directamente enPublicKeyCredentialCreationOptions. Cuando usas bibliotecas del cliente, como@simplewebauthn/browser, las funciones comostartRegistration({ optionsJSON: options })controlan esta conversión automáticamente.
Código de ejemplo: Crea opciones de creación de credenciales
En nuestros ejemplos, usamos la biblioteca SimpleWebAuthn. Aquí, entregamos la creación de opciones de credenciales de clave pública a su función generateRegistrationOptions.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers';
router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
const { user } = res.locals;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// `excludeCredentials` prevents users from re-registering existing
// credentials for a given passkey provider
const excludeCredentials = [];
const credentials = Credentials.findByUserId(user.id);
if (credentials.length > 0) {
for (const cred of credentials) {
excludeCredentials.push({
id: cred.id,
type: 'public-key',
transports: cred.transports,
});
}
}
// Generate registration options for WebAuthn create
const options = await generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.HOSTNAME,
userID: isoUint8Array.fromUTF8String(user.id),
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
residentKey: 'required',
userVerification: 'preferred',
},
});
// Keep the challenge in the session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Almacena la clave pública
navigator.credentials.create devuelve un objeto PublicKeyCredential.Cuando navigator.credentials.create se resuelve correctamente en el cliente, significa que se creó una llave de acceso de forma correcta. Se devuelve un objeto PublicKeyCredential.
El objeto PublicKeyCredential contiene un objeto AuthenticatorAttestationResponse, que representa la respuesta del proveedor de llaves de acceso a la instrucción del cliente de crear una llave de acceso. Contiene información sobre la credencial nueva que necesitas como RP para autenticar al usuario más adelante. Obtén más información sobre AuthenticatorAttestationResponse en Apéndice: AuthenticatorAttestationResponse.
Envía el objeto PublicKeyCredential al servidor. Una vez que lo recibas, verifícalo.
Entrega este paso de verificación a tu biblioteca del servidor de FIDO. Por lo general, ofrecerá una función de utilidad para este propósito. SimpleWebAuthn ofrece, por ejemplo, verifyRegistrationResponse. Obtén información sobre lo que sucede de forma interna en el Apéndice: Verificación de la respuesta de registro.
Una vez que la verificación se realice correctamente, almacena la información de las credenciales en tu base de datos para que el usuario pueda autenticarse más adelante con la llave de acceso asociada a esa credencial.
Usa una tabla dedicada para las credenciales de clave pública asociadas con las llaves de acceso. Un usuario solo puede tener una contraseña, pero puede tener varias llaves de acceso, por ejemplo, una sincronizada a través del Llavero de iCloud de Apple y otra a través del Administrador de contraseñas de Google.
A continuación, se muestra un esquema de ejemplo que puedes usar para almacenar información de credenciales:

- Tabla Users:
user_id: Es el ID de usuario principal. Es un ID aleatorio, único y permanente para el usuario. Usa este valor como clave primaria para tu tabla Users.username: Es un nombre de usuario definido por el usuario, que puede ser editable.passkey_user_id: Es el ID de usuario sin IIP específico de la llave de acceso, representado poruser.iden tus opciones de registro. Cuando el usuario intente autenticarse más adelante, el autenticador hará que estepasskey_user_idesté disponible en su respuesta de autenticación enuserHandle. Te recomendamos que no establezcaspasskey_user_idcomo clave primaria. Las claves primarias tienden a convertirse en PII de facto en los sistemas, ya que se usan de forma generalizada.
- Tabla Credenciales de clave pública:
id: Es el ID de la credencial. Usa este valor como clave primaria para tu tabla Credenciales de clave pública.public_key: Es la clave pública de la credencial.passkey_user_id: Usa este campo como clave externa para establecer un vínculo con la tabla Users.backed_up: Se crea una copia de seguridad de una llave de acceso si el proveedor de la llave de acceso la sincroniza. Almacenar el estado de la copia de seguridad es útil si quieres considerar dejar de usar contraseñas en el futuro para los usuarios que tienen llaves de acceso debacked_up. Puedes verificar si se creó una copia de seguridad de la llave de acceso examinando la marca BE enauthenticatorDatao usando una función de la biblioteca del servidor de FIDO que suele estar disponible para brindarte acceso fácil a esta información. Almacenar la elegibilidad de la copia de seguridad puede ser útil para abordar posibles consultas de los usuarios.name: De manera opcional, un nombre visible para la credencial que permita a los usuarios asignar nombres personalizados a las credenciales.transports: Es un array de transportes. Almacenar los transportes es útil para la experiencia del usuario de autenticación. Cuando los transportes están disponibles, el navegador puede comportarse según corresponda y mostrar una IU que coincida con el transporte que usa el proveedor de llaves de acceso para comunicarse con los clientes, en particular, para los casos de uso de reautenticación en los queallowCredentialsno está vacío.
Otra información puede ser útil para almacenar con fines de experiencia del usuario, incluidos elementos como el proveedor de llaves de acceso, la hora de creación de la credencial y la hora de último uso. Obtén más información en Diseño de la interfaz de usuario de las llaves de acceso.
Código de ejemplo: Almacena la credencial
En nuestros ejemplos, usamos la biblioteca SimpleWebAuthn.
Aquí, entregamos la verificación de la respuesta de registro a su función verifyRegistrationResponse.
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
const response = req.body;
// This sample code is for registering a passkey for an existing,
// signed-in user
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Verify the credential
const { verified, registrationInfo } = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
requireUserVerification: false,
});
if (!verified) {
throw new Error('Verification failed.');
}
const {
aaguid,
credential,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '00000000-0000-0000-0000-000000000000' ?
req.useragent?.platform : aaguids[aaguid]?.name;
const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: credential.id,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
transports: credential.transports || response.response.transports,
backed_up: credentialBackedUp,
registered_at: new Date().getTime()
});
// Kill the challenge for this session
delete req.session.challenge;
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Apéndice: AuthenticatorAttestationResponse
AuthenticatorAttestationResponse contiene dos objetos importantes:
response.clientDataJSONes una versión JSON de los datos del cliente, que en la Web son los datos tal como los ve el navegador. Contiene el origen de la RP, el desafío yandroidPackageNamesi el cliente es una app para Android. Como RP, leerclientDataJSONte brinda acceso a la información que vio el navegador en el momento de la solicitud decreate.response.attestationObjectcontiene dos tipos de información:attestationStatement, que no es relevante, a menos que uses la certificación.authenticatorDatason los datos tal como los ve el proveedor de la llave de acceso. Como RP, leerauthenticatorDatate brinda acceso a los datos que ve el proveedor de llaves de acceso y que se devuelven en el momento de la solicitud decreate.
authenticatorDatacontiene información esencial sobre la credencial de clave pública asociada con la llave de acceso recién creada:
- La credencial de clave pública en sí y un ID de credencial único para ella.
- Es el ID de la RP asociado con la credencial.
- Son marcas que describen el estado del usuario cuando se creó la llave de acceso: si el usuario estaba presente y si se verificó correctamente (consulta la información detallada sobre userVerification).
- El AAGUID es un identificador del proveedor de llaves de acceso, como el Administrador de contraseñas de Google. Según el AAGUID, puedes identificar al proveedor de la llave de acceso y mostrar el nombre en una página de administración de llaves de acceso. (consulta Determina el proveedor de la llave de acceso con el AAGUID).
Aunque authenticatorData está anidado dentro de attestationObject, la información que contiene es necesaria para la implementación de la llave de acceso, independientemente de si usas la certificación o no. authenticatorData está codificado y contiene campos que están codificados en un formato binario. Por lo general, la biblioteca del servidor se encargará del análisis y la decodificación. Si no usas una biblioteca del servidor, considera aprovechar getAuthenticatorData() del cliente para ahorrarte un poco de trabajo de análisis y decodificación en el servidor.
Apéndice: Verificación de la respuesta de registro
En segundo plano, la verificación de la respuesta de registro consiste en las siguientes comprobaciones:
- Asegúrate de que el ID de RP coincida con tu sitio.
- Asegúrate de que el origen de la solicitud sea el esperado para tu sitio (URL del sitio principal, app para Android).
- Si requieres la verificación del usuario, asegúrate de que la marca de verificación del usuario
authenticatorData.uvseatrue. - Por lo general, se espera que la marca de presencia del usuario
authenticatorData.upseatrue, pero si la credencial se crea de forma condicional, se espera que seafalse. - Verifica que el cliente haya podido proporcionar el desafío que le diste. Si no usas la certificación, esta verificación no es importante. Sin embargo, implementar esta verificación es una práctica recomendada: garantiza que tu código esté listo si decides usar la certificación en el futuro.
- Asegúrate de que el ID de credencial aún no esté registrado para ningún usuario.
- Verifica que el algoritmo que usa el proveedor de llaves de acceso para crear la credencial sea uno de los que indicaste (en cada campo
algdepublicKeyCredentialCreationOptions.pubKeyCredParams, que suele definirse dentro de la biblioteca del servidor y no es visible para ti). Esto garantiza que los usuarios solo puedan registrarse con los algoritmos que hayas elegido permitir.
Para obtener más información, consulta el código fuente de SimpleWebAuthn para verifyRegistrationResponse o consulta la lista completa de verificaciones en la especificación.