Passkeys y Autenticacion sin Contrasenas: Guia Completa 2026
El problema con las contrasenas en 2026
Las contrasenas han sido el metodo de autenticacion dominante durante decadas, pero su modelo esta fundamentalmente roto. Cada ano se filtran miles de millones de credenciales en brechas de seguridad, y la mayoria de los usuarios reutilizan las mismas contrasenas en multiples servicios.
Los ataques de phishing se han vuelto tan sofisticados que incluso usuarios tecnicos caen en ellos. Un correo bien elaborado, una pagina de login clonada, y las credenciales estan comprometidas. La autenticacion multifactor (MFA) mitiga parte del riesgo, pero agrega friccion al usuario y sigue siendo vulnerable a ataques de tiempo real como SIM swapping y MFA fatigue.

Que son las Passkeys (FIDO2/WebAuthn)
Las passkeys son credenciales criptograficas basadas en el estandar FIDO2 y la API WebAuthn. En lugar de compartir un secreto (la contrasena) con el servidor, las passkeys usan criptografia de clave publica: tu dispositivo genera un par de claves (publica y privada), y solo la clave publica se almacena en el servidor.
Cuando te autenticas, el servidor envia un desafio (challenge) que tu dispositivo firma con la clave privada. El servidor verifica la firma usando la clave publica almacenada. La clave privada nunca abandona tu dispositivo, lo que hace imposible el phishing tradicional.
Componentes clave del flujo
| Componente | Rol |
|---|---|
| Relying Party (RP) | Tu aplicacion web — el servidor que solicita autenticacion |
| Authenticator | El dispositivo que genera y almacena las claves (telefono, laptop, llave USB) |
| Client | El navegador que media entre el RP y el Authenticator via WebAuthn API |
Autenticadores de plataforma vs. cross-platform
Existen dos tipos de autenticadores FIDO2 que debes entender para disenar tu estrategia de passkeys:
- Platform authenticators: Integrados en el dispositivo — Touch ID, Face ID, Windows Hello, sensor de huella en Android. La clave privada se almacena en el chip de seguridad del dispositivo (TPM, Secure Enclave).
- Cross-platform (roaming) authenticators: Dispositivos externos como llaves de seguridad USB (YubiKey, Google Titan). Puedes llevarlos contigo y usarlos en cualquier computadora.

Implementando registro con WebAuthn en TypeScript
Vamos a implementar el flujo completo de registro de passkeys usando @simplewebauthn/server y @simplewebauthn/browser. Empecemos con el servidor:
1# Instalar dependencias
2npm install @simplewebauthn/server @simplewebauthn/browser
3npm install -D @simplewebauthn/types
Primero, el endpoint que genera las opciones de registro:
1// app/api/auth/register/options/route.ts
2import {
3 generateRegistrationOptions,
4 type GenerateRegistrationOptionsOpts,
5} from '@simplewebauthn/server';
6
7const RP_NAME = 'Mi App Segura';
8const RP_ID = 'miapp.com';
9
10export async function POST(request: Request) {
11 const { userId, userEmail } = await request.json();
12
13 // Obtener passkeys existentes del usuario (para excluirlas)
14 const existingCredentials = await db.credential.findMany({
15 where: { userId },
16 select: { credentialId: true, transports: true },
17 });
18
19 const opts: GenerateRegistrationOptionsOpts = {
20 rpName: RP_NAME,
21 rpID: RP_ID,
22 userName: userEmail,
23 userDisplayName: userEmail,
24 // No pedir de nuevo passkeys ya registradas
25 excludeCredentials: existingCredentials.map((c) => ({
26 id: c.credentialId,
27 transports: c.transports,
28 })),
29 authenticatorSelection: {
30 residentKey: 'preferred',
31 userVerification: 'preferred',
32 },
33 attestationType: 'none', // No necesitamos attestation en la mayoria de casos
34 };
35
36 const options = await generateRegistrationOptions(opts);
37
38 // Guardar el challenge en sesion para verificar despues
39 await saveChallenge(userId, options.challenge);
40
41 return Response.json(options);
42}
Este endpoint genera un challenge aleatorio y las opciones que el navegador necesita para crear una nueva credencial. Nota que excluimos credenciales existentes para evitar duplicados.
Verificacion del registro en el servidor
Una vez que el navegador crea la credencial, debemos verificarla en el servidor:
1// app/api/auth/register/verify/route.ts
2import {
3 verifyRegistrationResponse,
4 type VerifiedRegistrationResponse,
5} from '@simplewebauthn/server';
6
7export async function POST(request: Request) {
8 const body = await request.json();
9 const { userId } = body;
10
11 // Recuperar el challenge guardado
12 const expectedChallenge = await getChallenge(userId);
13 if (!expectedChallenge) {
14 return Response.json({ error: 'Challenge expirado' }, { status: 400 });
15 }
16
17 let verification: VerifiedRegistrationResponse;
18 try {
19 verification = await verifyRegistrationResponse({
20 response: body.credential,
21 expectedChallenge,
22 expectedOrigin: 'https://miapp.com',
23 expectedRPID: 'miapp.com',
24 });
25 } catch (error) {
26 console.error('Error de verificacion:', error);
27 return Response.json({ error: 'Verificacion fallida' }, { status: 400 });
28 }
29
30 const { verified, registrationInfo } = verification;
31
32 if (verified && registrationInfo) {
33 // Guardar la credencial en base de datos
34 await db.credential.create({
35 data: {
36 credentialId: registrationInfo.credential.id,
37 publicKey: Buffer.from(registrationInfo.credential.publicKey),
38 counter: registrationInfo.credential.counter,
39 transports: body.credential.response.transports ?? [],
40 userId,
41 },
42 });
43
44 return Response.json({ verified: true });
45 }
46
47 return Response.json({ error: 'No verificado' }, { status: 400 });
48}
counter de la credencial. WebAuthn usa un contador que se incrementa con cada uso — si recibes un valor menor al almacenado, podria indicar que la credencial fue clonada.
Flujo de autenticacion (login con passkeys)
El flujo de autenticacion es similar al de registro pero usa generateAuthenticationOptions y verifyAuthenticationResponse. Veamos la implementacion completa del lado del cliente:
1// lib/passkey-client.ts
2import {
3 startRegistration,
4 startAuthentication,
5} from '@simplewebauthn/browser';
6
7export async function registerPasskey(userId: string, email: string) {
8 // 1. Obtener opciones del servidor
9 const optionsRes = await fetch('/api/auth/register/options', {
10 method: 'POST',
11 headers: { 'Content-Type': 'application/json' },
12 body: JSON.stringify({ userId, userEmail: email }),
13 });
14 const options = await optionsRes.json();
15
16 // 2. Crear la credencial con el authenticator del dispositivo
17 const credential = await startRegistration({ optionsJSON: options });
18
19 // 3. Enviar la credencial al servidor para verificacion
20 const verifyRes = await fetch('/api/auth/register/verify', {
21 method: 'POST',
22 headers: { 'Content-Type': 'application/json' },
23 body: JSON.stringify({ userId, credential }),
24 });
25
26 return verifyRes.json();
27}
28
29export async function loginWithPasskey() {
30 // 1. Obtener opciones de autenticacion
31 const optionsRes = await fetch('/api/auth/login/options', {
32 method: 'POST',
33 });
34 const options = await optionsRes.json();
35
36 // 2. El navegador solicita la verificacion biometrica/PIN
37 const assertion = await startAuthentication({ optionsJSON: options });
38
39 // 3. Verificar en el servidor
40 const verifyRes = await fetch('/api/auth/login/verify', {
41 method: 'POST',
42 headers: { 'Content-Type': 'application/json' },
43 body: JSON.stringify(assertion),
44 });
45
46 return verifyRes.json();
47}
48
49// Verificar si el navegador soporta WebAuthn
50export function isWebAuthnSupported(): boolean {
51 return (
52 typeof window !== 'undefined' &&
53 typeof window.PublicKeyCredential !== 'undefined'
54 );
55}
56
57// Verificar si soporta Conditional UI (autofill)
58export async function supportsConditionalUI(): Promise<boolean> {
59 if (!isWebAuthnSupported()) return false;
60 return PublicKeyCredential.isConditionalMediationAvailable?.() ?? false;
61}
Conditional UI: Passkeys en el autocompletado
Una de las mejores experiencias de usuario con passkeys es la Conditional UI, que muestra las passkeys disponibles directamente en el campo de autocompletado del navegador. El usuario simplemente hace clic en su passkey, verifica con biometria, y esta dentro.
Para activar Conditional UI, necesitas agregar webauthn al atributo autocomplete del campo de usuario:
1// components/LoginForm.tsx
2'use client';
3
4import { useEffect, useState } from 'react';
5import { startAuthentication } from '@simplewebauthn/browser';
6
7export function LoginForm() {
8 const [email, setEmail] = useState('');
9 const [isConditionalAvailable, setConditionalAvailable] = useState(false);
10
11 useEffect(() => {
12 // Iniciar autenticacion condicional al cargar la pagina
13 async function initConditionalUI() {
14 const available =
15 await PublicKeyCredential.isConditionalMediationAvailable?.();
16 if (!available) return;
17 setConditionalAvailable(true);
18
19 try {
20 const optionsRes = await fetch('/api/auth/login/options', {
21 method: 'POST',
22 });
23 const options = await optionsRes.json();
24
25 // useBrowserAutofill: true activa Conditional UI
26 const assertion = await startAuthentication({
27 optionsJSON: options,
28 useBrowserAutofill: true,
29 });
30
31 const verifyRes = await fetch('/api/auth/login/verify', {
32 method: 'POST',
33 headers: { 'Content-Type': 'application/json' },
34 body: JSON.stringify(assertion),
35 });
36
37 if (verifyRes.ok) {
38 window.location.href = '/dashboard';
39 }
40 } catch (e) {
41 // El usuario cancelo o no selecciono una passkey
42 console.log('Conditional UI cancelada:', e);
43 }
44 }
45
46 initConditionalUI();
47 }, []);
48
49 return (
50 <form>
51 <input
52 type="email"
53 value={email}
54 onChange={(e) => setEmail(e.target.value)}
55 autoComplete="username webauthn"
56 placeholder="[email protected]"
57 />
58 {/* Fallback para login tradicional */}
59 <input type="password" autoComplete="current-password" />
60 <button type="submit">Iniciar sesion</button>
61 </form>
62 );
63}
UX de gestion de passkeys
Una buena experiencia de usuario requiere que los usuarios puedan gestionar sus passkeys: ver cuales tienen registradas, agregar nuevas y eliminar las que ya no necesitan.
Aspectos clave de la UX de gestion:
- Mostrar dispositivo y fecha: Indica en que dispositivo se creo cada passkey y cuando fue el ultimo uso.
- Renombrar passkeys: Permite al usuario dar nombres descriptivos como "MacBook del trabajo" o "iPhone personal".
- Alerta antes de eliminar la ultima: Si el usuario va a eliminar su unica passkey, asegurate de que tenga otro metodo de autenticacion activo.
- Onboarding progresivo: No fuerces la creacion de passkeys. Sugierelo despues del login con contrasena con un banner no intrusivo.
Estrategia de migracion de contrasenas a passkeys
No puedes eliminar las contrasenas de la noche a la manana. La migracion debe ser gradual y centrada en el usuario:
Fase 1: Passkeys como opcion (meses 1-3)
- Agregar passkeys como metodo alternativo de login
- Mostrar prompt post-login: "Activa passkeys para iniciar sesion mas rapido"
- Mantener contrasena + MFA como fallback completo
Fase 2: Passkeys como metodo preferido (meses 4-6)
- Activar Conditional UI — passkeys aparecen en el autocompletado
- Reducir la frecuencia de pedidos de MFA para usuarios con passkeys
- Tracking de adopcion: que porcentaje de usuarios tiene al menos una passkey
Fase 3: Deprecacion gradual de contrasenas (meses 7-12)
- Para cuentas nuevas: ofrecer passkeys como metodo principal, contrasena como "configurar despues"
- Usuarios con passkeys activas: permitir eliminar su contrasena voluntariamente
- Nunca forzar la eliminacion — siempre dar opcion al usuario
Comparativa de seguridad: contrasenas vs MFA vs passkeys
| Ataque | Contrasena | Contrasena + MFA | Passkeys |
|---|---|---|---|
| Phishing | Vulnerable | Parcialmente vulnerable | Inmune |
| Credential stuffing | Vulnerable | Protegido | Inmune |
| Brute force | Vulnerable | Protegido | Inmune |
| SIM swapping | N/A | Vulnerable (SMS) | Inmune |
| Man-in-the-middle | Vulnerable | Parcialmente vulnerable | Inmune (origin-bound) |
| Filtracion de BD | Exposicion de hashes | Exposicion de hashes | Solo claves publicas |
Las passkeys son inmunes al phishing porque la credencial esta vinculada al origen (dominio) — una pagina falsa con un dominio diferente simplemente no puede solicitar la credencial.
Errores comunes en la implementacion
Despues de trabajar con passkeys en produccion, estos son los errores mas frecuentes que he visto:
- No manejar la cancelacion del usuario: Cuando el usuario cancela el dialogo biometrico,
startRegistration()ystartAuthentication()lanzan una excepcion. Siempre envuelve en try/catch. - Ignorar el counter: Algunos authenticators no incrementan el counter. No rechaces la autenticacion por counter=0, pero si registra una alerta si el counter disminuye.
- RP ID demasiado restrictivo: Si usas
app.midominio.comcomo RP ID, las passkeys no funcionaran enmidominio.com. Usa el dominio raiz cuando sea posible. - No ofrecer fallback: Si el unico metodo de login es passkeys y el usuario esta en un dispositivo sin soporte, queda bloqueado.
- Olvidar la experiencia movil: En iOS y Android, las passkeys se integran con el sistema operativo. Asegurate de probar en dispositivos reales, no solo en el emulador del navegador.
1// Manejo correcto de errores en el cliente
2async function handlePasskeyLogin() {
3 try {
4 const result = await loginWithPasskey();
5 if (result.verified) {
6 router.push('/dashboard');
7 }
8 } catch (error) {
9 if (error instanceof Error) {
10 if (error.name === 'NotAllowedError') {
11 // Usuario cancelo el dialogo o timeout
12 showMessage('Autenticacion cancelada. Intenta de nuevo.');
13 } else if (error.name === 'AbortError') {
14 // Otra ceremonia WebAuthn interrumpio esta
15 showMessage('Operacion interrumpida.');
16 } else {
17 showMessage('Error inesperado. Usa tu contrasena como alternativa.');
18 showPasswordFallback();
19 }
20 }
21 }
22}
Adopcion empresarial: Google, Apple y Microsoft
Las tres grandes plataformas han adoptado passkeys de forma nativa en 2025-2026:
- Google: Passkeys como metodo predeterminado para todas las cuentas de Google. Mas de 800 millones de cuentas con passkeys activas.
- Apple: Passkeys integradas en iCloud Keychain con sincronizacion automatica entre iPhone, iPad y Mac. Soporte nativo en Safari y aplicaciones nativas.
- Microsoft: Windows Hello integrado con passkeys sincronizables. Soporte en Azure AD y Microsoft Entra para autenticacion empresarial.
El ecosistema esta maduro. Los navegadores principales soportan WebAuthn, los sistemas operativos sincronizan passkeys, y las bibliotecas de servidor (@simplewebauthn, py-webauthn, java-webauthn-server) estan probadas en produccion.
Conclusiones y proximos pasos
Las passkeys representan el mayor avance en autenticacion web en las ultimas dos decadas. Eliminan el phishing, mejoran la experiencia de usuario y reducen la carga de soporte tecnico por contrasenas olvidadas.
Para empezar a implementar passkeys en tu aplicacion:
- Evalua
@simplewebauthn/servercon un prototipo local - Implementa el registro y login de passkeys como metodo opcional
- Activa Conditional UI para que las passkeys aparezcan en el autocompletado
- Monitorea la adopcion y ajusta tu estrategia de migracion
- Mantene siempre un fallback — la transicion tomara tiempo
Comments
Sign in to leave a comment
No comments yet. Be the first!