Módulo 4 · Autenticación y seguridad

Lección 20 — Hash de contraseñas y MFA

bcrypt/argon2, MFA y recuperación de cuenta sin abrir la puerta al atacante.

Publicada
En esta lección
  1. Objetivos
  2. 1. Por qué no "hash" a secas
  3. 2. La contraseña en el flujo (más allá del hash)
  4. 3. TOTP: el segundo factor que sí funciona
  5. 4. Recuperación de cuenta: la puerta que no puede ser de atrás
  6. 5. MFA y el futuro del módulo
  7. 5. La biometría y el hardware: MFA más allá del TOTP
  8. 6. Recuperación de cuenta: la puerta que abre y cierra

Stack: Django (argon2) + pyotp · Proyecto: TicketFlow Estado: Publicada Prerrequisito: Lección 19 — OAuth2


Objetivos

  1. Almacenar contraseñas con KDFs modernos (argon2/bcrypt) entendiendo salt, iteraciones y coste de memoria.
  2. Añadir TOTP (Google Authenticator) con QR, verificación y códigos de recuperación de un solo uso.
  3. Diseñar la recuperación de cuenta sin convertirla en la puerta de atrás del sistema.

1. Por qué no "hash" a secas

SHA-256 de una contraseña es inútil: GPU hace miles de millones/segundo. Los KDFs están diseñados para ser lentos a propósito:

KDFCosteNota
argon2idCPU + MEMORIA configurableel ganador del concurso; la memoria duele en GPU
bcryptCPU (cost 10-12)veterano, 72 bytes de límite
PBKDF2CPU puroel más débil de los tres, pero omnipresente

Django: PASSWORD_HASHERS con Argon2 primero (pip install django[argon2]). El hash guarda algoritmo+parámetros+salt+digest (argon2id$v=19$m=65536,t=3,p=4$...): actualizar parámetros por usuario en el próximo login (upgrade transparente).

El salt (aleatorio por usuario, incluido en el hash) mata las rainbow tables: dos usuarios con "password123" tienen hashes distintos. El coste lento mata el brute force offline: si tu BD filtra, "password123" sigue costando horas por intento-masa.

La verificación con check_password es constante-time: no filtra por timing cuánto coincidió.

2. La contraseña en el flujo (más allá del hash)

  • Rate limit del login por usuario+IP (23): el hash lento también protege el endpoint, pero 10 intentos/s contra "p@ssw0rd" del usuario real = diccionario online. Límite + backoff + captcha progresivo.
  • Mensajes de error idénticos: "Credenciales inválidas" tanto si falla el email como la contraseña (no enumerar usuarios).
  • Nunca loguear contraseñas (ni en exception handlers): sanitizador de logs (45).
  • Longitud > complejidad: permite 64+ chars, no fuerces cambios ciegas mensuales (NIST: sin rotación forzada; sí: lista de contraseñas filtradas — django-pwned-passwords contra el API de Have I Been Pwned con k-anonymity).

3. TOTP: el segundo factor que sí funciona

  1. Enrolamiento: generas secreto compartido por usuario (pyotp.random_base32()), lo muestras como QR (otpauth://totp/TicketFlow:ana?secret=...&issuer=TicketFlow), la app del usuario lo escanea.
  2. Verificación: pyotp.TOTP(secret).verify(code, valid_window=1) — ventana de ±30s para relojes desincronizados.
  3. Guardado del secreto: cifrado en reposo (field encriptado con clave de settings/KMS — 23). Si tu BD filtra y el secreto está plano, el 2FA es decorativo.
  4. Códigos de recuperación: 8-10 códigos de un solo uso, hash como contraseñas, mostrados UNA vez al activar. Sin ellos, un móvil perdido = cuenta perdida (soporte = puerta de atrás si es flojo).
  5. Recordar dispositivo (opcional): cookie firmada 30 días con marca "MFA completado" — equilibro UX/riesgo consciente.

4. Recuperación de cuenta: la puerta que no puede ser de atrás

El flujo de "olvidé mi contraseña" es el punto más atacado (bypasea hash y MFA si lo haces mal):

  • Token: aleatorio (secrets.token_urlsafe), hash en BD (como contraseña), TTL 1h, un solo uso, vinculado al usuario.
  • Entrega: email al usuario registrado (NUNCA "¿es este tu email?" en la respuesta — enumera usuarios). Respuesta siempre la misma: "Si existe una cuenta, recibirás un email".
  • Al restablecer: invalidar todas las sesiones/tokens (logout global), notificar por email, y si tenía MFA... NO permitir reset sin MFA (o exigir códigos de recuperación: el atacante con el email solo no pasa el TOTP).
  • Enlaces: dominio propio, sin token en logs/referers (el email cliente puede pre-cargar links: el token de un solo uso + hash en BD mitiga).

5. MFA y el futuro del módulo

TOTP cubre el 95%. SMS es débil (SIM swapping) pero mejor que nada para usuarios sin app. Passkeys/WebAuthn (llaves del SO) es el destino en 2026 — se cita como evolución natural del ADR, no se implementa hoy para no abrir otro frente.

5. La biometría y el hardware: MFA más allá del TOTP

El TOTP de la 20 cubre el estándar; el mapa completo del segundo factor, de más débil a más fuerte:

FactorQué esResistencia al phishing
SMScódigo por mensajeBAJA: SIM-swapping y reenvío de SMS; desaconsejado por NIST
Emailcódigo/clic por correoBAJA: el email es el factor más robado del mundo
TOTP (app)código de 6 dígitos rotando cada 30 sMEDIA: el phishing en tiempo real (proxy entre usuario y sitio) lo captura y lo usa al instante
Push con númeronotificación con verificación cruzadaMEDIA-ALTA: exige atención del usuario (el "MFA fatigue" es el ataque)
WebAuthn / passkeyclave criptográfica ligada al origen (TPM/llave)ALTA: el phishing NO la roba — solo responde al dominio legítimo

La recomendación de TicketFlow: TOTP como factor universal (lo que la lección implementó), passkeys/WebAuthn como la vía premium (el navegador hace todo; el backend guarda la clave pública y verifica el desafío con django-webauthn o py_webauthn), y SMS solo como último recurso de recuperación — documentado como riesgo aceptado. La regla del phishing: el factor que NO sabe en qué sitio está (SMS, TOTP) es engañable; el que sabe (WebAuthn) no.

6. Recuperación de cuenta: la puerta que abre y cierra

El flujo de reset ya implementado (MFA exigida, códigos hasheados, no enumeración) es la mitad; la otra mitad es el RESTO de las puertas laterales que un atacante usa: (1) el soporte humano (el "llamo y me hacen reset por teléfono": protocolo del soporte con verificación equivalente al reset automático — la ingeniería social es el vector #1 real); (2) el email de la cuenta (recuperar el email ajeno = recuperar todo: documenta la cadena de dependencia); (3) los códigos de recuperación (los 10 de un solo uso: hasheados, ya implementados — el usuario debe guardarlos OFFLINE, no en el mismo gestor de contraseñas comprometible); (4) la rotación de SESIÓN tras el reset (invalidar todas las sesiones activas del usuario — la 18: el atacante que ya estaba dentro no hereda la recuperación).

La prueba de la puerta: el pentest de cuenta ajena lista TODAS las vías de volver a entrar y verifica que cada una exige la misma fuerza. El reset seguro con una puerta lateral débil es un candado en la puerta principal y la ventana abierta.


  1. ¿Qué tres parámetros de argon2 ajustarías en un servidor de 2GB y por qué la memoria es parte de la defensa?
  2. ¿Por qué "cambiar la contraseña cada mes" ya no se recomienda y qué se hace en su lugar?
  3. ¿Por qué el secreto TOTP se cifra en reposo y qué vale tu MFA si tu BD entera se filtra con secretos planos?
  4. Diseña el flujo de reset para una cuenta CON MFA: ¿qué exige además del email y por qué?
  5. ¿Por qué la respuesta del endpoint "forgot password" es idéntica exista o no el usuario?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.