Descripción general
A continuación, se muestra una descripción general de los pasos clave relacionados con el registro de llaves de acceso:
- Define opciones para crear una llave de acceso. Envíalas al cliente para que puedas pasarlas a tu llamada de creación de 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 y muestra una credencialPublicKeyCredential
. - Verifica la credencial y almacénala en el servidor.
En las siguientes secciones, se profundizan en 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 hacerlo, confía en la biblioteca del servidor FIDO. Por lo general, ofrecerá una función de utilidad que puede crear estas opciones por ti. Ofertas de SimpleWebAuthn, por ejemplo, generateRegistrationOptions
PublicKeyCredentialCreationOptions
debe incluir todo lo necesario para crear 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 todo esto, pásalos según sea necesario a la función de la biblioteca del servidor FIDO que es responsable 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 del RP en el servidor, usa funciones o variables del servidor que te proporcionan el nombre de host de la aplicación web, comoexample.com
.user.name
yuser.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 nueva, si el usuario crea una llave de acceso cuando se registra).user.name
suele ser 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 única y aleatoria que se genera cuando se crea la cuenta. Debería ser permanente, a diferencia de un nombre de usuario que se puede editar. 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 a fin de mantenerla libre de PII.excludeCredentials
: Una lista de los IDs de credenciales existentes para evitar que se duplique una llave de acceso del proveedor de llaves de acceso Para propagar este campo, busca las credenciales existentes de este usuario en tu base de datos. Revisa los detalles en Impedir 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 una 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. En ese caso, puedes establecer este desafío en un solo0
para mayor simplicidad. Las instrucciones para crear un desafío seguro de autenticación están disponibles en Autenticación con llave de acceso del servidor.
Codificación y decodificación
PublicKeyCredentialCreationOptions
incluyen campos que son ArrayBuffer
, por lo que no son compatibles con JSON.stringify()
. Esto significa que, por el momento, para entregar PublicKeyCredentialCreationOptions
a través de HTTPS, algunos campos deben codificarse manualmente en el servidor mediante 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 decodificación deben realizarse manualmente en este momento. Será más fácil en el futuro: estará disponible un método para convertir opciones de JSON a
PublicKeyCredentialCreationOptions
. Consulta el estado de la implementación en Chrome.
Código de ejemplo: Crea opciones de creación de credenciales
Estamos usando la biblioteca de SimpleWebAuthn en nuestros ejemplos. Aquí, 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 = 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
Cuando navigator.credentials.create
se resuelva correctamente en el cliente, significa que se creó una llave de acceso de forma correcta. Se muestra 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 tarde. Obtén más información sobre AuthenticatorAttestationResponse
en el Apéndice: AuthenticatorAttestationResponse
.
Envía el objeto PublicKeyCredential
al servidor. Cuando lo recibas, verifícalo.
Entrega este paso de verificación a la biblioteca del servidor de FIDO. Por lo general, ofrecerá una función de utilidad para este fin. Ofertas de SimpleWebAuthn, por ejemplo, verifyRegistrationResponse
Obtén más información sobre lo que está sucediendo en detalle en el Apéndice: Verificación de la respuesta del registro.
Una vez que se complete la verificación, almacena la información de la credencial en tu base de datos para que el usuario pueda autenticarse más tarde 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 puede tener una sola contraseña, pero puede tener varias llaves de acceso, por ejemplo, una sincronizada con el llavero de iCloud de Apple y una con el Administrador de contraseñas de Google.
Este es un esquema de ejemplo que puedes usar para almacenar información de credenciales:
- Tabla Users:
user_id
: Es el ID del usuario principal. Un ID aleatorio, único y permanente para el usuario. Úsala como clave primaria para la tabla Users.username
: Es un nombre de usuario definido por el usuario que se puede editar.passkey_user_id
: El ID de usuario sin PII específico de la llave de acceso, representado poruser.id
en las opciones de registro. Cuando el usuario intente autenticarse más tarde, 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 primaria. Las claves primarias tienden a convertirse en PII de facto en los sistemas, debido a que se usan ampliamente.
- Tabla de credenciales de clave pública:
id
: ID de la credencial. Úsalo como clave primaria en tu tabla de Credenciales de clave pública.public_key
: Es la clave pública de la credencial.passkey_user_id
: Usa esto como clave externa para establecer un vínculo con la tabla Usuarios.backed_up
: Se crea una copia de seguridad de las llaves de acceso si el proveedor de llaves de acceso la sincroniza. Almacenar el estado de la copia de seguridad es útil si deseas considerar descartar las contraseñas para los usuarios que tengan llaves de accesobacked_up
en el futuro. Para comprobar si se creó una copia de seguridad de la llave de acceso, examina las marcas enauthenticatorData
o usa 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 las copias de seguridad puede ser útil para abordar las posibles consultas de los usuarios.name
: De manera opcional, es un nombre visible de la credencial para permitir que los usuarios asignen nombres personalizados a las credenciales.transports
: Es un array de transportes. Almacenar transportes es útil para la experiencia de autenticación del usuario. Cuando hay transportes disponibles, el navegador puede comportarse según corresponda y mostrar una IU que coincida con el transporte que el proveedor de llaves de acceso usa para comunicarse con los clientes, en particular para casos de uso de reautenticación en los queallowCredentials
no está vacío.
Otra información puede ser útil para almacenar para la experiencia del usuario, incluidos elementos 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 el artículo sobre el diseño de la interfaz de usuario de llaves de acceso.
Código de ejemplo: almacena la credencial
Estamos usando la biblioteca de SimpleWebAuthn en nuestros ejemplos.
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 { credentialPublicKey, credentialID } = registrationInfo;
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
publicKey: base64PublicKey,
// Optional: set the platform as a default name for the credential
// (example: "Pixel 7")
name: req.useragent.platform,
transports: response.response.transports,
passkey_user_id: user.passkey_user_id,
backed_up: registrationInfo.credentialBackedUp
});
// 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 datos que ve el navegador. Contiene el origen del RP, el desafío yandroidPackageName
si el cliente es una app para Android. Como RP, la lecturaclientDataJSON
te brinda acceso a la información que el navegador vio en el momento de la solicitudcreate
.response.attestationObject
contiene dos datos:attestationStatement
, que no es relevante, a menos que uses una certificación.authenticatorData
son los datos que ve el proveedor de llaves de acceso. Como RP, la lecturaauthenticatorData
te brinda acceso a los datos que ve el proveedor de llaves de acceso y que se muestran en el momento de la solicitudcreate
.
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 la RP asociado con la credencial.
- Marcas que describen el estado del usuario cuando se creó la llave de acceso: si un usuario estaba presente y si se lo verificó correctamente (consulta
userVerification
). - AAGUID, que identifica el proveedor de llaves de acceso. Mostrar el proveedor de llaves de acceso puede ser útil para los usuarios, especialmente si tienen una llave de acceso registrada para tu servicio en varios proveedores de llaves de acceso.
Aunque authenticatorData
está anidado en attestationObject
, la información que contiene es necesaria para la implementación de tu llave de acceso, independientemente de si usas la certificación. authenticatorData
está codificada y contiene campos que están codificados en formato binario. Por lo general, la biblioteca del servidor se encarga del análisis y la decodificación. Si no usas una biblioteca del servidor, considera aprovechar getAuthenticatorData()
del cliente para ahorrar un poco de trabajo de análisis y decodificación en el servidor.
Apéndice: Verificación de la respuesta del registro
De forma interna, 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 principal del sitio, app para Android).
- Si necesitas la verificación del usuario, asegúrate de que el valor de la marca de verificación del usuario
authenticatorData.uv
seatrue
. Comprueba que la marca de presencia del usuarioauthenticatorData.up
esté entrue
, ya que la presencia del usuario siempre es necesaria para las llaves de acceso. - 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, 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 llaves de acceso para crear la credencial sea uno que hayas incluido (en cada campo
alg
depublicKeyCredentialCreationOptions.pubKeyCredParams
, que por lo general se define dentro de la biblioteca del servidor y no es visible para ti). Esto garantiza que los usuarios solo se puedan registrar con los algoritmos que elijas 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.