Autenticación con llave de acceso del servidor

Descripción general

A continuación, se incluye una descripción general de los pasos clave que se deben seguir para la autenticación con llaves de acceso:

Flujo de autenticación con llave de acceso

  • Define el desafío y otras opciones necesarias para autenticarte con una llave de acceso. Envíalos al cliente para 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, se resuelve la llamada de autenticación con llave de acceso y se devuelve 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 analizan 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 garantizar que el desafío cumpla con su propósito, debes hacer lo siguiente:

  1. 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 haya tenido éxito o no. También descarta el desafío después de un período determinado. Nunca aceptes el mismo desafío en una respuesta más de una vez.
  2. Asegúrate de que el desafío sea criptográficamente seguro. Un desafío debería ser prácticamente imposible de adivinar. Para crear un desafío criptográficamente seguro del lado del servidor, lo mejor es confiar en una biblioteca de FIDO del lado del servidor de confianza. 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. Algunos ejemplos son 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.

Opciones de solicitud de creación de credenciales

Crea opciones de solicitud de credenciales como un objeto publicKeyCredentialRequestOptions.

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, 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 tu biblioteca del servidor de FIDO que se encarga 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: Es el ID de RP con el que esperas que se asocie la credencial, por ejemplo, example.com. La autenticación solo se realizará correctamente si el ID de RP que proporcionas aquí coincide con el ID de RP asociado con la credencial. Para propagar el ID de RP, usa el mismo valor que el ID de RP que estableciste en publicKeyCredentialCreationOptions durante el registro de credenciales.
  • challenge: Es 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 Recupera un desafío del servidor de RP y Análisis detallado de las credenciales detectables para obtener más detalles.
  • userVerification: Indica si la verificación del usuario con el bloqueo de pantalla del dispositivo es "obligatoria", "preferida" o "no recomendada". Consulta Cómo recuperar un desafío del servidor de la RP.
  • timeout: Es el tiempo (en milisegundos) que puede tardar el usuario en completar la autenticación. Debe ser razonablemente generoso y más corto que la vida útil de la challenge. El valor predeterminado recomendado es de 5 minutos, pero puedes aumentarlo hasta 10 minutos, que aún se encuentra dentro del rango recomendado. Los tiempos de espera largos 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á un NotAllowedError.

Una vez que hayas creado el publicKeyCredentialRequestOptions, envíaselo al cliente.

publicKeyCredentialCreationOptions enviado por el servidor
Opciones que envía el servidor. challenge se decodifica del lado del cliente.

Código de ejemplo: Crea opciones de solicitud de credenciales

En nuestros ejemplos, usamos la biblioteca SimpleWebAuthn. Aquí, entregamos la creación de las opciones de solicitud de credenciales a su función generateAuthenticationOptions.

import {
  generateAuthenticationOptions,
} 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 al usuario y haz que acceda

Cuando navigator.credentials.get se resuelve correctamente en el cliente, devuelve un objeto PublicKeyCredential.

Objeto PublicKeyCredential enviado por el servidor
navigator.credentials.get devuelve un PublicKeyCredential.

El response es un AuthenticatorAssertionResponse. Representa la respuesta del proveedor de llaves de acceso a la instrucción del cliente de crear lo que se necesita para intentar autenticarse con una llave de acceso en la RP. Contiene los siguientes elementos:

  • response.authenticatorDatayresponse.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:

Esquema de base de datos
Esquema de base de datos sugerido. Obtén más información sobre este diseño en Registro de llaves de acceso del servidor.
  • 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 sabes quién es el usuario que realiza una solicitud de autenticación. Para averiguarlo, tienes dos opciones:
      • Opción 1: Usa el response.userHandle en el objeto PublicKeyCredential. En la tabla Usuarios, busca el passkey_user_id que coincida con userHandle.
      • Opción 2: Usa la credencial id presente en el objeto PublicKeyCredential. En la tabla Credenciales de clave pública, busca la credencial id que coincida con la credencial id presente en el objeto PublicKeyCredential. Luego, busca el usuario correspondiente con la clave externa passkey_user_id de tu tabla Users.
    • Busca en tu base de datos la información de credenciales 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 la credencial id presente en el objeto PublicKeyCredential.
  • Verifica la aserción de autenticación. Entrega este paso de verificación a tu biblioteca del servidor de FIDO, que, por lo general, ofrecerá una función de utilidad para este propósito. SimpleWebAuthn ofrece, 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, ya sea que la verificación se realice correctamente o no, para evitar ataques de reproducción.

  • Realiza el acceso del usuario. Si la verificación se realizó correctamente, actualiza la información de la sesión para marcar al usuario como conectado. También puedes devolver un objeto user al cliente para que el frontend pueda usar la información asociada con el usuario que acaba de acceder.

Código de ejemplo: Verifica al usuario y haz que acceda

En nuestros ejemplos, usamos la biblioteca SimpleWebAuthn. Aquí, entregamos la verificación de la respuesta de autenticación a su función verifyAuthenticationResponse.

import {
  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.');
    }
    // Prepare credential information for verification
    const credential = {
      id: cred.id,
      publicKey: isoBase64URL.toBuffer(cred.publicKey),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      credential,
      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 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 coincida con el origen de acceso de tu sitio. En el caso de las apps para Android, consulta Verifica el origen.
  • Verifica que el dispositivo haya podido proporcionar el desafío que le diste.
  • Verifica que, durante la autenticación, el usuario haya seguido los requisitos que exiges como RP. Si requieres la verificación del usuario, asegúrate de que la marca uv (usuario verificado) en authenticatorData sea true. Verifica que la marca up (usuario presente) en authenticatorData sea true, 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, para verificar la firma.
    • Son los datos firmados originales. Estos son los datos cuya firma se verificará.
    • Es el algoritmo criptográfico que se usó para crear la firma.

Para obtener más información sobre estos pasos, consulta el código fuente de SimpleWebAuthn para verifyAuthenticationResponse o consulta la lista completa de verificaciones en la especificación.