Una web te propone entrar con una huella, el PIN del equipo o una llave USB. No escribes una contraseña del sitio, pero el servidor sigue necesitando una prueba de que puedes acceder a tu cuenta. Esa prueba es una respuesta criptográfica verificable.
En esta guía vamos a seguir el registro y el inicio de sesión con una passkey, entender las piezas de WebAuthn y construir una aplicación local en Node.js. El laboratorio incluye servidor, interfaz y verificaciones: el navegador no decide por sí solo si alguien está autenticado.
Passkeys, WebAuthn y FIDO2: qué es cada cosa
Una passkey es una credencial de autenticación basada en los estándares FIDO, asociada a una cuenta de un servicio. Puede estar disponible mediante un gestor de credenciales, el dispositivo o una llave de seguridad. No es una contraseña que el servidor reciba al iniciar sesión. FIDO Alliance: passkeys.
Las otras siglas describen cómo se comunican las piezas:
| Término | Responsabilidad |
|---|---|
| WebAuthn, Web Authentication | API del navegador para crear credenciales y obtener pruebas de autenticación |
| CTAP, Client to Authenticator Protocol | Comunicación entre la plataforma y un autenticador externo, por ejemplo una llave USB |
| FIDO2 | Conjunto formado por WebAuthn y CTAP |
| Relying party, o RP | Servicio que registra la credencial y verifica las respuestas; aquí será nuestro servidor |
| Autenticador | Componente que administra la credencial y realiza las operaciones criptográficas |
WebAuthn expone navigator.credentials.create() para registrar y navigator.credentials.get() para autenticar. Una biblioteca puede envolver estas llamadas, pero no elimina la verificación del servidor. FIDO Alliance: especificaciones, Yubico: WebAuthn, FIDO2 y CTAP.
Dos claves, dos responsabilidades
La criptografía de clave pública utiliza un par de claves relacionadas matemáticamente:
- La clave privada permite producir la firma y debe quedar protegida por el autenticador o proveedor de passkeys.
- La clave pública permite verificar esa firma; el servidor la guarda junto con el identificador de la credencial y su cuenta.
La clave pública no permite generar las firmas de la privada. Y la web no recibe la huella, el rostro ni el PIN que usas para desbloquear el autenticador. FIDO Alliance: especificaciones y privacidad.
Durante la autenticación, WebAuthn firma una estructura que incluye los datos del autenticador y el hash de clientDataJSON, donde viajan el desafío, el origen y el tipo de operación. No consiste en cifrar una contraseña ni en descifrar un desafío con la clave privada. W3C WebAuthn: firma de una assertion.
Una filtración de claves públicas no equivale a filtrar contraseñas reutilizables. Aun así, hay que proteger la base de datos: quien pueda sustituir la clave pública asociada a una cuenta puede comprometer su autenticación. Yubico: repositorio de credenciales.
El desafío demuestra que la respuesta pertenece a este intento
El challenge, o desafío, es un valor aleatorio que el servidor genera para una operación. No es una contraseña ni necesita ser secreto: debe ser impredecible y comprobarse contra el valor que el servidor esperaba.
WebAuthn exige desafíos aleatorios y recomienda al menos 16 bytes. Nuestro laboratorio deja que SimpleWebAuthn los genere, los vincula a la sesión, les da cinco minutos de validez y los consume una sola vez. El plazo es una decisión del ejemplo. W3C WebAuthn: desafíos criptográficos.
Reenviar una respuesta antigua no debe funcionar, aunque su firma sea válida. Tampoco sirve aceptar un desafío nuevo que el propio cliente haya elegido en lugar del guardado por el servidor.
RP ID y origin: el dominio también importa
El RP ID delimita el servicio para el que se crea y usa una credencial. Es un dominio, sin protocolo, puerto ni ruta. El origin identifica el contexto web mediante protocolo, host y puerto cuando corresponde. MDN: Web Authentication API.
| Contexto del ejemplo | RP ID | Origin esperado |
|---|---|---|
| Laboratorio local | localhost | http://localhost:3000 |
| Aplicación de ejemplo | login.example.com | https://login.example.com |
En el caso web habitual, el RP ID coincide con el dominio efectivo del origen o con un sufijo de dominio permitido; no puede ser un sufijo público como com. El servidor valida tanto el origen esperado como el hash del RP ID de la respuesta. W3C WebAuthn: RP ID, verificación de autenticación.
Esta vinculación dificulta que una página impostora utilice una credencial del sitio legítimo. No autoriza a aceptar cualquier origen que contenga el nombre de tu empresa. En el código usaremos valores fijos, no valores derivados de cabeceras enviadas por quien hace la petición.
Presencia del usuario no significa verificación del usuario
User presence (UP) indica una interacción o presencia del usuario, como tocar una llave. User verification (UV) añade una comprobación local mediante PIN, biometría u otro mecanismo admitido por el autenticador. Un toque por sí solo no demuestra UV.
El servidor recibe indicadores del resultado, no tu PIN ni tu huella. UV comprueba el acceso local al autenticador; no certifica tu identidad civil ni convierte automáticamente un correo en verificado. Yubico: user verification y user presence.
Para este laboratorio elegimos userVerification: 'required' en las opciones y requireUserVerification: true al verificar las respuestas. No basta con configurar el frontend: un cliente puede modificar sus opciones. Esta exigencia es nuestra política del ejemplo, no una obligación universal para toda integración WebAuthn. SimpleWebAuthn: opciones y verificación.
Qué ocurre al registrar una passkey
El registro vincula una credencial a una cuenta. Si la cuenta ya existe, añadir una nueva clave requiere autorización sobre esa cuenta: conocer su nombre o correo no es suficiente.
No se pudo mostrar el diagrama. Puedes consultar su definición.
sequenceDiagram
participant B as Navegador
participant S as Servidor RP
participant A as Autenticador
B->>S: Solicitar registro autorizado
S->>S: Guardar challenge y cuenta
S-->>B: Opciones de creación
B->>A: Crear credencial para el RP
A->>A: Verificar al usuario y crear claves
A-->>B: Datos de la nueva credencial
B->>S: Respuesta de registro
S->>S: Validar y consumir challenge
S->>S: Asociar clave pública a la cuenta
S-->>B: Registro completado
El servidor conserva el ID de credencial, la clave pública, el identificador de cuenta y los metadatos necesarios para verificar futuros accesos. La clave privada no se entrega al servidor del sitio. Yubico: registro de passkeys.
Usaremos attestationType: 'none'. La attestation puede aportar evidencia sobre el autenticador; no es lo mismo que la autenticación posterior. Con esta política no pretendemos certificar fabricante, modelo ni protección física de la clave. No deduzcas que una credencial procede de una YubiKey solo porque el cliente lo afirma. Yubico: attestation.
Qué ocurre al iniciar sesión
En el acceso se usa la credencial ya registrada. El servidor emite otro desafío; no reutiliza el del registro.
No se pudo mostrar el diagrama. Puedes consultar su definición.
sequenceDiagram
participant B as Navegador
participant S as Servidor RP
participant A as Autenticador
B->>S: Solicitar inicio de sesión
S-->>B: Challenge nuevo y RP ID
B->>A: Seleccionar credencial del RP
A->>A: Verificar al usuario y firmar
A-->>B: Assertion de autenticación
B->>S: ID, userHandle y respuesta
S->>S: Buscar credencial y comprobar cuenta
S->>S: Verificar firma, challenge, origin, RP ID y UV
S-->>B: Nueva sesión si todo es válido
La assertion es la respuesta de autenticación. Su ID ayuda a localizar la clave pública guardada. El userHandle identifica la cuenta mediante un valor opaco; hay que comprobar que corresponde a esa credencial. No es un permiso ni una prueba de identidad por sí solo. Yubico: solicitudes de autenticación.
El protocolo también contempla un contador de firmas. Su comportamiento depende del autenticador; algunos mantienen cero. Guardaremos el contador devuelto por la biblioteca, sin inventar la regla «si es cero, la passkey es falsa». W3C WebAuthn: contadores de firma.
Passkeys sincronizadas y vinculadas a un dispositivo
| Tipo | Dónde puede usarse | Qué debes considerar |
|---|---|---|
| Sincronizada | Dispositivos donde el proveedor la haya sincronizado | Protección de la cuenta del proveedor y su mecanismo de recuperación |
| Vinculada a un dispositivo | Mediante el autenticador que conserva esa credencial | Pérdida o daño del autenticador y acceso alternativo |
Una llave física puede conectarse a varios ordenadores sin sincronizar la clave entre ellos: se transporta el autenticador. Por eso «vinculada a un dispositivo» no significa necesariamente «solo usable desde un ordenador». Yubico: credenciales de uno o varios dispositivos.
Tampoco conviene afirmar que toda clave privada «nunca sale del dispositivo»: la sincronización permite que una credencial esté disponible en varios. Lo que no se entrega al servidor del sitio es la clave privada.
Nuestro registro solicita una credencial descubrible con residentKey: 'required'. Así el autenticador puede ofrecerla sin que el servidor envíe antes una lista de credenciales del usuario. «Descubrible» y «sincronizada» describen propiedades distintas: una llave física también puede guardar passkeys descubribles. SimpleWebAuthn: passkeys.
Dónde entra una YubiKey
Una YubiKey compatible con FIDO2 puede actuar como autenticador externo: la plataforma se comunica con ella y la llave realiza la operación con su clave. Según modelo y configuración, la interacción puede incluir conexión USB o NFC, PIN y toque; algunos modelos admiten biometría. No todos los modelos tienen las mismas capacidades. Yubico: WebAuthn, FIDO2 y CTAP, verificación local.
En el ejemplo no forzaremos authenticatorAttachment: 'platform': queremos permitir tanto autenticadores integrados como llaves externas. El navegador mostrará las opciones compatibles disponibles.
Laboratorio: registro y acceso con Node.js
Necesitas Node.js 22 o superior, pnpm y un navegador con WebAuthn. Para usar una llave física, debe admitir FIDO2, credenciales descubribles y UV; sigue su configuración de PIN si el navegador la solicita. El soporte de autenticadores depende de la combinación de navegador, sistema y dispositivo.
La aplicación funciona únicamente en http://localhost:3000. WebAuthn requiere un contexto seguro; localhost permite desarrollo local sin montar HTTPS. No cambies indistintamente entre localhost y 127.0.0.1: en este laboratorio fijamos el host, el origen y el RP ID. MDN: Web Authentication API, SimpleWebAuthn: configuración del servidor.
Cada registro crea una cuenta ficticia nueva con una sola credencial. Guardamos credenciales y sesiones en memoria: reiniciar Node elimina los registros del servidor. Esto es un laboratorio de un solo proceso, no un servicio de cuentas listo para exponer a Internet.
1. Preparar los archivos
Crea una carpeta fuera del proyecto Astro para ejecutar el laboratorio por separado:
mkdir laboratorio-passkeys
cd laboratorio-passkeys
mkdir publicGuarda este package.json. Las versiones se fijan para reproducir el ejemplo:
{
"name": "laboratorio-passkeys",
"private": true,
"type": "module",
"scripts": { "start": "node server.mjs" },
"dependencies": {
"@simplewebauthn/server": "14.0.3",
"@simplewebauthn/browser": "14.0.0",
"express": "5.3.0"
}
}Instala las dependencias con pnpm install. Conserva el pnpm-lock.yaml generado si versionas tu laboratorio; después podrás instalarlo con pnpm install --frozen-lockfile.
2. El servidor: verificar antes de abrir la sesión
Guarda lo siguiente como server.mjs. Usamos las cuatro operaciones de SimpleWebAuthn Server: generar opciones y verificar respuestas, tanto para registro como para autenticación.
Los desafíos se guardan en el servidor y se consumen antes de verificar. Los fallos no revelan detalles criptográficos al cliente. Después de autenticar rotamos la cookie de sesión y consultamos el usuario desde el servidor.
import express from 'express';
import { randomBytes } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
const app = express();
const ORIGIN = 'http://localhost:3000';
const RP_ID = 'localhost';
const SESSION_TTL = 10 * 60_000;
const CHALLENGE_TTL = 5 * 60_000;
const sessions = new Map();
const credentials = new Map();
const localPath = (path) => fileURLToPath(new URL(path, import.meta.url));
app.disable('x-powered-by');
app.use((req, res, next) => {
if (req.get('host') !== 'localhost:3000') {
return res.status(403).json({ error: 'Usa http://localhost:3000.' });
}
res.set({
'Cache-Control': 'no-store',
'X-Content-Type-Options': 'nosniff',
'Content-Security-Policy': "default-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'",
});
next();
});
app.get('/vendor/webauthn.js', (_req, res) => {
res.sendFile(localPath('./node_modules/@simplewebauthn/browser/dist/bundle/index.umd.min.js'));
});
app.use(express.static(localPath('./public')));
app.use('/api', (req, res, next) => {
if (req.method === 'POST' &&
(req.get('origin') !== ORIGIN || !req.is('application/json'))) {
return res.status(403).json({ error: 'Petición no permitida.' });
}
const id = req.headers.cookie?.match(/(?:^|;\s*)passkey_lab=([\w-]+)(?:;|$)/)?.[1];
const session = sessions.get(id);
if (session && session.expiresAt > Date.now()) {
req.sessionId = id;
req.session = session;
} else {
if (id) sessions.delete(id);
replaceSession(req, res);
}
next();
});
app.use(express.json({ limit: '16kb' }));
function replaceSession(req, res, userId = null) {
sessions.delete(req.sessionId);
const id = randomBytes(32).toString('base64url');
const session = { userId, pending: null, expiresAt: Date.now() + SESSION_TTL };
sessions.set(id, session);
req.sessionId = id;
req.session = session;
// Solo HTTP en loopback. En producción: HTTPS y secure: true.
res.cookie('passkey_lab', id, {
httpOnly: true,
sameSite: 'strict',
secure: false,
path: '/',
maxAge: SESSION_TTL,
});
}
function saveChallenge(req, kind, challenge, userId = null) {
req.session.pending = {
kind,
challenge,
userId,
expiresAt: Date.now() + CHALLENGE_TTL,
};
}
function consumeChallenge(req, kind) {
const pending = req.session.pending;
req.session.pending = null;
if (!pending || pending.kind !== kind || pending.expiresAt <= Date.now()) {
throw new Error('Desafío ausente, caducado o ya consumido.');
}
return pending;
}
app.post('/api/register/options', async (req, res) => {
// Este endpoint crea cuentas nuevas; no añade claves a cuentas existentes.
if (req.session.userId || req.session.registered) {
return res.status(409).json({ error: 'Cierra la sesión de laboratorio antes de crear otra cuenta.' });
}
const userID = randomBytes(32);
const userId = userID.toString('base64url');
const options = await generateRegistrationOptions({
rpName: 'Laboratorio uncodigo',
rpID: RP_ID,
userID,
userName: `demo-${userId.slice(0, 8)}`,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'required',
userVerification: 'required',
},
});
saveChallenge(req, 'registration', options.challenge, userId);
res.json(options);
});
app.post('/api/register/verify', async (req, res) => {
try {
const pending = consumeChallenge(req, 'registration');
const result = await verifyRegistrationResponse({
response: req.body,
expectedChallenge: pending.challenge,
expectedOrigin: ORIGIN,
expectedRPID: RP_ID,
requireUserVerification: true,
});
if (!result.verified) throw new Error('Registro rechazado.');
const { credential, credentialDeviceType, credentialBackedUp } = result.registrationInfo;
if (credentials.has(credential.id)) throw new Error('Credencial duplicada.');
credentials.set(credential.id, {
userId: pending.userId,
credential,
credentialDeviceType,
credentialBackedUp,
});
req.session.registered = true;
res.json({ message: 'Cuenta creada. Ahora inicia sesión con su passkey.' });
} catch {
res.status(400).json({ error: 'No se pudo registrar la passkey. Inicia un intento nuevo.' });
}
});
app.post('/api/login/options', async (req, res) => {
const options = await generateAuthenticationOptions({
rpID: RP_ID,
allowCredentials: [],
userVerification: 'required',
});
saveChallenge(req, 'authentication', options.challenge);
res.json(options);
});
app.post('/api/login/verify', async (req, res) => {
try {
const pending = consumeChallenge(req, 'authentication');
const record = credentials.get(req.body?.id);
// El userHandle debe pertenecer a la cuenta asociada a esta clave pública.
if (!record || req.body?.response?.userHandle !== record.userId) {
throw new Error('Credencial no válida.');
}
const result = await verifyAuthenticationResponse({
response: req.body,
expectedChallenge: pending.challenge,
expectedOrigin: ORIGIN,
expectedRPID: RP_ID,
credential: record.credential,
requireUserVerification: true,
});
if (!result.verified) throw new Error('Autenticación rechazada.');
const info = result.authenticationInfo;
record.credential.counter = info.newCounter;
record.credentialDeviceType = info.credentialDeviceType;
record.credentialBackedUp = info.credentialBackedUp;
replaceSession(req, res, record.userId);
res.json({ message: 'Sesión iniciada tras verificar la respuesta.' });
} catch {
res.status(400).json({ error: 'No se pudo autenticar. Inicia un intento nuevo.' });
}
});
app.get('/api/me', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'No hay una sesión autenticada.' });
}
res.json({ userId: req.session.userId });
});
app.post('/api/logout', (req, res) => {
replaceSession(req, res);
res.json({ message: 'Sesión cerrada.' });
});
app.use((error, _req, res, _next) => {
const status = error.status === 413 ? 413 : 400;
res.status(status).json({ error: 'No se pudo procesar la petición.' });
});
setInterval(() => {
for (const [id, session] of sessions) {
if (session.expiresAt <= Date.now()) sessions.delete(id);
}
}, 60_000).unref();
app.listen(3000, '127.0.0.1', () => {
console.log(`Laboratorio local: ${ORIGIN}`);
});Aquí userID son bytes aleatorios. Su representación Base64URL se utiliza como identificador de cuenta y se compara con el userHandle recibido. No construimos ese identificador a partir de un correo ni dejamos que el navegador elija a qué cuenta se adjunta la clave.
La cookie usa HttpOnly y SameSite=Strict. secure: false se limita al HTTP del laboratorio en loopback; un servicio publicado debe usar HTTPS y cookies Secure. No uses esta excepción como configuración de producción.
3. Una interfaz web pequeña
Guarda este archivo como public/index.html:
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laboratorio de passkeys</title>
<script src="/vendor/webauthn.js" defer></script>
<script src="/app.js" defer></script>
</head>
<body>
<h1>Passkeys en localhost</h1>
<p>Cada registro crea una cuenta ficticia nueva. Los datos viven en memoria.</p>
<button id="register">Crear cuenta con passkey</button>
<button id="login">Iniciar sesión</button>
<button id="me">Ver sesión</button>
<button id="logout">Cerrar sesión</button>
<p id="status" role="status" aria-live="polite">Listo.</p>
</body>
</html>Y este como public/app.js:
const { startRegistration, startAuthentication, browserSupportsWebAuthn } = SimpleWebAuthnBrowser;
const statusMessage = document.querySelector('#status');
const buttons = [...document.querySelectorAll('button')];
async function api(path, body) {
const response = await fetch(path, {
method: body === undefined ? 'GET' : 'POST',
credentials: 'same-origin',
headers: body === undefined ? {} : { 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'La operación falló.');
return data;
}
async function run(operation) {
buttons.forEach((button) => { button.disabled = true; });
statusMessage.textContent = 'Sigue las indicaciones del navegador…';
try {
const result = await operation();
statusMessage.textContent = result.message || `Cuenta autenticada: ${result.userId}`;
} catch (error) {
statusMessage.textContent = error.name === 'NotAllowedError'
? 'Operación cancelada o no disponible. Puedes intentarlo de nuevo.'
: error.message;
} finally {
buttons.forEach((button) => { button.disabled = false; });
}
}
document.querySelector('#register').addEventListener('click', () => run(async () => {
const optionsJSON = await api('/api/register/options', {});
const response = await startRegistration({ optionsJSON });
return api('/api/register/verify', response);
}));
document.querySelector('#login').addEventListener('click', () => run(async () => {
const optionsJSON = await api('/api/login/options', {});
const response = await startAuthentication({ optionsJSON });
return api('/api/login/verify', response);
}));
document.querySelector('#me').addEventListener('click', () => run(() => api('/api/me')));
document.querySelector('#logout').addEventListener('click', () => run(() => api('/api/logout', {})));
if (!browserSupportsWebAuthn()) {
buttons.forEach((button) => { button.disabled = true; });
statusMessage.textContent = 'Este navegador no ofrece WebAuthn en este contexto.';
}El bundle de @simplewebauthn/browser se sirve desde la dependencia local instalada. startRegistration({ optionsJSON }) y startAuthentication({ optionsJSON }) se encargan de las conversiones entre JSON y los datos binarios de WebAuthn. Mostrar «Sesión iniciada» solo ocurre después de la respuesta satisfactoria del servidor. SimpleWebAuthn Browser.
4. Ejecutar y comprobar el recorrido
Arranca el servidor:
pnpm startAbre http://localhost:3000 y sigue estos pasos:
- Pulsa Ver sesión: debe indicar que todavía no estás autenticado.
- Pulsa Crear cuenta con passkey y completa el diálogo del navegador. Si usas una llave, elige esa opción cuando esté disponible.
- Pulsa Iniciar sesión y selecciona la credencial recién creada.
- Pulsa Ver sesión: debe aparecer el identificador de la cuenta ficticia.
- Pulsa Cerrar sesión: la consulta de sesión debe volver a fallar.
- Vuelve a iniciar sesión con la misma passkey: debe recuperarse la misma cuenta.
Estos son los resultados esperados de la prueba manual. Para esta guía se ejecutó también el recorrido con Chromium y un autenticador virtual CTAP2: pasaron registro, autenticación, rotación de sesión y cierre. Se comprobó el rechazo de respuestas reutilizadas, firmas y datos alterados, ausencia de UV y respuestas pertenecientes a otra sesión. Eso no sustituye una prueba con una YubiKey física ni verifica la sincronización de un proveedor real.
5. Limpiar el laboratorio
Detén Node con Ctrl+C y elimina la carpeta del laboratorio cuando ya no la necesites. Borra también la passkey de prueba desde el gestor de credenciales o la herramienta del autenticador: reiniciar el servidor no elimina las credenciales guardadas en esos dispositivos.
Qué falta antes de llevarlo a una aplicación real
El diseño deliberadamente pequeño deja visibles las responsabilidades que tendrás que resolver:
- Persistencia y concurrencia. Guarda cuentas y credenciales en una base de datos. El consumo del desafío debe ser atómico si hay varios procesos; actualizar contadores también necesita una estrategia coherente.
- Alta de nuevas claves. Para una cuenta existente, exige una sesión autenticada y una comprobación reciente adecuada al riesgo. No conviertas el registro público del laboratorio en un endpoint que adjunte claves al correo recibido.
- Sesiones y abuso. Configura HTTPS, cookies seguras, vencimientos, límites de intentos y registro de eventos sin guardar respuestas completas ni secretos.
- Recuperación. Permite gestionar y revocar credenciales. Define cómo recuperar una cuenta sin convertir un mecanismo débil en una puerta trasera del acceso con passkeys.
- Compatibilidad. Prueba dispositivos reales, cancelación del diálogo, pérdida del autenticador y navegadores soportados antes de prometer una experiencia uniforme.
Estas son recomendaciones de implementación; no una certificación ni una lista de obligaciones legales. La gestión de credenciales y su revocación forman parte del ciclo de vida que debe diseñar la aplicación. Yubico: gestión de credenciales.
Las passkeys tampoco reemplazan la autorización: una sesión válida no da permiso para acceder a datos de otra cuenta. Y la resistencia al phishing no elimina el riesgo de XSS, robo de sesiones o recuperación insegura. Si delegas el inicio de sesión a un proveedor, puedes combinar passkeys en ese proveedor con OpenID Connect en tu aplicación; cumplen funciones distintas.
La confianza está en lo que verificas
La experiencia visible puede ser un toque o una huella. La decisión de acceso requiere vincular una respuesta válida a un desafío vigente, un servicio concreto y una cuenta registrada. Una integración útil mantiene esa cadena de comprobaciones y contempla qué ocurrirá cuando el usuario cambie o pierda su autenticador.
