Volver al blog

Passkeys y WebAuthn: El Futuro de la Autenticación Llegó

Hola HaWkers, probablemente ya viste la opción "Entrar con Passkey" apareciendo en más y más sitios. Esta semana, Telegram anunció soporte a passkeys, siguiendo una tendencia que está transformando cómo nos autenticamos en la web.

¿Aún dependes de contraseñas tradicionales? Vamos a entender por qué passkeys son consideradas el futuro de la autenticación y cómo implementarlas en tus aplicaciones.

Qué Son Passkeys y WebAuthn

Passkeys y WebAuthn no son tecnologías competidoras - son partes de un sistema que trabaja junto para permitir login sin contraseña de forma segura.

WebAuthn (Web Authentication)

WebAuthn es una API estándar desarrollada por FIDO Alliance y W3C que permite logins sin contraseñas tradicionales.

Características principales:

  • Basado en criptografía de clave pública
  • Estándar del W3C desde 2019
  • Soportado por todos los navegadores modernos
  • Componente central del proyecto FIDO2

Passkeys

Passkeys son credenciales de autenticación FIDO que permiten login usando el mismo proceso de desbloqueo del dispositivo.

Cómo funcionan:

  • Usan biometría, PIN o patrón del dispositivo
  • Sincronizados entre dispositivos vía cloud
  • No requieren memorizar contraseñas
  • Resistentes a phishing por diseño

Por Qué Passkeys Son Más Seguros

Contraseñas tradicionales tienen problemas fundamentales de seguridad que passkeys resuelven por diseño.

Problemas de las Contraseñas

Vulnerabilidades comunes:

Problema Contraseñas Passkeys
Reutilización Común Imposible
Phishing Vulnerable Inmune
Fuga de base Datos expuestos Solo clave pública
Fuerza bruta Posible Imposible
Keyloggers Vulnerable Inmune

Cómo Passkeys Protegen

1. Vinculadas al dominio:
Una passkey creada para tubanco.com no puede ser usada en tubanco.fake.com, aunque el usuario sea engañado.

2. Sin secreto compartido:
El servidor almacena solo la clave pública, que es inútil sin la clave privada que nunca sale del dispositivo.

3. Únicas por servicio:
Cada passkey es un par de claves único, entonces fuga de un servicio no afecta otros.

Cómo Funciona Técnicamente

Vamos a entender el flujo de registro y autenticación con WebAuthn.

Flujo de Registro

// 1. Servidor genera challenge y opciones
const publicKeyCredentialCreationOptions = {
  challenge: new Uint8Array(32), // Challenge aleatorio del servidor
  rp: {
    name: "Mi Aplicación",
    id: "misitio.com"
  },
  user: {
    id: new Uint8Array(16), // ID único del usuario
    name: "usuario@email.com",
    displayName: "Nombre del Usuario"
  },
  pubKeyCredParams: [
    { alg: -7, type: "public-key" },   // ES256
    { alg: -257, type: "public-key" }  // RS256
  ],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    residentKey: "required",
    userVerification: "required"
  },
  timeout: 60000
};

// 2. Navegador solicita creación de credencial
const credential = await navigator.credentials.create({
  publicKey: publicKeyCredentialCreationOptions
});

// 3. Enviar clave pública para el servidor almacenar
await fetch('/api/auth/register', {
  method: 'POST',
  body: JSON.stringify({
    id: credential.id,
    rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
    response: {
      clientDataJSON: btoa(String.fromCharCode(
        ...new Uint8Array(credential.response.clientDataJSON)
      )),
      attestationObject: btoa(String.fromCharCode(
        ...new Uint8Array(credential.response.attestationObject)
      ))
    }
  })
});

Flujo de Autenticación

// 1. Servidor envía challenge
const publicKeyCredentialRequestOptions = {
  challenge: new Uint8Array(32), // Challenge nuevo del servidor
  rpId: "misitio.com",
  allowCredentials: [], // Vacío para passkeys descubribles
  userVerification: "required",
  timeout: 60000
};

// 2. Navegador solicita autenticación
const assertion = await navigator.credentials.get({
  publicKey: publicKeyCredentialRequestOptions
});

// 3. Enviar firma para verificación en el servidor
const response = await fetch('/api/auth/login', {
  method: 'POST',
  body: JSON.stringify({
    id: assertion.id,
    rawId: btoa(String.fromCharCode(...new Uint8Array(assertion.rawId))),
    response: {
      clientDataJSON: btoa(String.fromCharCode(
        ...new Uint8Array(assertion.response.clientDataJSON)
      )),
      authenticatorData: btoa(String.fromCharCode(
        ...new Uint8Array(assertion.response.authenticatorData)
      )),
      signature: btoa(String.fromCharCode(
        ...new Uint8Array(assertion.response.signature)
      ))
    }
  })
});

Implementación Práctica

Para facilitar la implementación, existen bibliotecas que abstraen la complejidad.

SimpleWebAuthn (Recomendado)

// Frontend - @simplewebauthn/browser
import {
  startRegistration,
  startAuthentication
} from '@simplewebauthn/browser';

// Registro
const registrationOptions = await fetch('/api/webauthn/register-options')
  .then(res => res.json());

const registration = await startRegistration(registrationOptions);

await fetch('/api/webauthn/register', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(registration)
});

// Login
const authOptions = await fetch('/api/webauthn/auth-options')
  .then(res => res.json());

const authentication = await startAuthentication(authOptions);

await fetch('/api/webauthn/authenticate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(authentication)
});

Backend con Node.js

// Backend - @simplewebauthn/server
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';

// Generar opciones de registro
app.get('/api/webauthn/register-options', async (req, res) => {
  const options = await generateRegistrationOptions({
    rpName: 'Mi Aplicación',
    rpID: 'misitio.com',
    userID: user.id,
    userName: user.email,
    attestationType: 'none',
    authenticatorSelection: {
      residentKey: 'required',
      userVerification: 'required'
    }
  });

  // Guardar challenge en la sesión
  req.session.challenge = options.challenge;

  res.json(options);
});

// Verificar registro
app.post('/api/webauthn/register', async (req, res) => {
  const verification = await verifyRegistrationResponse({
    response: req.body,
    expectedChallenge: req.session.challenge,
    expectedOrigin: 'https://misitio.com',
    expectedRPID: 'misitio.com'
  });

  if (verification.verified) {
    // Guardar credencial en la base de datos
    await saveCredential(user.id, verification.registrationInfo);
    res.json({ success: true });
  }
});

Actualizaciones de 2025

El ecosistema de passkeys continúa evolucionando con mejoras significativas.

WebAuthn Level 3

La especificación WebAuthn Level 3 fue publicada como Working Draft en enero de 2025, trayendo:

  • APIs mejoradas para creación y uso de passkeys
  • Nuevos recursos de seguridad
  • Mejor soporte cross-device

Directrices NIST

El NIST finalizará las Digital Identity Guidelines (SP 800-63-4) en julio de 2025:

  • Passkeys reconocidas como "syncable authenticators"
  • Pueden alcanzar Authenticator Assurance Level 2 (AAL2)
  • MFA resistente a phishing exigido para agencias federales de EE.UU.

Portabilidad de Passkeys

La FIDO Alliance está desarrollando nuevos protocolos:

  • CXP (Credential Exchange Protocol) - Transferencia segura entre proveedores
  • CXF (Credential Exchange Format) - Formato estándar de intercambio

Plataformas con Soporte

Passkeys ya son soportadas por las principales plataformas.

Sincronización por Proveedor

Proveedor Sincronización Plataformas
Apple iCloud Keychain iOS, macOS
Google Google Password Manager Android, Chrome
Microsoft Windows Hello Windows 10/11
1Password Aplicación Todas
Dashlane Aplicación Todas

Adopción por Servicios

Servicios populares con soporte a passkeys:

  • Google
  • Microsoft
  • Apple
  • Amazon
  • GitHub
  • PayPal
  • eBay
  • Nintendo
  • Telegram (noviembre 2025)

Consideraciones de Implementación

Al agregar passkeys a tu aplicación, considera estos puntos.

Fallback Necesario

No todos los dispositivos soportan passkeys. Mantén opciones alternativas:

// Verificar soporte del navegador
const supportsWebAuthn = () => {
  return window.PublicKeyCredential !== undefined &&
    typeof window.PublicKeyCredential === 'function';
};

// Verificar soporte a passkeys condicionales
const supportsConditionalUI = async () => {
  if (!supportsWebAuthn()) return false;

  return await PublicKeyCredential
    .isConditionalMediationAvailable?.() ?? false;
};

Consideraciones de UX

  • Ofrecer passkeys como opción, no obligatorio
  • Explicar beneficios para usuarios
  • Permitir registro de múltiples passkeys
  • Mantener método de recuperación

Conclusión

Passkeys representan la mayor evolución en autenticación web desde la popularización de contraseñas. Con soporte amplio de navegadores y sistemas operativos, resistencia intrínseca a phishing y experiencia de usuario superior, es el momento correcto para comenzar a implementar.

Para desarrolladores, bibliotecas como SimpleWebAuthn hacen la implementación accesible. Para usuarios, la experiencia es más simple y segura que contraseñas tradicionales.

Si quieres profundizar tus conocimientos en seguridad web, recomiendo que revises otro artículo: GitHub Actions Cambia Precios en 2026 donde vas a descubrir cómo proteger tus pipelines de CI/CD.

¡Vamos a por ello! 🦅

💻 Domina JavaScript de Verdad

El conocimiento que adquiriste en este artículo es solo el comienzo. Hay técnicas, patrones y prácticas que transforman desarrolladores principiantes en profesionales requeridos.

Invierte en Tu Futuro

Preparé un material completo para que domines JavaScript:

Formas de pago:

  • 1x de R$9,90 sin intereses
  • o R$9,90 al contado

📖 Ver Contenido Completo

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios