Cristhian Villegas
Security12 min read1 views

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.

Seguridad digital y candado — concepto de proteccion de datos

Dato alarmante: Segun el informe de Verizon DBIR 2025, el 80% de las brechas relacionadas con aplicaciones web involucraron credenciales robadas o debiles. Las contrasenas son el eslabon mas debil de la cadena de seguridad.

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

ComponenteRol
Relying Party (RP)Tu aplicacion web — el servidor que solicita autenticacion
AuthenticatorEl dispositivo que genera y almacena las claves (telefono, laptop, llave USB)
ClientEl 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.
Sincronizacion de passkeys: Apple, Google y Microsoft ahora sincronizan passkeys entre dispositivos a traves de iCloud Keychain, Google Password Manager y Microsoft Account respectivamente. Esto resuelve el problema historico de perder acceso si pierdes el dispositivo.

Autenticacion biometrica con huella digital

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:

bash
1# Instalar dependencias
2npm install @simplewebauthn/server @simplewebauthn/browser
3npm install -D @simplewebauthn/types

Primero, el endpoint que genera las opciones de registro:

typescript
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:

typescript
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}
Buena practica: Siempre almacena el 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:

typescript
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:

typescript
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}
Compatibilidad: Conditional UI esta soportada en Chrome 108+, Safari 16+, Edge 108+ y Firefox 122+. En navegadores sin soporte, el formulario funciona normalmente con contrasenas como fallback.

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
Precaucion: Siempre manten un metodo de recuperacion. Si el usuario pierde todos sus dispositivos, necesita poder recuperar su cuenta via correo electronico, codigo de recuperacion o soporte al cliente.

Comparativa de seguridad: contrasenas vs MFA vs passkeys

AtaqueContrasenaContrasena + MFAPasskeys
PhishingVulnerableParcialmente vulnerableInmune
Credential stuffingVulnerableProtegidoInmune
Brute forceVulnerableProtegidoInmune
SIM swappingN/AVulnerable (SMS)Inmune
Man-in-the-middleVulnerableParcialmente vulnerableInmune (origin-bound)
Filtracion de BDExposicion de hashesExposicion de hashesSolo 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:

  1. No manejar la cancelacion del usuario: Cuando el usuario cancela el dialogo biometrico, startRegistration() y startAuthentication() lanzan una excepcion. Siempre envuelve en try/catch.
  2. 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.
  3. RP ID demasiado restrictivo: Si usas app.midominio.com como RP ID, las passkeys no funcionaran en midominio.com. Usa el dominio raiz cuando sea posible.
  4. No ofrecer fallback: Si el unico metodo de login es passkeys y el usuario esta en un dispositivo sin soporte, queda bloqueado.
  5. 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.
typescript
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:

  1. Evalua @simplewebauthn/server con un prototipo local
  2. Implementa el registro y login de passkeys como metodo opcional
  3. Activa Conditional UI para que las passkeys aparezcan en el autocompletado
  4. Monitorea la adopcion y ajusta tu estrategia de migracion
  5. Mantene siempre un fallback — la transicion tomara tiempo
Recurso recomendado: Visita passkeys.dev para una guia interactiva de implementacion, compatibilidad de navegadores y demos en vivo del flujo completo.
Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles