Descripción general
A continuación, se incluye una descripción general de los pasos clave involucrados en el registro de la llave de acceso:
- Define opciones para crear una llave de acceso. Envía los datos al cliente para que puedas pasarlos a la llamada de creación de la llave de acceso: la llamada a la API de WebAuthn
navigator.credentials.create
en la Web ycredentialManager.createCredential
en 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 profundiza en los detalles de cada paso.
Crea opciones de creación de credenciales
El primer paso que debes seguir en el servidor es crear un objeto PublicKeyCredentialCreationOptions
.
Para ello, usa tu biblioteca del servidor FIDO. Por lo general, ofrecerá una función de utilidad que puede crear estas opciones por ti. Por ejemplo, SimpleWebAuthn ofrece generateRegistrationOptions
.
PublicKeyCredentialCreationOptions
debe incluir todo lo necesario para la creación de la llave de acceso: información sobre el usuario, sobre el 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 completar 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.name
yuser.displayName
: Para completar estos campos, usa la información de la sesión del usuario que accedió a su cuenta (o la información de la cuenta del usuario nuevo, si el usuario está creando una llave de acceso durante el registro). Por lo general,user.name
es una dirección de correo electrónico y es única para el RP.user.displayName
es 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 editarse. 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 y asegúrate de que no contenga PII.excludeCredentials
: Es una lista de IDs de credenciales existentes para evitar la duplicación de una llave de acceso del proveedor de llaves de acceso. Para completar este campo, busca en tu base de datos las credenciales existentes para este usuario. Revisa los detalles en Cómo evitar 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 con llave de acceso del servidor.
Codificación y decodificación

PublicKeyCredentialCreationOptions
que envía el servidor. challenge
, user.id
y excludeCredentials.credentials
deben codificarse del lado del servidor en base64URL
, de modo que PublicKeyCredentialCreationOptions
se pueda entregar a través de HTTPS.PublicKeyCredentialCreationOptions
incluye campos que son ArrayBuffer
, por lo que JSON.stringify()
no los admite. Esto significa que, por el momento, para entregar PublicKeyCredentialCreationOptions
a través de HTTPS, algunos campos deben codificarse manualmente en el servidor con base64URL
y, luego, decodificarse en el cliente.
- En el servidor, la biblioteca del servidor FIDO suele encargarse de la codificación y decodificación.
- En el cliente, la codificación y la decodificación deben realizarse manualmente por el momento. En el futuro, será más fácil: estará disponible un método para convertir opciones como JSON en
PublicKeyCredentialCreationOptions
. Consulta el estado de la implementación en Chrome.
Código de ejemplo: crea opciones de creación de credenciales
En nuestros ejemplos, usamos la biblioteca SimpleWebAuthn. Aquí, le entregamos la creación de opciones de credenciales de clave pública a su función generateRegistrationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } 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: isoBase64URL.toBuffer(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: user.id,
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
requireResidentKey: true
},
});
// 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 correctamente. 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 para crear una llave de acceso. Contiene información sobre la nueva credencial que necesitas como RP para autenticar al usuario más adelante. Obtén más información sobre AuthenticatorAttestationResponse
en el Apéndice: AuthenticatorAttestationResponse
.
Envía el objeto PublicKeyCredential
al servidor. Una vez que la recibas, verifícala.
Entrega este paso de verificación a tu biblioteca del servidor FIDO. Por lo general, ofrecerá una función de utilidad para este propósito. Por ejemplo, SimpleWebAuthn ofrece verifyRegistrationResponse
. Obtén más información sobre lo que sucede en segundo plano 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 llave de acceso sincronizada a través de Llavero de iCloud de Apple y otra a través del Administrador de contraseñas de Google.
A continuación, se muestra un ejemplo de esquema 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. Úsalo como clave principal para tu tabla Users.username
: Es un nombre de usuario definido por el usuario que se puede editar.passkey_user_id
: Es el ID de usuario específico de la llave de acceso y sin PII, representado poruser.id
en tus opciones de registro. Cuando el usuario intente autenticarse más adelante, el autenticador pondrá estepasskey_user_id
a disposición en su respuesta de autenticación enuserHandle
. Te recomendamos que no establezcaspasskey_user_id
como clave principal. Las claves primarias tienden a convertirse en PII de facto en los sistemas, ya que se usan de forma extensa.
- Tabla de credenciales de clave pública:
id
: ID de credencial. Úsalo como clave principal para tu tabla de Credenciales de clave pública.public_key
: Es la clave pública de la credencial.passkey_user_id
: Úsalo 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 la sincroniza. Almacenar el estado de copia de seguridad es útil si deseas considerar la posibilidad de descartar 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 de BE enauthenticatorData
o 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 para 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 permite a los usuarios asignarle nombres personalizados.transports
: Es un array de transportes. El almacenamiento de transportes es útil para la experiencia del usuario de autenticación. Cuando los transportes están disponibles, el navegador puede comportarse de manera acorde y mostrar una IU que coincida con el transporte que usa el proveedor de claves de acceso para comunicarse con los clientes, en particular para los casos de uso de reautenticación en los queallowCredentials
no está vacío.
También puede ser útil almacenar otra información para mejorar la experiencia del usuario, como el proveedor de la llave de acceso, la hora de creación de la credencial y la hora del ú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í, le 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,
credentialPublicKey,
credentialID,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '000000-0000-0000-0000-00000000' ?
req.useragent?.platform : aaguids[aaguid].name;
const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
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.clientDataJSON
es una versión JSON de los datos del cliente, que en la Web son los datos que ve el navegador. Contiene el origen del RP, el desafío yandroidPackageName
si el cliente es una app para Android. Como RP, leerclientDataJSON
te da acceso a la información que vio el navegador en el momento de la solicitudcreate
.response.attestationObject
contiene dos elementos de información:attestationStatement
, que no es relevante a menos que uses la certificación.authenticatorData
son los datos que ve el proveedor de llaves de acceso. Como RP, la lectura deauthenticatorData
te da acceso a los datos que ve el proveedor de claves de acceso y que se devuelven en el momento de la solicitud decreate
.
authenticatorData
contiene 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 RP asociado con la credencial.
- Son marcas que describen el estado del usuario cuando se creó la llave de acceso: si el usuario estaba realmente presente y si se verificó correctamente (consulta la inmersión en userVerification).
- El AAGUID es un identificador del proveedor de la llave 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 Cómo determinar el proveedor de llaves de acceso con el AAGUID)
Aunque authenticatorData
está anidado dentro de attestationObject
, la información que contiene es necesaria para la implementación de tu llave de acceso, ya sea que uses la certificación o no. authenticatorData
está codificado y contiene campos que se codifican en formato binario. Por lo general, tu 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 trabajo de análisis y decodificación del servidor.
Apéndice: Verificación de la respuesta de registro
En segundo plano, la verificación de la respuesta de registro consta de las siguientes verificaciones:
- Asegúrate de que el ID de RP coincida con tu sitio.
- Asegúrate de que el origen de la solicitud sea un origen esperado para tu sitio (URL del sitio principal, app para Android).
- Si necesitas la verificación del usuario, asegúrate de que la marca de verificación del usuario
authenticatorData.uv
esté establecida entrue
. - Por lo general, se espera que la marca de presencia del usuario
authenticatorData.up
seatrue
, pero si la credencial se crea de forma condicional, se espera que seafalse
. - Comprueba 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, ya que 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 claves de acceso para crear la credencial sea uno de los que enumeraste (en cada campo
alg
depublicKeyCredentialCreationOptions.pubKeyCredParams
, que suele definirse dentro de tu 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 verifyRegistrationResponse
de SimpleWebAuthn o explora la lista completa de verificaciones en la especificación.