Autenticación con llave de acceso del servidor

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:

Flujo de 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:

  1. 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.
  2. 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 en publicKeyCredentialCreationOptions 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 del challenge. 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á una NotAllowedError.

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

publicKeyCredentialCreationOptions enviadas por el servidor
Opciones enviadas por el servidor. La decodificación de challenge se realiza del lado del 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.

Objeto PublicKeyCredential que envió el servidor
navigator.credentials.get 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.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 la 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 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 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, usa la clave externa passkey_user_id para buscar el usuario correspondiente en tu tabla Users.
    • 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 la id presente en el objeto PublicKeyCredential.
  • 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) en authenticatorData sea true. Comprueba 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, 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.

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.