Publicado el / 6 min
Passkeys: hacia un inicio de sesión sin contraseñas
Qué cambia con WebAuthn y las passkeys, cómo es el registro y el login, y qué debe cuidar un equipo al migrar la autenticación.
Las contraseñas se reutilizan, se filtran y pueden escribirse en una página falsa. Añadir un código por SMS mejora algunos casos, pero no elimina toda la fricción ni todo el phishing. Las passkeys proponen otro contrato: el dispositivo guarda una clave privada y el servidor conserva una clave pública; para iniciar sesión el navegador pide demostrar posesión de la clave, no recordar una cadena de texto.
Qué es una passkey, sin promesas mágicas
Una passkey es una credencial basada en WebAuthn/FIDO. Al crearla, el autenticador genera un par de claves único para el servicio. La privada permanece bajo control del dispositivo o su proveedor de sincronización; la pública se registra en tu servidor. Para usarla, la persona desbloquea su autenticador con PIN, biometría u otro método local. La huella no se envía a tu backend.
El navegador vincula la operación al origen del sitio y al Relying Party ID (RP ID). Por eso una página impostora en otro dominio no puede simplemente pedir la misma credencial como si fuera el sitio real. Esto aporta resistencia al phishing y elimina el problema de reutilizar la misma contraseña en distintos servicios.
Pero «sin contraseña» no significa «sin riesgo»: todavía puedes perder acceso a dispositivos, tener una recuperación de cuenta mal diseñada, sufrir robo de sesión después del login o dar permisos excesivos a una cuenta comprometida.
El flujo completo en dos momentos
Registro: tu servidor autentica a la persona mediante un método existente, genera un challenge aleatorio de un solo uso y opciones de creación. El navegador pide al autenticador crear la credencial. El servidor valida la respuesta —challenge, origen, RP ID, verificación de usuario según tu política— y guarda identificador + clave pública + metadatos necesarios.
Autenticación: el servidor emite otro challenge. El autenticador lo firma con la clave privada y devuelve la prueba. El servidor verifica firma, challenge, origen y RP ID usando la credencial guardada; solo entonces crea la sesión. Nunca confíes en un verified: true enviado por el navegador: la verificación real ocurre del lado del servidor.
La API nativa usa navigator.credentials.create() y navigator.credentials.get(), pero exige convertir formatos binarios y validar respuestas correctamente. Para una implementación real conviene usar una biblioteca WebAuthn mantenida en ambos extremos. Este ejemplo usa @simplewebauthn/browser como referencia, no es una dependencia de este portafolio:
import { startRegistration } from "@simplewebauthn/browser";
async function crearPasskey() {
const optionsResponse = await fetch("/api/passkeys/registration/options", {
method: "POST", credentials: "same-origin",
});
if (!optionsResponse.ok) throw new Error("No se pudieron crear las opciones");
const optionsJSON = await optionsResponse.json();
const credential = await startRegistration({ optionsJSON });
const verification = await fetch("/api/passkeys/registration/verify", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(credential),
});
if (!verification.ok) throw new Error("Registro no verificado");
}
El servidor debe asociar las opciones a la sesión que inició el registro, no a un ID de usuario arbitrario incluido por el cliente. Una parte esencial de la verificación se ve así (fragmento: faltan almacenamiento, sesiones y gestión de errores de producción):
import { verifyRegistrationResponse } from "@simplewebauthn/server";
const challenge = await consumeChallenge(session.id); // leer y gastar una vez
const result = await verifyRegistrationResponse({
response: credential,
expectedChallenge: challenge,
expectedOrigin: "https://ejemplo.com",
expectedRPID: "ejemplo.com",
requireUserVerification: true,
});
if (!result.verified || !result.registrationInfo) throw new Error("Registro inválido");
await saveCredential(session.userId, result.registrationInfo.credential);
El login tiene pasos equivalentes con startAuthentication y verifyAuthenticationResponse. La verificación consulta la clave pública almacenada, rechaza challenges caducados o reutilizados y actualiza los metadatos de la credencial según la biblioteca. No copies un fragmento de UI y lo llames «autenticación lista» si falta el backend.
Consejo de migración: ofrece registrar una passkey después de un acceso exitoso y permite varias credenciales por cuenta. Una segunda passkey y un proceso de recuperación claro evitan que cambiar de teléfono se convierta en perder la cuenta.
Decisiones que suelen olvidarse
- Recuperación: diseña qué ocurre si la persona pierde todos sus dispositivos. Un enlace de recuperación inseguro puede deshacer la ventaja del login con passkey.
- Claves sincronizadas o ligadas al dispositivo: ambas existen; tu política de riesgo puede requerir distinguir escenarios. No asumas que biometría siempre está presente.
- Sesión posterior: una passkey protege el inicio de sesión, no las cookies robadas. Mantén protección CSRF donde corresponda, cookies seguras y revocación de sesiones.
- Origen y despliegue: WebAuthn necesita contexto seguro (
https, salvo excepciones de desarrollo), configuración correcta de RP ID y coherencia entre dominios. - Fallback: permite migración gradual sin obligar a conservar contraseñas para siempre; mide uso, errores y tasa de recuperación antes de retirar métodos anteriores.
¿Adiós a todas las contraseñas?
Para muchos productos, sí puede ser el destino. Para cada organización, el camino es una decisión de producto y seguridad: inscripción comprensible, soporte para varios dispositivos, recuperación robusta y backend que valide cada challenge. Empieza por ofrecer passkeys junto al método actual; cuando el flujo esté probado, podrás reducir la dependencia de contraseñas sin trasladar el problema al soporte.
Fuentes: passkeys.dev: What are passkeys? · WebAuthn en MDN · SimpleWebAuthn.