Registro de la 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 registrar una llave de acceso:

Flujo de registro de la llave de acceso

  • Define opciones para crear una llave de acceso. Envíalos al cliente para que puedas pasarlos a tu llamada de creación de llaves de acceso: la llamada a la API de WebAuthn 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 de la llave de acceso y se devuelve una credencial PublicKeyCredential.
  • Verifica la credencial y almacénala en el servidor.

En las siguientes secciones, se analizan 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 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, 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 de FIDO que se encarga de crear el objeto PublicKeyCredentialCreationOptions.

Algunos de los campos de PublicKeyCredentialCreationOptions pueden ser constantes. Otros deben definirse de forma dinámica en el servidor:

  • rpId: Para propagar el ID de RP en el servidor, usa funciones o variables del servidor que te proporcionen el nombre de host de tu aplicación web, como example.com.
  • user.name y user.displayName: Para propagar estos campos, usa la información de la sesión del usuario que accedió (o la información de la cuenta del usuario nuevo, si el usuario está creando una llave de acceso durante el registro). user.name suele ser una dirección de correo electrónico y es única para la RP. 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 aleatoria y única que se genera cuando se crea la cuenta. Debe ser permanente, a diferencia de un nombre de usuario, que puede ser editable. 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 que no contenga ninguna IIP.
  • excludeCredentials: Es una lista de IDs de credenciales existentes para evitar que se duplique una llave de acceso del proveedor de llaves de acceso. Para completar este campo, busca en tu base de datos las credenciales existentes de este usuario. Revisa los detalles en Evita la creación de una llave de acceso nueva si ya existe una.
  • challenge: Para el 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. Las instrucciones para crear un desafío seguro para la autenticación están disponibles en Autenticación de llaves de acceso del servidor.

Codificación y decodificación

PublicKeyCredentialCreationOptions enviado por el servidor
PublicKeyCredentialCreationOptions enviado por el servidor. Los campos binarios, como challenge y user.id, se codifican en el servidor en base64URL para que las opciones se puedan entregar como JSON a través de HTTPS.

El objeto PublicKeyCredentialCreationOptions estándar de WebAuthn incluye campos que son ArrayBuffer, que no se pueden serializar directamente con JSON.stringify(). Para enviar estas opciones a través de HTTPS, los campos binarios se deben codificar como cadenas base64URL:

  • En el servidor, tu biblioteca del servidor de FIDO (como SimpleWebAuthn) produce automáticamente un objeto de opciones serializable en JSON (p.ej., PublicKeyCredentialCreationOptionsJSON) con campos codificados en Base64URL.
  • En el cliente, los navegadores actualizados admiten PublicKeyCredential.parseCreationOptionsFromJSON() para convertir las opciones de JSON directamente en PublicKeyCredentialCreationOptions. Cuando usas bibliotecas del cliente, como @simplewebauthn/browser, las funciones como startRegistration({ optionsJSON: options }) controlan esta conversión automáticamente.

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

En nuestros ejemplos, usamos la biblioteca SimpleWebAuthn. Aquí, entregamos la creación de opciones de credenciales de clave pública a su función generateRegistrationOptions.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } 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: cred.id,
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

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

    // 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 enviado por el servidor
navigator.credentials.create devuelve un objeto PublicKeyCredential.

Cuando navigator.credentials.create se resuelve correctamente en el cliente, significa que se creó una llave de acceso de forma correcta. Se devuelve 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 credencial nueva que necesitas como RP para autenticar al usuario más adelante. Obtén más información sobre AuthenticatorAttestationResponse en Apéndice: AuthenticatorAttestationResponse.

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

Entrega este paso de verificación a tu biblioteca del servidor de FIDO. Por lo general, ofrecerá una función de utilidad para este propósito. SimpleWebAuthn ofrece, por ejemplo, verifyRegistrationResponse. Obtén 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 se realice correctamente, almacena la información de las credenciales en tu base de datos para que el usuario pueda autenticarse más adelante 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 otra a través del Administrador de contraseñas de Google.

A continuación, se muestra un esquema de ejemplo que puedes usar para almacenar información de credenciales:

Esquema de la base de datos para las llaves de acceso

  • Tabla Users:
    • user_id: Es el ID de usuario principal. Es un ID aleatorio, único y permanente para el usuario. Usa este valor como clave primaria para tu tabla Users.
    • username: Es un nombre de usuario definido por el usuario, que puede ser editable.
    • passkey_user_id: Es el ID de usuario sin IIP específico de la llave de acceso, representado por user.id en tus opciones de registro. Cuando el usuario intente autenticarse más adelante, el autenticador hará que este 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 de forma generalizada.
  • Tabla Credenciales de clave pública:
    • id: Es el ID de la credencial. Usa este valor como clave primaria para tu tabla Credenciales de clave pública.
    • public_key: Es la clave pública de la credencial.
    • passkey_user_id: Usa este campo como clave externa para establecer un vínculo con la tabla Users.
    • backed_up: Se crea una copia de seguridad de una llave de acceso si el proveedor de la llave de acceso la sincroniza. Almacenar el estado de la copia de seguridad es útil si quieres considerar dejar de usar contraseñas en el futuro para los usuarios que tienen llaves de acceso de backed_up. Puedes verificar si se creó una copia de seguridad de la llave de acceso examinando la marca BE en authenticatorData o usando una función de la biblioteca del servidor de FIDO que suele estar disponible para brindarte acceso fácil a esta información. Almacenar la elegibilidad de la copia de seguridad puede ser útil para abordar posibles consultas de los usuarios.
    • name: De manera opcional, un nombre visible para la credencial que permita a los usuarios asignar nombres personalizados a las credenciales.
    • transports: Es un array de transportes. Almacenar los transportes es útil para la experiencia del usuario de autenticación. Cuando los transportes están disponibles, el navegador puede comportarse según corresponda 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.

Otra información puede ser útil para almacenar con fines de experiencia del usuario, incluidos elementos como el proveedor de llaves de acceso, la hora de creación de la credencial y la hora de último uso. Obtén más información en Diseño de la interfaz de usuario de las llaves de acceso.

Código de ejemplo: Almacena la credencial

En nuestros ejemplos, usamos la biblioteca SimpleWebAuthn. Aquí, entregamos la verificación de la respuesta de 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 {
      aaguid,
      credential,
      credentialBackedUp
    } = registrationInfo;

    // Name the credential based on AAGUID
    const name =
      aaguid === undefined ||
      aaguid === '00000000-0000-0000-0000-000000000000' ?
        req.useragent?.platform : aaguids[aaguid]?.name;

    const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);

    // Existing, signed-in user
    const { user } = res.locals;

    // Save the credential
    await Credentials.update({
      id: credential.id,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      transports: credential.transports || response.response.transports,
      backed_up: credentialBackedUp,
      registered_at: new Date().getTime()
    });

    // 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 los 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 brinda acceso a la información que vio el navegador en el momento de la solicitud de create.
  • response.attestationObject contiene dos tipos de información:
    • attestationStatement, que no es relevante, a menos que uses la certificación.
    • authenticatorData son los datos tal como los ve el proveedor de la llave de acceso. Como RP, leer authenticatorData te brinda acceso a los datos que ve el proveedor de llaves de acceso y que se devuelven 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:

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

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

En segundo plano, la verificación de la respuesta de registro 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 sea el esperado para tu sitio (URL del sitio principal, app para Android).
  • Si requieres la verificación del usuario, asegúrate de que la marca de verificación del usuario authenticatorData.uv sea true.
  • Por lo general, se espera que la marca de presencia del usuario authenticatorData.up sea true, pero si la credencial se crea de forma condicional, se espera que sea false.
  • Verifica que el cliente haya podido proporcionar el desafío que le diste. Si no usas la certificación, esta verificación no es importante. Sin embargo, implementar esta verificación es una práctica recomendada: garantiza que tu código esté listo si decides usar la certificación en el futuro.
  • Asegúrate de que el ID de 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 de los que indicaste (en cada campo alg de publicKeyCredentialCreationOptions.pubKeyCredParams, que suele definirse 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 consulta la lista completa de verificaciones en la especificación.

Cuál es el próximo paso

Autenticación con llaves de acceso del servidor