Descripción general
A continuación, se muestra una descripción general de los pasos clave relacionados con la autenticación con llaves de acceso:
- Define el desafío y otras opciones necesarias para autenticar con una llave de acceso. Envíalos al cliente, de modo que puedas pasarlos a tu llamada de autenticación con llave de acceso (
navigator.credentials.get
en la Web). Después de que el usuario confirma la autenticación con llave de acceso, la llamada de autenticación se resuelve y muestra una credencial (PublicKeyCredential
), que 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 profundizan en los detalles de cada paso.
Crea el desafío
En la práctica, un desafío es un array de bytes aleatorios, representados como un objeto ArrayBuffer
.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
Para asegurarse de que el desafío cumpla su propósito, deberá hacer lo siguiente:
- Asegúrate de que nunca se use 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. También descarta el desafío después de un tiempo 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 debe ser prácticamente imposible de adivinar. Para crear un desafío del servidor con seguridad criptográfica, es mejor confiar en una biblioteca del servidor FIDO en la que confíes. Si 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 de longitud 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 credencial
Crea opciones de solicitud de credenciales como un objeto publicKeyCredentialRequestOptions
.
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, 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
.
Algunos de los campos de publicKeyCredentialRequestOptions
pueden ser constantes. Otros deben definirse de forma dinámica en el servidor:
rpId
: El ID del RP con el que esperas que se asocie la credencial, por ejemplo,example.com
La autenticación solo se realizará correctamente si el ID del RP que proporcionas aquí coincide con el de la RP asociado con la credencial. Para propagar el ID del RP, usa el mismo valor que el ID del RP que configuraste enpublicKeyCredentialCreationOptions
durante el registro de la credencial.challenge
: Es un dato que el proveedor de llaves de acceso firmará para demostrar que el usuario la tiene en el momento de la solicitud de autenticación. Revisa los detalles en Crea el desafío.allowCredentials
: Es una matriz 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 muestre el navegador. Consulta Cómo recuperar un desafío del servidor de la parte restringida y Análisis detallado de las credenciales detectables para obtener más detalles.userVerification
: Indica si la verificación del usuario mediante el bloqueo de pantalla del dispositivo es "obligatoria", "preferida" o "no recomendada". Consulta Cómo recuperar un desafío del servidor de RP.timeout
: Se refiere a cuánto tiempo (en milisegundos) puede tardar el usuario en completar la autenticación. Debe ser razonablemente generosa y más corta que la vida útil dechallenge
. El valor predeterminado recomendado es 5 minutos, pero puedes aumentarlo hasta 10 minutos, que sigue dentro del rango recomendado. Los tiempos de espera prolongados tienen sentido 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
Estamos usando la biblioteca de SimpleWebAuthn en nuestros ejemplos. Aquí, entregamos 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
.
El 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 elementos siguientes:
response.authenticatorData
yresponse.clientDataJSON
, como en el paso de registro de llaves 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 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.
- Encuentra en tu base de datos quién es el usuario. En el caso de las credenciales detectables, no sabrás 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, busca el usuario correspondiente mediante la clave externapasskey_user_id
en tu tabla Users.
- Opción 1: Usa
- Encuentra en tu base de datos la información de la credencial de clave pública que coincide con la aserción de autenticación que recibiste. Para hacerlo, en la tabla Credenciales de clave pública, busca la credencial
id
que coincida con la credencialid
presente en el objetoPublicKeyCredential
.
Verifica la aserción de autenticación. Entrega este paso de verificación a la biblioteca del servidor FIDO, que por lo general ofrecerá una función de utilidad para este propósito. Ofertas de SimpleWebAuthn, por ejemplo,
verifyAuthenticationResponse
Obtén más información sobre lo que está sucediendo en segundo plano en el Apéndice: Verificación de la respuesta de autenticación.Borra el desafío, independientemente de si la verificación se realizó 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ó. También puedes mostrar un objeto
user
al cliente para que el frontend pueda usar la información asociada con el usuario que acaba de acceder.
Ejemplo de código: Verifica y accede al usuario
Estamos usando la biblioteca de SimpleWebAuthn en nuestros ejemplos. Aquí, entregamos 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 verificaciones:
- Asegúrate de que el ID de 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 le diste.
- Verifica que, durante la autenticación, el usuario haya cumplido los requisitos que exiges como RP. 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 necesaria 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 para verificar la firma.
- Los datos firmados originales Estos son los datos cuya firma debe verificarse.
- 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 verifyAuthenticationResponse
de SimpleWebAuthn o consulta la lista completa de verificaciones en la especificación.