Registro de la 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 el registro de la llave de acceso:

Flujo de registro de la llave de acceso

  • Define opciones para crear una llave de acceso. Envíalas al cliente para que puedas pasarlas a la llamada de creación de llaves de acceso: la API de WebAuthn llama a navigator.credentials.create en la Web y credentialManager.createCredential en Android. Después de que el usuario confirma la creación de la llave de acceso, se resuelve la llamada de creación y se muestra una credencial PublicKeyCredential.
  • Verifica la credencial y almacénala en el servidor.

En las siguientes secciones, se profundiza en los detalles de cada paso.

Crea opciones de creación de credenciales

El primer paso que debes realizar en el servidor es crear un objeto PublicKeyCredentialCreationOptions.

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, generateRegistrationOptions.

PublicKeyCredentialCreationOptions debe incluir todo lo necesario para la creación de llaves de acceso: información sobre el usuario, sobre la RP y una configuración para las propiedades de la credencial que estás creando. Una vez que hayas definido todos estos elementos, pásalos según sea necesario a la función de tu biblioteca del servidor FIDO que es responsable de crear el objeto PublicKeyCredentialCreationOptions.

Parte de PublicKeyCredentialCreationOptions pueden ser constantes. Otros se deben definir de forma dinámica en el servidor:

  • rpId: Para propagar el ID de RP en el servidor, usa variables o funciones del servidor que te den el nombre de host de tu aplicación web, como example.com.
  • user.name y user.displayName: Para completar estos campos, usa la información de sesión del usuario que accedió (o la información de la cuenta de usuario nueva, si el usuario crea una llave de acceso durante el registro). user.name suele ser una dirección de correo electrónico y es única para la parte restringida. user.displayName es un nombre fácil de usar. Ten en cuenta que no todas las plataformas usarán displayName.
  • user.id: Es una cadena única y aleatoria que se genera cuando se crea la cuenta. Debe ser permanente, a diferencia de un nombre de usuario que se puede editar. El ID de usuario identifica una cuenta, pero no debe contener información de identificación personal (PII). Es probable que ya tengas un ID de usuario en tu sistema, pero, si es necesario, crea uno específicamente para las llaves de acceso para mantenerlo libre de PII.
  • excludeCredentials: Una lista de credenciales existentes IDs para evitar que se duplique una llave de acceso del proveedor de llaves de acceso. Para propagar este campo, busca las credenciales existentes de este usuario en tu base de datos. Consulta los detalles en Cómo impedir que se cree una llave de acceso nueva si ya existe.
  • challenge: En el caso del registro de credenciales, el desafío no es relevante, a menos que uses la certificación, una técnica más avanzada para verificar la identidad de un proveedor de llaves de acceso y los datos que emite. Sin embargo, incluso si no usas la certificación, el desafío sigue siendo un campo obligatorio. En ese caso, puedes establecer este desafío en un solo 0 para mayor simplicidad. Las instrucciones para crear una comprobación segura de autenticación están disponibles en Autenticación con llave de acceso del servidor.

Codificación y decodificación

PublicKeyCredentialCreationOptions que envió el servidor
PublicKeyCredentialCreationOptions enviado por el servidor. challenge, user.id y excludeCredentials.credentials deben estar codificados del servidor en base64URL para que PublicKeyCredentialCreationOptions se pueda entregar a través de HTTPS.

PublicKeyCredentialCreationOptions incluye campos que son ArrayBuffer, por lo que no son compatibles con JSON.stringify(). Esto significa que, por el momento, para entregar PublicKeyCredentialCreationOptions a través de HTTPS, algunos campos deben codificarse manualmente en el servidor mediante base64URL y, luego, decodificarse en el cliente.

  • En el servidor, la codificación y la decodificación se suelen encargar de la biblioteca del servidor FIDO.
  • En el cliente, la codificación y la decodificación deben realizarse de forma manual en este momento. Esto será más fácil en el futuro: estará disponible un método para convertir opciones de JSON a PublicKeyCredentialCreationOptions. Consulta el estado de la implementación en Chrome.

Código de ejemplo: crea opciones de creación de credenciales

Usamos la biblioteca de SimpleWebAuthn en nuestros ejemplos. Aquí, transferimos la creación de opciones de credenciales de clave pública a su función generateRegistrationOptions.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // 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 {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

Almacena la clave pública

PublicKeyCredentialCreationOptions que envió el servidor
navigator.credentials.create muestra un objeto PublicKeyCredential.

Cuando navigator.credentials.create se resuelve correctamente en el cliente, significa que se creó correctamente una llave de acceso. Se muestra un objeto PublicKeyCredential.

El objeto PublicKeyCredential contiene un objeto AuthenticatorAttestationResponse, que representa la respuesta del proveedor de llaves de acceso a la instrucción del cliente de crear una llave de acceso. Contiene información sobre la nueva credencial que necesitas como un RP para autenticar al usuario más tarde. Obtén más información sobre AuthenticatorAttestationResponse en el Apéndice: AuthenticatorAttestationResponse.

Envía el objeto PublicKeyCredential al servidor. Una vez que lo recibas, verifícalo.

Transfiere este paso de verificación a la biblioteca del servidor FIDO. Por lo general, ofrecerá una función de utilidad para este propósito. Ofrece SimpleWebAuthn, por ejemplo, verifyRegistrationResponse. Obtén más información sobre lo que sucede de forma interna en el Apéndice: Verificación de la respuesta de registro.

Una vez que la verificación sea exitosa, almacena la información de la credencial en tu base de datos para que el usuario pueda autenticarse más tarde con la llave de acceso asociada a esa credencial.

Usa una tabla dedicada para las credenciales de clave pública asociadas con las llaves de acceso. Un usuario solo puede tener una contraseña, pero puede tener varias llaves de acceso; por ejemplo, una sincronizada a través del llavero de iCloud de Apple y una mediante el Administrador de contraseñas de Google.

Este es un esquema de ejemplo que puedes usar para almacenar información de credenciales:

Esquema de base de datos para llaves de acceso

  • Tabla Users:
    • user_id: Es el ID del usuario principal. Es un ID aleatorio, único y permanente para el usuario. Usa esta clave como clave primaria para tu tabla Users.
    • username Un nombre de usuario definido por el usuario, potencialmente editable.
    • passkey_user_id: Es el ID de usuario sin PII específico de la llave de acceso, representado por user.id en tus opciones de registro. Cuando el usuario intente realizar la autenticación más tarde, el autenticador hará que passkey_user_id esté disponible en su respuesta de autenticación en userHandle. Te recomendamos que no establezcas passkey_user_id como clave primaria. Las claves primarias tienden a convertirse en PII de facto en los sistemas, ya que se usan ampliamente.
  • Tabla de Credenciales de clave pública:
    • id: ID de la credencial Usa esto como una clave primaria para tu tabla Credenciales de clave pública.
    • public_key: Clave pública de la credencial
    • passkey_user_id: Usa esto como una clave externa para establecer un vínculo con la tabla Users.
    • backed_up: Se crea una copia de seguridad de las llaves de acceso si el proveedor de llaves de acceso la sincroniza. Almacenar el estado de copia de seguridad es útil si quieres considerar descartar las contraseñas en el futuro para los usuarios que tengan backed_up llaves de acceso. Para verificar si se creó una copia de seguridad de la llave de acceso, examina las marcas de authenticatorData o usa una función de biblioteca del servidor FIDO que suele estar disponible para facilitar el acceso a esta información. Almacenar la elegibilidad de copia de seguridad puede ser útil para abordar las posibles consultas de los usuarios.
    • name: De manera opcional, es un nombre visible de la credencial para permitir que los usuarios asignen nombres personalizados a las credenciales.
    • transports: Es un array de transportes. El almacenamiento de transportes es útil para la experiencia del usuario de autenticación. Cuando los transportes están disponibles, el navegador puede comportarse de manera acorde y mostrar una IU que coincida con el transporte que usa el proveedor de llaves de acceso para comunicarse con los clientes, en particular para los casos de uso de reautenticación en los que allowCredentials no está vacío.

Puede ser útil almacenar otra información para mejorar la experiencia del usuario, incluidos elementos como el proveedor de llaves de acceso, la hora de creación de la credencial y la hora del último uso. Obtén más información en el artículo sobre diseño de la interfaz de usuario de las llaves de acceso.

Código de ejemplo: almacena la credencial

Usamos la biblioteca de SimpleWebAuthn en nuestros ejemplos. Aquí, transferimos la verificación de la respuesta del registro a su función verifyRegistrationResponse.

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // 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 {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('Verification failed.');
    }

    const { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

    // Kill the challenge for this session
    delete req.session.challenge;

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

Apéndice: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse contiene dos objetos importantes:

  • response.clientDataJSON es una versión JSON de los datos del cliente, que en la Web son datos tal como los ve el navegador. Contiene el origen de la RP, el desafío y androidPackageName si el cliente es una app para Android. Como RP, leer clientDataJSON te da acceso a la información que el navegador vio cuando se solicitó create.
  • response.attestationObject contiene dos datos:
    • attestationStatement, que no es relevante, a menos que uses una certificación.
    • authenticatorData son los datos tal como los ve el proveedor de llaves de acceso. Como RP, leer authenticatorData te da acceso a los datos que ve el proveedor de llaves de acceso y que se muestran en el momento de la solicitud de create.

authenticatorDatacontiene información esencial sobre la credencial de clave pública asociada con la llave de acceso recién creada:

  • La credencial de clave pública y su ID de credencial único
  • El ID de RP asociado con la credencial.
  • Marcas que describen el estado del usuario cuando se creó la llave de acceso: si el usuario estaba presente y si se verificó correctamente (consulta userVerification).
  • AAGUID, que identifica al proveedor de la llave de acceso Mostrar el proveedor de llaves de acceso puede ser útil para los usuarios, especialmente si tienen una llave de acceso registrada para tu servicio en varios proveedores de llaves de acceso.

Aunque authenticatorData está anidado en attestationObject, la información que contiene es necesaria para la implementación de la llave de acceso, sin importar si usas la certificación o no. authenticatorData está codificada y contiene campos codificados en un formato binario. Por lo general, tu biblioteca del servidor se encarga del análisis y la decodificación. Si no usas una biblioteca del servidor, considera aprovechar el cliente getAuthenticatorData() para ahorrarte un poco de trabajo de análisis y decodificación del servidor.

Apéndice: Verificación de la respuesta de registro

De forma interna, la verificación de la respuesta del registro consta de las siguientes verificaciones:

  • Asegúrate de que el ID de la RP coincida con tu sitio.
  • Asegúrate de que el origen de la solicitud sea el origen esperado para tu sitio (URL del sitio principal, app para Android).
  • Si necesitas la verificación del usuario, asegúrate de que la marca de verificación del usuario authenticatorData.uv esté true. Comprueba que la marca de presencia del usuario authenticatorData.up sea true, ya que las llaves de acceso siempre requieren su presencia.
  • Comprueba que el cliente haya podido presentar el desafío que te planteaste. Si no usas la certificación, esta verificación no es importante. Sin embargo, se recomienda implementar esta verificación, ya que garantiza que tu código esté listo si decides usar la certificación en el futuro.
  • Asegúrate de que el ID de la credencial aún no esté registrado para ningún usuario.
  • Verifica que el algoritmo que usa el proveedor de llaves de acceso para crear la credencial sea uno que hayas incluido (en cada campo alg de publicKeyCredentialCreationOptions.pubKeyCredParams, que normalmente se define dentro de la biblioteca del servidor y no es visible para ti). Esto garantiza que los usuarios solo puedan registrarse con los algoritmos que hayas elegido permitir.

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

Cuál es el próximo paso

Autenticación con llave de acceso del servidor