pip install django[argon2] pyotp qrcode. No mires solutions.md hasta entregar.
Ejercicio 1 — El hash en la mano
- Cambia
PASSWORD_HASHERSpara 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. - 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). - Sube
iterationsen bcrypt o memory_cost en argon2 al doble: re-mide. ¿Qué pagas (login) y qué ganas (brute force)? - Añade
django-pwned-passwordscomo validador: intenta registrarte con "password123" y captura la respuesta.
Ejercicio 2 — Enumeración y mensajes
- 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.
- Intenta
?user=adminen tu forgot-password: ¿la respuesta filtra si existe? Corrige a respuesta única.
Ejercicio 3 — TOTP completo
- 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. - 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. - Relojes: usa
valid_window=1y explica qué ventana de tiempo aceptas con eso.
Ejercicio 4 — Códigos de recuperación
- Al confirmar MFA: genera 10 códigos (
secrets.token_hex(4)formateados), muéstralos UNA vez, guardahashde cada uno (check_password) yused_atNULL. - 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).
- Contador de códigos restantes en el perfil + aviso a los 3.
Ejercicio 5 — Reset seguro
- Implementa el flujo completo:
POST /api/auth/forgot/(respuesta única siempre) + email simulado (imprime el link) +POST /api/auth/reset/con token+password nueva. - 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).
- 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
- 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.
- 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
- 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?
- 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).
- 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.