Registro de la llave de acceso del servidor

Descripción general

A continuación, se muestra una descripción general de los pasos clave relacionados con el registro de llaves 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 tu llamada de creación de llave 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 y muestra una credencial PublicKeyCredential.
  • Verifica la credencial y almacénala en el servidor.

En las siguientes secciones, se profundizan 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 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, generateRegistrationOptions

PublicKeyCredentialCreationOptions debe incluir todo lo necesario para crear la llave de acceso: información sobre el usuario, sobre el RP y una configuración para las propiedades de la credencial que estás creando. Una vez que hayas definido todo esto, pásalos según sea necesario a la función de la biblioteca del servidor FIDO que es responsable 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 del RP en el servidor, usa funciones o variables del servidor que te proporcionan el nombre de host de la 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 nueva, si el usuario crea una llave de acceso cuando se registra). user.name suele ser una dirección de correo electrónico y es única para el 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 única y aleatoria que se genera cuando se crea la cuenta. Debería 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 a fin de mantenerla libre de PII.
  • excludeCredentials: Una lista de los IDs de credenciales existentes 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. Revisa los detalles en Impedir 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 una 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 un desafío seguro de autenticación están disponibles en Autenticación con llave de acceso del servidor.

Codificación y decodificación

PublicKeyCredentialCreationOptions que envía el servidor
PublicKeyCredentialCreationOptions enviado por el servidor. challenge, user.id y excludeCredentials.credentials se deben codificar del lado del servidor en base64URL para que PublicKeyCredentialCreationOptions se pueda entregar a través de HTTPS.

PublicKeyCredentialCreationOptions incluyen 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 biblioteca del servidor FIDO suele encargarse de la codificación y decodificación.
  • En el cliente, la codificación y decodificación deben realizarse manualmente en este momento. 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

Estamos usando la biblioteca de SimpleWebAuthn en nuestros ejemplos. Aquí, entregamos 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 envía el servidor
navigator.credentials.create muestra un objeto PublicKeyCredential.

Cuando navigator.credentials.create se resuelva correctamente en el cliente, significa que se creó una llave de acceso de forma correcta. 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 para crear una llave de acceso. Contiene información sobre la nueva credencial que necesitas como 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. Cuando lo recibas, verifícalo.

Entrega este paso de verificación a la biblioteca del servidor de FIDO. Por lo general, ofrecerá una función de utilidad para este fin. Ofertas de SimpleWebAuthn, por ejemplo, verifyRegistrationResponse Obtén más información sobre lo que está sucediendo en detalle en el Apéndice: Verificación de la respuesta del registro.

Una vez que se complete la verificación, 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 puede tener una sola contraseña, pero puede tener varias llaves de acceso, por ejemplo, una sincronizada con el llavero de iCloud de Apple y una con 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 las llaves de acceso

  • Tabla Users:
    • user_id: Es el ID del usuario principal. Un ID aleatorio, único y permanente para el usuario. Úsala como clave primaria para la tabla Users.
    • username: Es un nombre de usuario definido por el usuario que se puede editar.
    • passkey_user_id: El ID de usuario sin PII específico de la llave de acceso, representado por user.id en las opciones de registro. Cuando el usuario intente autenticarse más tarde, el autenticador pondrá este passkey_user_id a disposición 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, debido a que se usan ampliamente.
  • Tabla de credenciales de clave pública:
    • id: ID de la credencial. Úsalo como clave primaria en tu tabla de Credenciales de clave pública.
    • public_key: Es la clave pública de la credencial.
    • passkey_user_id: Usa esto como clave externa para establecer un vínculo con la tabla Usuarios.
    • 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 la copia de seguridad es útil si deseas considerar descartar las contraseñas para los usuarios que tengan llaves de acceso backed_up en el futuro. Para comprobar si se creó una copia de seguridad de la llave de acceso, examina las marcas en authenticatorData o usa 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 las copias 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. Almacenar transportes es útil para la experiencia de autenticación del usuario. Cuando hay transportes disponibles, el navegador puede comportarse según corresponda y mostrar una IU que coincida con el transporte que el proveedor de llaves de acceso usa para comunicarse con los clientes, en particular para casos de uso de reautenticación en los que allowCredentials no está vacío.

Otra información puede ser útil para almacenar para la experiencia del usuario, incluidos elementos como el proveedor de la llave 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 el diseño de la interfaz de usuario de llaves de acceso.

Código de ejemplo: almacena la credencial

Estamos usando la biblioteca de SimpleWebAuthn en nuestros ejemplos. 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 { 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 que ve el navegador. Contiene el origen del RP, el desafío y androidPackageName si el cliente es una app para Android. Como RP, la lectura clientDataJSON te brinda acceso a la información que el navegador vio en el momento de la solicitud create.
  • response.attestationObject contiene dos datos:
    • attestationStatement, que no es relevante, a menos que uses una certificación.
    • authenticatorData son los datos que ve el proveedor de llaves de acceso. Como RP, la lectura authenticatorData te brinda acceso a los datos que ve el proveedor de llaves de acceso y que se muestran en el momento de la solicitud create.

authenticatorData contiene 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 en sí y un ID de credencial único para ella
  • Es el ID de la RP asociado con la credencial.
  • Marcas que describen el estado del usuario cuando se creó la llave de acceso: si un usuario estaba presente y si se lo verificó correctamente (consulta userVerification).
  • AAGUID, que identifica el proveedor de llaves 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 tu llave de acceso, independientemente de si usas la certificación. authenticatorData está codificada y contiene campos que están codificados en formato binario. Por lo general, la biblioteca del servidor se encarga del análisis y la decodificación. Si no usas una biblioteca del servidor, considera aprovechar getAuthenticatorData() del cliente para ahorrar un poco de trabajo de análisis y decodificación en el servidor.

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

De forma interna, la verificación de la respuesta de registro 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 sea un origen esperado para tu sitio (URL principal del sitio, app para Android).
  • Si necesitas la verificación del usuario, asegúrate de que el valor de la marca de verificación del usuario authenticatorData.uv sea true. Comprueba que la marca de presencia del usuario authenticatorData.up esté en true, ya que la presencia del usuario siempre es necesaria para las llaves de acceso.
  • 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, 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 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 por lo general se define dentro de la biblioteca del servidor y no es visible para ti). Esto garantiza que los usuarios solo se puedan registrar con los algoritmos que elijas permitir.

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

Cuál es el próximo paso

Autenticación con llave de acceso del servidor