Passkeys y WebAuthn: guía práctica para implementar un login sin contraseñas

Las passkeys permiten iniciar sesión sin pedirle al usuario que memorice otra contraseña. Pero instalarlas bien no consiste en añadir un botón y llamar a navigator.credentials.create(). Hay que coordinar el navegador, el autenticador, el servidor y un plan de recuperación que no convierta la seguridad en un bloqueo.

En esta guía verás cómo pensar una implementación de passkeys con WebAuthn, qué datos debe conservar el backend, por qué importan el challenge, el origin y el RP ID, y cómo probar el flujo sin confundir una demostración de frontend con una autenticación realmente verificada.

Índice de Contenido 💻
  1. Passkey, WebAuthn y autenticador: no son exactamente lo mismo
  2. El mapa correcto del proceso
  3. Registro: qué debe ocurrir antes de crear la passkey
  4. El frontend no debe inventar la validación
  5. Qué debe verificar el backend
  6. Inicio de sesión sin nombre de usuario
  7. Recuperación: la parte que más implementaciones olvidan
  8. Errores frecuentes al pasar de demo a producción
  9. Checklist de lanzamiento
  10. Conclusión

Passkey, WebAuthn y autenticador: no son exactamente lo mismo

Una passkey es una credencial basada en un par de claves. La clave privada permanece dentro de un autenticador del dispositivo, un gestor de credenciales o una llave de seguridad. El servidor guarda la clave pública y la utiliza para comprobar firmas. MDN explica este modelo de clave pública con los flujos de registro e inicio de sesión.

WebAuthn es la API web que permite hablar con el autenticador. El navegador expone navigator.credentials.create() para registrar una credencial y navigator.credentials.get() para solicitar una aserción durante el acceso. El backend sigue siendo responsable de generar retos, comprobar respuestas y decidir si la sesión puede abrirse.

El mapa correcto del proceso

Divide la implementación en cuatro piezas:

  • Backend: crea retos de un solo uso, conoce la cuenta, guarda la clave pública y valida la respuesta.
  • Frontend: solicita las opciones al backend, convierte los datos a los tipos que espera WebAuthn y devuelve la credencial.
  • Navegador: aplica las reglas de contexto seguro y muestra la interacción con el autenticador.
  • Autenticador: protege la clave privada y exige la verificación local configurada por el usuario, como PIN o biometría.

Si el navegador crea una credencial pero el servidor no verifica su respuesta, solo tienes una interfaz convincente; no tienes login seguro.

Registro: qué debe ocurrir antes de crear la passkey

El usuario debe iniciar la acción de forma explícita, por ejemplo desde “Añadir passkey” dentro de su cuenta. El servidor genera un challenge aleatorio, lo asocia temporalmente a la cuenta y devuelve opciones parecidas a estas:

{
  "challenge": "base64url-generado-por-el-servidor",
  "rp": {
    "name": "Programador",
    "id": "programador.info"
  },
  "user": {
    "id": "identificador-estable-no-email",
    "name": "usuario@example.com",
    "displayName": "Nombre del usuario"
  },
  "pubKeyCredParams": [
    { "type": "public-key", "alg": -7 },
    { "type": "public-key", "alg": -257 }
  ],
  "excludeCredentials": [],
  "authenticatorSelection": {
    "residentKey": "required",
    "userVerification": "preferred"
  }
}

El user.id debe ser un identificador estable que no dependa de un correo que pueda cambiar. excludeCredentials sirve para no registrar dos veces la misma credencial. La documentación de web.dev sobre registro de passkeys también recomienda verificar la cuenta antes del alta y enviar una notificación cuando se registra una nueva credencial.

El frontend no debe inventar la validación

En el navegador, el frontend recibe las opciones del backend y llama a WebAuthn:

const options = await fetch('/auth/passkeys/register/options', {
  method: 'POST',
  credentials: 'include'
}).then(response => response.json());

const credential = await navigator.credentials.create({
  publicKey: decodeCreationOptions(options)
});

await fetch('/auth/passkeys/register/verify', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(serializeCredential(credential))
});

Las funciones de conversión no son decorativas: los campos binarios llegan como ArrayBuffer y normalmente se transportan como Base64URL. Convierte de forma consistente y rechaza respuestas incompletas. Nunca aceptes desde el cliente una clave pública o un identificador de usuario que el servidor no esperaba.

Qué debe verificar el backend

Durante el registro, el servidor debe comprobar al menos:

  1. Que el challenge coincide con uno generado recientemente y todavía no utilizado.
  2. Que el tipo de operación es el esperado: creación durante el registro o aserción durante el login.
  3. Que el origin corresponde al dominio autorizado.
  4. Que el RP ID coincide con la política del sitio.
  5. Que la respuesta tiene una credencial nueva y que su identificador no pertenece ya a otra cuenta.
  6. Que la firma y los datos del autenticador son válidos según una biblioteca WebAuthn mantenida.

La especificación WebAuthn Level 3 del W3C deja claro que el origin, el RP ID y el challenge forman parte del modelo de seguridad. La API se expone en contextos seguros, por lo que producción debe usar HTTPS sin errores de certificado.

Inicio de sesión sin nombre de usuario

Las credenciales discoverable permiten que el autenticador seleccione la cuenta sin que el usuario escriba primero su correo. El flujo es parecido:

const options = await fetch('/auth/passkeys/login/options', {
  method: 'POST',
  credentials: 'include'
}).then(response => response.json());

const assertion = await navigator.credentials.get({
  publicKey: decodeRequestOptions(options)
});

const result = await fetch('/auth/passkeys/login/verify', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(serializeAssertion(assertion))
});

El backend busca la clave pública asociada al identificador de credencial, comprueba la firma, valida el challenge de esa sesión y actualiza el contador del autenticador cuando la biblioteca lo requiera. Solo después crea la sesión normal de la aplicación.

Recuperación: la parte que más implementaciones olvidan

Una passkey mejora la resistencia al phishing, pero el usuario puede perder un dispositivo, borrar un gestor o perder acceso a una cuenta sincronizada. Antes de activar el flujo en producción define:

  • Cómo registrar varias passkeys por cuenta.
  • Cómo eliminar una credencial desde una sesión ya verificada.
  • Cómo avisar por correo cada alta y cada baja.
  • Cómo recuperar la cuenta con un proceso independiente, con límites, registro de auditoría y controles contra abuso.
  • Cómo evitar que el soporte desactive la protección solo con una petición informal.

El plan de recuperación no debe convertirse en el punto más débil del sistema. Si el atacante puede saltarse WebAuthn con un enlace de recuperación sin protección, la passkey solo protege el camino normal.

Errores frecuentes al pasar de demo a producción

  • Generar el challenge en JavaScript y confiar en él sin guardarlo y validarlo en el servidor.
  • Permitir cualquier origin porque “el navegador ya lo comprobó”.
  • Usar un RP ID distinto en producción, staging y subdominios sin documentar el alcance.
  • Guardar el correo como user.id, aunque pueda cambiar o exponga información personal.
  • Registrar una passkey sin notificar al propietario de la cuenta.
  • Suponer que una respuesta válida del navegador equivale a un usuario autenticado antes de verificar la firma.

Checklist de lanzamiento

  1. HTTPS válido en todos los dominios que participan en el flujo.
  2. Challenge aleatorio, corto y de un solo uso.
  3. Validación de origin, RP ID, firma y credencial en el backend.
  4. Varias credenciales por cuenta y pantalla para revocarlas.
  5. Notificación de altas, bajas y recuperaciones.
  6. Pruebas en autenticadores de plataforma y llaves externas.
  7. Plan de recuperación probado por soporte y por usuarios reales.

Conclusión

Las passkeys no son simplemente contraseñas guardadas en el teléfono. Son credenciales de clave pública cuyo valor depende de que el servidor controle el challenge, el origin, el RP ID, la firma y la recuperación. Si separas esas responsabilidades y pruebas el sistema fuera del caso feliz, puedes añadir un acceso más cómodo sin trasladar el riesgo al soporte o a una API mal validada.

Fuentes técnicas: MDN Passkeys, web.dev Passkey registration y W3C WebAuthn Level 3.

Si quieres conocer otros artículos parecidos a Passkeys y WebAuthn: guía práctica para implementar un login sin contraseñas puedes visitar la categoría Blog.

También puede gustarte:

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir