Autenticación con llave de acceso del servidor

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:

Flujo de autenticación con llave 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:

  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 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.
  2. 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 en publicKeyCredentialCreationOptions 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 de 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 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á una NotAllowedError.

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

publicKeyCredentialCreationOptions que envió el servidor
Opciones que envía el servidor. La decodificación de challenge se realiza del lado del 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.

Objeto PublicKeyCredential que envió el servidor
navigator.credentials.get muestra 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 necesario para intentar autenticarse con una llave de acceso en el RP. Contiene los elementos siguientes:

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

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 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 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 mediante la clave externa passkey_user_id en tu tabla Users.
    • 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 credencial id presente en el objeto PublicKeyCredential.
  • 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) en authenticatorData sea true. Comprueba que la marca up (usuario presente) en authenticatorData sea true, 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.

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.