Descripción general
A continuación, se incluye una descripción general de alto nivel de los pasos clave relacionados con la autenticación con llave de acceso:
- Define el desafío y otras opciones necesarias para autenticar con una llave de acceso. Envíalas al cliente para poder pasarlas a la llamada de autenticación de tu llave de acceso (
navigator.credentials.get
en la Web). Después de que el usuario confirma la autenticación con la llave de acceso, esta se resuelve y muestra una credencial (PublicKeyCredential
). La credencial contiene una aserción de autenticación.
- Verifica la aserción de autenticación.
- Si la aserción de autenticación es válida, autentica al usuario.
En las siguientes secciones, se profundiza en los detalles de cada paso.
Crea el desafío
En la práctica, un desafío es un array de bytes aleatorios, representado como un objeto ArrayBuffer
.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
Para asegurarte de que el desafío cumpla su propósito, debes hacer lo siguiente:
- Asegúrate de que nunca se utilice el mismo desafío más de una vez. Genera un desafío nuevo en cada intento de acceso. Descarta el desafío después de cada intento de acceso, ya sea que se haya realizado correctamente o no. Descarte el desafío después de un período determinado. Nunca aceptes el mismo desafío en una respuesta más de una vez.
- Asegúrate de que el desafío sea seguro a nivel criptográfico. Un desafío debería ser prácticamente imposible de adivinar. Para crear un desafío criptográficamente seguro del lado del servidor, es mejor confiar en una biblioteca del servidor FIDO en la que confíes. Si, en cambio, creas tus propios desafíos, usa la funcionalidad criptográfica integrada disponible en tu pila tecnológica o busca bibliotecas diseñadas para casos de uso criptográficos. Los ejemplos incluyen iso-crypto en Node.js o secrets en Python. Según la especificación, el desafío debe tener al menos 16 bytes para que se considere seguro.
Una vez que hayas creado un desafío, guárdalo en la sesión del usuario para verificarlo más tarde.
Crea opciones de solicitud de credenciales
Crea opciones de solicitud de credenciales como un objeto publicKeyCredentialRequestOptions
.
Para hacerlo, confía en tu biblioteca del servidor FIDO. Por lo general, ofrecerá una función de utilidad que puede crear estas opciones por ti. Ofrece SimpleWebAuthn, por ejemplo, generateAuthenticationOptions
.
publicKeyCredentialRequestOptions
debe contener toda la información necesaria para la autenticación con llave de acceso. Pasa esta información a la función de la biblioteca del servidor FIDO que es responsable de crear el objeto publicKeyCredentialRequestOptions
.
Parte de publicKeyCredentialRequestOptions
pueden ser constantes. Otros se deben definir de forma dinámica en el servidor:
rpId
: Con qué ID de RP esperas que se asocie la credencial (por ejemplo,example.com
) La autenticación solo se realizará correctamente si el ID de la RP que proporcionas aquí coincide con el ID de la RP asociado con la credencial. Para propagar el ID de RP, usa el mismo valor que el ID de RP que estableciste enpublicKeyCredentialCreationOptions
durante el registro de la credencial.challenge
: Un dato que el proveedor de llaves de acceso firmará para demostrar que el usuario tiene la llave de acceso en el momento de la solicitud de autenticación. Revisa los detalles en Crea el desafío.allowCredentials
: Es un array de credenciales aceptables para esta autenticación. Pasa un array vacío para permitir que el usuario seleccione una llave de acceso disponible de una lista que muestra el navegador. Consulta Cómo recuperar un desafío del servidor RP y el artículo Análisis detallado de credenciales detectables para obtener más información.userVerification
: Indica si la verificación del usuario mediante el bloqueo de pantalla del dispositivo es "obligatoria" o "preferida". o desaconsejado. Consulta Cómo recuperar un desafío del servidor RP.timeout
: Indica cuánto tiempo (en milisegundos) puede tardar el usuario en completar la autenticación. Debe ser razonablemente generosa y menor que la vida útil delchallenge
. El valor predeterminado recomendado es 5 minutos, pero puedes aumentarlo (hasta 10 minutos), que sigue dentro del rango recomendado. Los tiempos de espera prolongados son adecuados si esperas que los usuarios usen el flujo de trabajo híbrido, que suele tardar un poco más. Si se agota el tiempo de espera de la operación, se arrojará unaNotAllowedError
.
Una vez que hayas creado publicKeyCredentialRequestOptions
, envíalo al cliente.
Código de ejemplo: crea opciones de solicitud de credenciales
Usamos la biblioteca de SimpleWebAuthn en nuestros ejemplos. Aquí, transferimos la creación de opciones de solicitud de credenciales a su función generateAuthenticationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
router.post('/signinRequest', csrfCheck, async (req, res) => {
// Ensure you nest 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 {
// Use the generateAuthenticationOptions function from SimpleWebAuthn
const options = await generateAuthenticationOptions({
rpID: process.env.HOSTNAME,
allowCredentials: [],
});
// Save the challenge in the user session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).json({ error: e.message });
}
});
Verifica el usuario y haz que acceda a él
Cuando navigator.credentials.get
se resuelve correctamente en el cliente, muestra un objeto PublicKeyCredential
.
response
es un AuthenticatorAssertionResponse
. Representa la respuesta del proveedor de llaves de acceso a la instrucción del cliente de crear lo necesario para intentar autenticarse con una llave de acceso en el RP. Contiene los siguientes elementos:
response.authenticatorData
yresponse.clientDataJSON
, como en el paso de registro de la llave de acceso.response.signature
, que contiene una firma sobre estos valores
Envía el objeto PublicKeyCredential
al servidor.
En el servidor, haz lo siguiente:
- Recopila la información que necesitarás para verificar la aserción y autenticar al usuario:
- Obtén el desafío esperado que almacenaste en la sesión cuando generaste las opciones de autenticación.
- Obtén el origen y el ID de RP esperados.
- Busca en tu base de datos quién es el usuario. En el caso de las credenciales detectables, no se sabe quién es el usuario que realiza la solicitud de autenticación. Para averiguarlo, tienes dos opciones:
- Opción 1: Usa
response.userHandle
en el objetoPublicKeyCredential
. En la tabla Usuarios, busca elpasskey_user_id
que coincida conuserHandle
. - Opción 2: Usa la credencial
id
presente en el objetoPublicKeyCredential
. En la tabla Credenciales de clave pública, busca la credencialid
que coincida con la credencialid
presente en el objetoPublicKeyCredential
. Luego, usa la clave externapasskey_user_id
para buscar el usuario correspondiente en tu tabla Users.
- Opción 1: Usa
- Busca en tu base de datos la información de la credencial de clave pública que coincida con la aserción de autenticación que recibiste. Para ello, en la tabla Credenciales de clave pública, busca la credencial
id
que coincida con laid
presente en el objetoPublicKeyCredential
.
Verifica la aserción de autenticación. Pasa este paso de verificación a la biblioteca del servidor FIDO, que generalmente ofrecerá una función de utilidad para este fin. Ofrece SimpleWebAuthn, por ejemplo,
verifyAuthenticationResponse
. Obtén información sobre lo que sucede de forma interna en el Apéndice: Verificación de la respuesta de autenticación.Borra el desafío independientemente de si la verificación se realiza correctamente o no para evitar ataques de repetición.
Haz que el usuario acceda. Si la verificación se realizó correctamente, actualiza la información de la sesión para indicar que el usuario accedió. Es posible que también desees mostrar un objeto
user
al cliente, de modo que el frontend pueda usar la información asociada con el usuario que acaba de acceder.
Código de ejemplo: verifica el usuario y accede
Usamos la biblioteca de SimpleWebAuthn en nuestros ejemplos. Aquí, transferimos la verificación de la respuesta de autenticación a su función verifyAuthenticationResponse
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/signinResponse', csrfCheck, async (req, res) => {
const response = req.body;
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
// 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 {
// Find the credential stored to the database by the credential ID
const cred = Credentials.findById(response.id);
if (!cred) {
throw new Error('Credential not found.');
}
// Find the user - Here alternatively we could look up the user directly
// in the Users table via userHandle
const user = Users.findByPasskeyUserId(cred.passkey_user_id);
if (!user) {
throw new Error('User not found.');
}
// Base64URL decode some values
const authenticator = {
credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
credentialID: isoBase64URL.toBuffer(cred.id),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
authenticator,
requireUserVerification: false,
});
if (!verified) {
throw new Error('User verification failed.');
}
// Kill the challenge for this session.
delete req.session.challenge;
req.session.username = user.username;
req.session['signed-in'] = 'yes';
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).json({ error: e.message });
}
});
Apéndice: Verificación de la respuesta de autenticación
La verificación de la respuesta de autenticación consta de las siguientes comprobaciones:
- Asegúrate de que el ID de la RP coincida con tu sitio.
- Asegúrate de que el origen de la solicitud coincida con el origen de acceso de tu sitio. En el caso de las apps para Android, consulta Verificar el origen.
- Comprueba que el dispositivo haya podido cumplir con el desafío que te diste.
- Verifica que, durante la autenticación, el usuario haya seguido los requisitos que exiges como parte restringida. Si necesitas la verificación del usuario, asegúrate de que la marca
uv
(verificada por el usuario) enauthenticatorData
seatrue
. Comprueba que la marcaup
(usuario presente) enauthenticatorData
seatrue
, ya que la presencia del usuario siempre es obligatoria para las llaves de acceso. - Verifica la firma. Para verificar la firma, necesitas lo siguiente:
- La firma, que es el desafío firmado:
response.signature
- La clave pública, con la que se debe verificar la firma.
- Son los datos firmados originales. Estos son los datos cuya firma se debe verificar.
- El algoritmo criptográfico que se usó para crear la firma.
- La firma, que es el desafío firmado:
Para obtener más información sobre estos pasos, consulta el código fuente de SimpleWebAuthn para verifyAuthenticationResponse
o revisa la lista completa de verificaciones en la especificación.