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. Ejercicio 1 — El hash en la mano
  2. Ejercicio 2 — Enumeración y mensajes
  3. Ejercicio 3 — TOTP completo
  4. Ejercicio 4 — Códigos de recuperación
  5. Ejercicio 5 — Reset seguro
  6. Ejercicio 6 — El mapa de factores
  7. Ejercicio 7 — El pentest de las puertas laterales
  8. Entrega

pip install django[argon2] pyotp qrcode. No mires solutions.md hasta entregar.

Ejercicio 1 — El hash en la mano

  1. Cambia PASSWORD_HASHERS para que Argon2id sea el primero. Crea un usuario nuevo y mira su hash en shell (user.password): identifica algoritmo, m (memoria), t (iteraciones), p (paralelismo) y el salt.
  2. Mide con time.perf_counter(): check_password("xxx", hash) 5 veces. ¿Cuánto tarda una verificación? ¿Cuántos intentos/segundo haría un atacante con 1 thread? ¿Y 10.000 GPUs? (razona el orden de magnitud).
  3. Sube iterations en bcrypt o memory_cost en argon2 al doble: re-mide. ¿Qué pagas (login) y qué ganas (brute force)?
  4. Añade django-pwned-passwords como validador: intenta registrarte con "password123" y captura la respuesta.

Ejercicio 2 — Enumeración y mensajes

  1. En tu login: login con email inexistente y login con contraseña errónea. ¿Los tiempos difieren? ¿Los mensajes? Iguala ambos ("Credenciales inválidas") y justifica.
  2. Intenta ?user=admin en tu forgot-password: ¿la respuesta filtra si existe? Corrige a respuesta única.

Ejercicio 3 — TOTP completo

  1. Modelo MFA(user OneToOne, secret_encrypted, confirmed bool) + vista de enrolamiento: genera secreto, renderiza QR (qrcode → SVG inline), confirma con un código válido.
  2. Login con MFA: si mfa.confirmed, el login devuelve {"mfa_required": true} en vez de tokens; segundo endpoint verifica el código TOTP y EMITE tokens (18). Demuéstralo con el flujo completo.
  3. Relojes: usa valid_window=1 y explica qué ventana de tiempo aceptas con eso.

Ejercicio 4 — Códigos de recuperación

  1. Al confirmar MFA: genera 10 códigos (secrets.token_hex(4) formateados), muéstralos UNA vez, guarda hash de cada uno (check_password) y used_at NULL.
  2. Permite login con código de recuperación cuando no hay TOTP a mano: verificación con constant-time + marca used_at en la misma transacción (¿qué evita usar dos veces el mismo código a la vez? conéctalo con la 10).
  3. Contador de códigos restantes en el perfil + aviso a los 3.

Ejercicio 5 — Reset seguro

  1. Implementa el flujo completo: POST /api/auth/forgot/ (respuesta única siempre) + email simulado (imprime el link) + POST /api/auth/reset/ con token+password nueva.
  2. Verifica: token usado dos veces → el segundo rechaza; token de hace 2h → rechaza; tras reset exitoso, todos los refresh tokens del usuario quedan invalidados (18).
  3. Para una cuenta con MFA: ¿qué exige tu reset? Implementa al menos la exigencia de un código de recuperación/TOTP al usar el link.

Ejercicio 6 — El mapa de factores

  1. Clasifica los factores que tu TicketFlow soporta HOY en la tabla del §5 y decide cuál añadirías: ¿passkeys con django-webauthn, o reforzar la recovery? Escribe la decisión con su razón (49: el trade-off). Nota: si eliges passkeys, el mínimo viable es registrar un credential (clave pública) y verificar el desafío en el login.
  2. La rotación de sesión: implementa que el reset de contraseña (y el cambio de MFA) invalida TODAS las sesiones activas del usuario (18: la blacklist de refresh + el jti). Test: login en 2 dispositivos → reset desde el 1 → el refresh del 2 falla.

Ejercicio 7 — El pentest de las puertas laterales

  1. Enumera TODAS las vías de volver a entrar en una cuenta de TicketFlow (contraseña, reset por email, códigos de recuperación, soporte humano, cambio de email del perfil). Para cada una: ¿qué exige? ¿Alguna es más débil que el resto?
  2. El protocolo del soporte: escribe (48: el runbook) los 5 pasos que sigue el soporte humano ante "he perdido todo": qué verifica, qué NO hace jamás (¿reset por llamada?), y qué registra en el audit log (56).
  3. El test de enumeración del reset: automatiza 20 peticiones de reset (10 con emails existentes, 10 inventados): ¿la respuesta y el timing son idénticos en ambos grupos? Mide el tiempo de respuesta de ambos grupos: ¿la diferencia es detectable? (si sí: el flujo hace trabajo desigual según la existencia del email — el hallazgo del timing attack).

Entrega

Pega hashes (recortados), tiempos y el flujo del QR. Después: Lección 21 — Autorización.