Este tutorial adapta pedagógicamente el Día 68. Las secciones 📝 Contenido original del curso y 📝 Ejercicio original del curso provienen de las lecciones 482 a 493; las soluciones oficiales están en los gists enlazados y los archivos de inicio y final (
day-68-starting-files-flask-auth.zip, conusers.dbystatic/files/cheat_sheet.pdf, yday-68-flask-auth-end.zip) en los recursos. Los retos 🟢🟡🟠🔴, las secciones sobre ética y contraseñas en Colombia, el proyecto extra y la etapa 10 del proyecto integrador son creaciones de esta adaptación. Las cifras que da el curso sobre velocidad de las tarjetas gráficas se presentan como afirmaciones del curso. Las correcciones al material original aparecen como 🛠️ Nota técnica.
Nota sobre las verificaciones: el 27 de septiembre de 2026, con Python 3.14, Flask 3.1.3, Werkzeug 3.1.8, Flask-Login 0.6.3 y Flask-SQLAlchemy 3.1.1, se construyó el proyecto completo (registro, inicio y cierre de sesión, rutas protegidas, descarga y mensajes flash) y se probó con el cliente de pruebas de Flask. Se comprobó: el formato del hash que genera Werkzeug, que la misma contraseña produce hashes distintos gracias a la sal, que el método por defecto de Werkzeug 3.1 es scrypt, que una ruta protegida responde 401 si no se configura
login_view, y que el archivo "secreto" que el curso pone enstatic/files/se puede descargar sin iniciar sesión por la URL de los estáticos.
Tabla de contenidos
- Antes de empezar: ¿por qué tener usuarios?
- El proyecto de inicio y el registro de usuarios
- Descargar archivos
- Niveles de seguridad: de texto plano a cifrado
- Hash: una función de un solo sentido
- Cómo se descifran contraseñas (para defenderse)
- Sal y funciones lentas
- Hash con sal usando Werkzeug
- Flask-Login: iniciar sesión y proteger rutas
- Mensajes flash y estado de sesión en las plantillas
- 🚀 Proyecto del día: el sitio con secretos y usuarios
- 🌟 Proyecto extra: el banco de recursos del curso
- 🏗️ Proyecto integrador del bloque avanzado — Etapa 10
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 Evidencias de aprendizaje del Día 68
0. Antes de empezar: ¿por qué tener usuarios?
(Fuentes: 482-day-68-goals-login-and-registering-users-with-authentication.md y 483 - What is Authentication-.md)
📝 Contenido original del curso
"El componente más importante de un sitio web son sus usuarios": sin usuarios, Facebook serían solo anuncios y un blog, "los desvaríos de un autor". Para asociar datos a cada persona hay que registrarla y permitirle volver a entrar. Eso implica guardar información que debemos proteger. La autenticación consiste en comprobar que un usuario es quien dice ser.
Dos razones para autenticar:
- Asociar datos a cada usuario (sus mensajes privados, sus recetas) y que nadie más los vea.
- Restringir el acceso según el estado del usuario (por ejemplo, Spotify o Netflix solo muestran contenido a quien pagó).
Lo difícil no es hacer un registro, sino qué tan seguro es. La profesora promete llevar el sitio desde uno "protegido con un Cheeto" hasta uno acorde con los estándares de la industria.
El sitio del día: una portada con botones de registro e inicio de sesión; una vez autenticado, el usuario ve la página de secretos y puede descargar una "hoja de trucos de programación en Flask".
Prerequisitos: formularios y POST (Día 60), herencia de plantillas (Día 61), SQLAlchemy (Día 63), clases y herencia (Días 16 a 21), el cifrado César (Día 8).
📖 Autenticación y autorización (adaptación)
| Concepto | Pregunta | Ejemplo |
|---|---|---|
| Autenticación | ¿Quién eres? | Iniciar sesión con correo y contraseña |
| Autorización | ¿Qué puedes hacer? | Solo el docente puede borrar anuncios |
Hoy trabajas la autenticación; el Día 69, la autorización (solo el administrador publica).
1. El proyecto de inicio y el registro de usuarios
(Fuentes: 484-download-the-starting-project.md y 485-register-new-users.md)
📝 Ejercicio original del curso
Proyecto de inicio: entorno virtual y requirements.txt como siempre; incluye la base de datos users.db. Ábrela en DB Browser, ejecuta la aplicación y recórrela: todas las páginas se ven, pero los formularios de registro e inicio de sesión no funcionan. "Revisa todo el código antes de empezar".
Registrar un usuario: toma los datos del formulario de register.html, crea un objeto User con email, name y password, guárdalo en users.db y lleva al usuario a secrets.html. Deberías ver el nuevo registro en la base de datos.
Saludar al usuario: el <h1> de secrets.html debe decir "Hello ", con el nombre del registro.
📖 Explicación conceptual
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
new_user = User(
email=request.form.get("email"),
name=request.form.get("name"),
password=request.form.get("password"), # ⚠️ texto plano: lo corregirás en la sección 7
)
db.session.add(new_user)
db.session.commit()
return render_template("secrets.html", name=new_user.name)
return render_template("register.html")Esta primera versión guarda la contraseña tal como la escribió el usuario. Es el "nivel 1" de seguridad, y la base de todo lo que mejorarás hoy.
2. Descargar archivos
(Fuente: 486-downloading-files.md)
📝 Ejercicio original del curso
En secrets.html, el usuario debe poder descargar un archivo secreto ubicado en static > files > cheat_sheet.pdf, con la función send_from_directory() de Flask:
- El enlace de
secrets.htmlhace un GET a/download. - En la ruta
/download, usasend_from_directory()para enviarcheat_sheet.pdfal pulsar "Download Your File".
📖 Explicación conceptual (verificada)
from flask import send_from_directory
@app.route("/download")
def download():
return send_from_directory("static", path="files/cheat_sheet.pdf", as_attachment=True)send_from_directoryimpide salir de la carpeta indicada (por ejemplo, con../../), algo que sí podría pasar si armaras la ruta tú mismo.- Sin
as_attachment=True, la respuesta llevaContent-Disposition: inline(verificado) y el navegador abre el PDF en lugar de descargarlo.
🛠️ Nota técnica (seguridad, verificado): el archivo "secreto" está dentro de
static/, y Flask sirve todo lo que hay enstatic/a cualquiera:GET /static/files/cheat_sheet.pdfresponde 200 sin iniciar sesión. Proteger/download(sección 8) no sirve de nada si el archivo también es accesible por otra URL. Los archivos privados deben guardarse fuera destatic/(por ejemplo, en una carpetaprivado/) y entregarse solo desde una ruta protegida.
3. Niveles de seguridad: de texto plano a cifrado
(Fuente: 487 - Encryption and Hashing.md, primera parte)
📝 Contenido original del curso
Nivel 1: texto plano. Guardar la contraseña tal cual en la base de datos. Al menos no se ve en el código de la página, pero "no es suficiente".
Nivel 2: cifrado. Cifrar es "revolver" algo para que nadie sepa el original, a menos que conozca el secreto (la clave) para descifrarlo. Ejemplos:
- La máquina Enigma, que usaron los alemanes en la Segunda Guerra Mundial. Su falla permitió que Alan Turing y su equipo en Bletchley Park la descifraran con una computadora especializada (la profesora recomienda los videos de Numberphile y visitar Bletchley Park y su museo de computadoras).
- El cifrado César, de Julio César: sustituir cada letra por la que está un número fijo de posiciones adelante; la clave es ese número. Con desplazamiento 3, "hello" se vuelve "khoor" (herramienta en línea: cryptii.com).
Por qué un cifrado débil es peligroso: la historia de María Estuardo, reina de Escocia, que conspiró con Lord Babington contra Isabel I de Inglaterra mediante cartas cifradas con un método de sustitución parecido al César. El descifrador de Isabel rompió la clave, escribió cartas falsas para descubrir a los cómplices y María fue ejecutada. Las empresas con seguridad débil terminan "decapitadas" metafóricamente, como TalkTalk o Equifax. Recomendación: The Code Book (Los códigos secretos) de Simon Singh.
El eslabón débil del cifrado: hace falta guardar la clave en algún lugar, y quien logre entrar a la base de datos probablemente también la encuentre.
📖 Ya lo programaste (adaptación)
En el Día 8 escribiste el cifrado César. Con la clave, se revierte:
def cesar(texto, desplazamiento):
resultado = ""
for letra in texto:
if letra.isalpha():
base = ord("a") if letra.islower() else ord("A")
resultado += chr((ord(letra) - base + desplazamiento) % 26 + base)
else:
resultado += letra
return resultado
print(cesar("hello", 3)) # khoor
print(cesar("khoor", -3)) # hello ← con la clave, se recupera el originalEse es justamente el problema para contraseñas: si alguien tiene la clave, recupera todas.
4. Hash: una función de un solo sentido
(Fuente: 487 - Encryption and Hashing.md, segunda parte)
📝 Contenido original del curso
Nivel 3: hash. Elimina la necesidad de una clave. ¿Cómo se "descifra" entonces? No se descifra. Al registrarse, la contraseña pasa por una función hash y se guarda solo el resultado (el hash). Las funciones hash son fáciles de calcular hacia adelante y casi imposibles de revertir.
La analogía del 377: ¿cuáles son los factores de 377, además de 1 y 377? Hay que probar 2, 3, 4… hasta llegar a 13 × 29. En cambio, multiplicar 13 × 29 = 377 es inmediato. "Casi imposible" significa que, con la potencia de cómputo actual, tomaría demasiado tiempo: si calcular un hash toma un milisegundo y revertirlo dos años, "el pirata tiene mejores cosas que hacer".
Iniciar sesión: se calcula el hash de la contraseña que escribe el usuario y se compara con el guardado. Si coinciden, la contraseña es la misma. La única persona que conoce la contraseña es el propio usuario. En cryptii, con MD5 no existe la opción de "decodificar".
📖 Pruébalo (adaptación, verificado)
import hashlib
print(hashlib.md5(b"qwerty").hexdigest()) # d8578edf8458ce06fbc5bb76a58c5ca4
print(hashlib.md5(b"qwertz").hexdigest()) # cambia por completo con una sola letraPropiedades de un buen hash: mismo texto → mismo hash; un cambio mínimo → hash totalmente distinto; no hay forma práctica de volver atrás.
5. Cómo se descifran contraseñas (para defenderse)
(Fuente: 488 - How to Hack Passwords 101.md)
📝 Contenido original del curso
Con el hash ya estás "al nivel de la mayoría de los sitios". El sitio plaintextoffenders.com recopila correos de empresas que, al pedir recuperar la contraseña, se la envían en texto plano: señal de que la guardan sin hash. La profesora sugiere, si buscas trabajo, escribirles ofreciendo tus conocimientos, y, si tienes cuenta en esos sitios, borrarla.
Filtraciones reales: Adobe (2013), LinkedIn (2012) y otras. Hay quien compra esas cuentas filtradas y prueba las mismas contraseñas en PayPal u otros servicios. Recomienda revisar tu correo en un sitio que informa si apareció en filtraciones (como Have I Been Pwned) y cambiar las contraseñas repetidas.
Cómo ataca un pirata una base de datos con hashes:
- Hashes repetidos: si tres usuarios tienen el mismo hash, tienen la misma contraseña.
- Tabla de hashes: calcular el hash de las contraseñas más comunes (123456, qwerty, password, 111111…) y buscar coincidencias.
- Ataque de diccionario: todas las palabras de un diccionario (~150 000), los números de una guía telefónica y todas las combinaciones de hasta 6 caracteres: unos 19 800 millones de hashes. Según el curso, una tarjeta gráfica moderna calcula unos 20 000 millones de hashes MD5 por segundo: la tabla entera en 0,9 segundos.
- Hay tablas ya calculadas para las 10 000 contraseñas más comunes; incluso buscar un hash MD5 en Google suele devolver la contraseña.
La defensa del usuario: contraseñas largas. El tiempo para descifrar crece exponencialmente con la longitud. Según un verificador de contraseñas, 6 caracteres aleatorios se descifran en unos 3 segundos en un computador de escritorio; con 12, en unos 31 años (2 años con una GPU rápida). Evita palabras de diccionario, nombres de lugares o números de teléfono.
(Y un bono: hackertyper.net, para "parecer un pirata de Hollywood"… pero no en un avión.)
📖 Ética: conocer el ataque para defender (adaptación)
- Estas técnicas se estudian para proteger sistemas. Intentar entrar a cuentas o sistemas ajenos sin autorización es un delito: en Colombia, la Ley 1273 de 2009 tipifica el acceso abusivo a sistemas informáticos, entre otros delitos informáticos.
- Si encuentras una falla de seguridad en un sitio, lo responsable es reportarla a sus dueños por un canal oficial (divulgación responsable), no explotarla ni publicarla.
- Sobre la sugerencia del curso de escribir a las empresas de plaintextoffenders: si lo haces, sé respetuoso y profesional.
📖 Pruébalo: por qué la longitud importa (adaptación)
simbolos = 26 + 26 + 10 + 32 # minúsculas, mayúsculas, dígitos, símbolos
velocidad = 20_000_000_000 # hashes MD5 por segundo (cifra del curso)
for largo in (6, 8, 10, 12):
segundos = simbolos ** largo / velocidad
print(f"{largo} caracteres: {segundos / 31_536_000:,.4f} años")Ejecútalo: cada carácter extra multiplica el tiempo por 94.
6. Sal y funciones lentas
(Fuente: 489 - Salting Passwords.md)
📝 Contenido original del curso
Nivel 4: hash con sal. Además de la contraseña, se genera un conjunto aleatorio de caracteres (la sal), se combina con la contraseña y se calcula el hash de todo. Así:
- aunque la contraseña sea simple, el conjunto es más largo y complejo;
- tres usuarios con "qwerty" tienen hashes distintos, porque cada uno tiene su propia sal;
- la sal se guarda en la base de datos junto al hash (el usuario no tiene que recordarla); al iniciar sesión, se combina la contraseña escrita con la sal guardada y se compara.
Aun así, MD5 es demasiado rápido (20 000 millones por segundo). Por eso se usan algoritmos lentos a propósito, como bcrypt: según el curso, una GPU de 2019 calcula solo unos 17 000 hashes bcrypt por segundo; una tabla que con MD5 tomaría 3 segundos, con bcrypt tomaría unos 8 meses.
Rondas de sal: bcrypt permite fijar un número de "rondas". Por cada ronda que se agrega, el tiempo de cálculo se duplica, así que, a medida que las computadoras se vuelven más rápidas (ley de Moore), basta con subir un número (por ejemplo, de 10 a 12) sin cambiar el código.
🛠️ Nota técnica: el video explica las "rondas" como volver a salar y calcular el hash del resultado una y otra vez. La idea de fondo (repetir mucho trabajo para hacerlo lento) es correcta, pero en bcrypt el número es un factor de costo: con costo 12, el algoritmo hace 2¹² repeticiones internas de su preparación de la clave. Por eso cada +1 duplica el tiempo. PBKDF2 (el que usarás con Werkzeug) usa iteraciones, y scrypt, además, gasta memoria para dificultar el uso de tarjetas gráficas.
7. Hash con sal usando Werkzeug
(Fuente: 490-hashing-and-salting-passwords-using-werkzeug.md)
📝 Ejercicio original del curso
Ahora las contraseñas están en texto plano.
- Borra el usuario guardado sin hash.
- Con la documentación de Werkzeug, usa
generate_password_hash()para aplicar hash y sal a la contraseña antes de guardarla. Usa el métodopbkdf2:sha256consalt_length=8.
📖 Explicación conceptual (verificada con Werkzeug 3.1.8)
from werkzeug.security import generate_password_hash, check_password_hash
hash_y_sal = generate_password_hash("qwerty", method="pbkdf2:sha256", salt_length=8)
print(hash_y_sal)
# pbkdf2:sha256:1000000$dv2wMbPp$55fd3d2d4edbba6069dea4a4364f2a029147b4c13284cf809e2921578158eb2dEl texto guardado tiene todo lo necesario para verificar después:
| Parte | Significado |
|---|---|
pbkdf2:sha256:1000000 |
Método e iteraciones (un millón en Werkzeug 3.1) |
dv2wMbPp |
La sal (8 caracteres aleatorios) |
55fd3d… |
El hash |
Resultados verificados:
- Llamar dos veces a
generate_password_hash("qwerty", ...)da resultados distintos (sal diferente). check_password_hash(hash_y_sal, "qwerty")→True; con"Qwerty"→False.
🛠️ Nota técnica (verificado): en Werkzeug 3.1, sin argumentos,
generate_password_hash("qwerty")usa scrypt con sal de 16 caracteres (scrypt:32768:8:1$...), una opción más resistente que la del curso. Salvo que necesites compatibilidad con otro sistema, lo recomendable es dejar los valores por defecto:generate_password_hash(password).
8. Flask-Login: iniciar sesión y proteger rutas
(Fuente: 491-authenticating-users-with-flask-login.md)
🎯 Objetivo de aprendizaje
Configurar Flask-Login, iniciar y cerrar sesión, y proteger rutas para que solo las vean usuarios autenticados.
📝 Ejercicio original del curso
Hoy cualquiera puede escribir /secrets y ver la página. La mayoría de los desarrolladores de Flask usan Flask-Login.
💪 Reto difícil: con la documentación de Flask-Login, implementa /login y protege /secrets. Tendrás que configurar Flask-Login y modificar /register, /login, /secrets y /download. Orden recomendado:
- Configurar Flask-Login.
- Iniciar la sesión del usuario al registrarse.
- Proteger
/secretsy/download. - Escribir
/loginy/logout.
Pistas: configurar la aplicación para Flask-Login; crear el callback user_loader; que la clase User herede de UserMixin (un mixin es una forma de herencia múltiple: class MyClass(MixinClassB, MixinClassA, BaseClass):); comprobar la contraseña con check_password_hash; buscar al usuario por su correo con where; iniciar la sesión con login_user(); proteger rutas con login_required.
📖 Explicación conceptual (verificada)
from flask_login import UserMixin, LoginManager, login_user, login_required, current_user, logout_user
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "login" # a dónde enviar a quien no ha iniciado sesión
class User(UserMixin, db.Model):
...
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
email = request.form.get("email")
password = request.form.get("password")
user = db.session.execute(db.select(User).where(User.email == email)).scalar()
if user and check_password_hash(user.password, password):
login_user(user)
return redirect(url_for("secrets"))
... # sección 9: mensajes de error
return render_template("login.html")
@app.route("/secrets")
@login_required
def secrets():
return render_template("secrets.html", name=current_user.name)
@app.route("/logout")
def logout():
logout_user()
return redirect(url_for("home"))Cómo funciona:
login_user(user)guarda el id del usuario en una cookie de sesión firmada conSECRET_KEY(por eso la clave secreta debe ser secreta, Día 61).- En cada solicitud, Flask-Login lee la cookie y llama a
load_user(id)para obtener el usuario; queda disponible comocurrent_user. UserMixinagrega lo que Flask-Login necesita (is_authenticated,get_id()…).@login_requiredva debajo de@app.route(recuerda el orden de los decoradores, Día 55).
🛠️ Nota técnica (verificado): si no configuras
login_manager.login_view, visitar/secretssin sesión responde 401 Unauthorized, una página de error. Conlogin_view = "login", Flask-Login redirige al formulario de inicio de sesión, que es lo que el usuario espera.
🛠️ Nota técnica: en
user_loader, usadb.session.get(...), que devuelveNonesi el usuario ya no existe. Si usarasget_or_404, un usuario borrado con una cookie vieja vería un 404 en todas las páginas.
⚠️ Errores frecuentes
| Error | Causa | Solución |
|---|---|---|
Exception: Missing user_loader or request_loader |
No definiste @login_manager.user_loader |
Defínelo |
AttributeError: 'User' object has no attribute 'is_active' |
User no hereda de UserMixin |
class User(UserMixin, db.Model) |
/secrets sigue abierta |
@login_required encima de @app.route o ausente |
Debajo de la ruta |
| El inicio de sesión nunca funciona | Comparas user.password == password con un hash guardado |
check_password_hash(user.password, password) |
AttributeError: 'NoneType' object has no attribute 'password' |
El correo no existe | Comprueba if user antes (sección 9) |
9. Mensajes flash y estado de sesión en las plantillas
(Fuentes: 492-flask-flash-messages.md y 493-passing-authentication-status-to-templates.md)
📝 Ejercicio original del curso
Mensajes flash. En lugar de redirigir en silencio al inicio de sesión, conviene decirle al usuario qué pasó. Los mensajes flash se envían a la plantilla para mostrarse una sola vez y desaparecen al recargar.
- Si el correo no existe: mensaje flash y volver a
/login. (Un<p>en la página de inicio de sesión se verá en rojo.) - Si la contraseña es incorrecta: mensaje flash y volver a
/login. - En
/register, si el correo ya existe: redirigir a/logincon un mensaje de que ya está registrado.
Estado de sesión en las plantillas. Si el usuario ya inició sesión, la portada y la barra de navegación no deben mostrar los botones de registro e inicio de sesión, y la portada debe decir "(you are already logged in)". Hazlo en base.html e index.html.
📖 Explicación conceptual (verificada)
from flask import flash
if not user:
flash("That email does not exist, please try again.")
return redirect(url_for("login"))
elif not check_password_hash(user.password, password):
flash("Password incorrect, please try again.")
return redirect(url_for("login")){% with messages = get_flashed_messages() %}
{% for message in messages %}
<p class="flash">{{ message }}</p>
{% endfor %}
{% endwith %}
{% if not current_user.is_authenticated %}
<a href="{{ url_for('login') }}">Login</a>
<a href="{{ url_for('register') }}">Register</a>
{% else %}
<a href="{{ url_for('logout') }}">Log Out</a>
{% endif %}current_user está disponible en todas las plantillas gracias a Flask-Login (no hay que enviarlo con render_template).
🛠️ Nota técnica (seguridad): los mensajes distintos para "el correo no existe" y "contraseña incorrecta" son cómodos, pero le dicen a un atacante qué correos están registrados (enumeración de usuarios). Muchos sitios prefieren un solo mensaje: "Correo o contraseña incorrectos". Es un equilibrio entre facilidad de uso y seguridad; discútelo en clase.
10. 🚀 Proyecto del día: el sitio con secretos y usuarios
(Integra las lecciones 484 a 493.)
Problema
Un sitio necesita que cada persona tenga su cuenta, que solo los usuarios registrados vean ciertos contenidos y que, si roban la base de datos, las contraseñas no queden expuestas.
Objetivo
El sitio del curso con registro, inicio y cierre de sesión, contraseñas con hash y sal, rutas protegidas, descarga protegida, mensajes flash y navegación según el estado de sesión.
Requisitos funcionales
- Registro con hash y sal; si el correo ya existe, mensaje y redirección.
- Inicio de sesión con mensajes de error; cierre de sesión.
/secretsy/downloadsolo para usuarios autenticados, con redirección al inicio de sesión.- El archivo privado fuera de
static/. - Navegación que cambia según
current_user.is_authenticated.
Restricciones técnicas
SECRET_KEYen.env, generada consecrets.token_hex(32).generate_password_hashcon los valores por defecto (o los del curso, justificando).- Nunca comparar contraseñas en texto plano.
Planificación
- Comprender el proyecto de inicio. 2. Entradas: correo, nombre, contraseña. 3. Procesos: registrar, verificar, recordar la sesión, proteger. 4. Salidas: páginas, mensajes y el archivo. 5. Dividir en el orden recomendado por el curso. 6. Diseñar la tabla de pruebas. 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.
Construcción paso a paso
- Versión 1: registro en texto plano y saludo (lección 485).
- Versión 2: descarga (lección 486).
- Versión 3: hash con sal (lección 490).
- Versión 4: Flask-Login (lección 491).
- Versión 5: mensajes flash (lección 492).
- Versión final: navegación según la sesión (lección 493) y archivo fuera de
static/.
Pruebas (verificadas en el proyecto equivalente)
| Caso | Resultado esperado |
|---|---|
/secrets sin sesión |
Redirige a /login (o 401 sin login_view) |
| Registro nuevo | Entra directo a /secrets con "Hello " |
| Registro con correo repetido | Mensaje "You've already signed up…" |
| Contraseña incorrecta | Mensaje "Password incorrect…" |
| Base de datos en DB Browser | Contraseñas como scrypt:... o pbkdf2:sha256:..., nunca en texto plano |
| Dos usuarios con "qwerty" | Hashes distintos |
Cerrar sesión y volver a /secrets |
Redirige al inicio de sesión |
/static/files/cheat_sheet.pdf sin sesión |
No debe existir (archivo movido fuera de static/) |
🧪 Prueba y depuración
class User(db.Model):
...
@app.route("/login", methods=["POST"])
def login():
user = db.session.execute(db.select(User).where(User.email == request.form["email"])).scalar()
if user.password == generate_password_hash(request.form["password"]):
login_user(user)
return redirect(url_for("secrets"))
@login_required
@app.route("/secrets")
def secrets():
...Encuentra cinco problemas: User no hereda de UserMixin; si el correo no existe, user.password falla; comparar con un hash nuevo nunca coincide (la sal cambia; hay que usar check_password_hash); se redirige a /secrets aunque la contraseña sea incorrecta; y @login_required está encima de @app.route, así que no protege nada.
⭐ Extensión del proyecto
- Exigir contraseñas de al menos 12 caracteres con un validador de WTForms.
- Bloquear temporalmente una cuenta tras 5 intentos fallidos.
- "Recordarme":
login_user(user, remember=True).
11. 🌟 Proyecto extra: el banco de recursos del curso
(Proyecto creado para esta adaptación; no existe en el curso original.)
Situación
El docente quiere compartir guías y talleres (PDF) solo con los estudiantes del curso, no con todo Internet.
Requisitos
- Registro solo con un código de invitación que el docente entrega en clase (guardado en
.env) y un correo institucional (validar el dominio). - Contraseñas con los valores por defecto de Werkzeug; mínimo 12 caracteres.
- Los PDF en una carpeta
recursos/fuera destatic/, listados en/recursosy descargables en/recursos/<nombre>conlogin_requiredysend_from_directory. - Registro de descargas (usuario, archivo, fecha) en la base de datos.
- Mensajes flash para todos los errores y navegación según la sesión.
Decisiones que debes tomar (y justificar)
- ¿Qué pasa si un estudiante comparte su contraseña? ¿Cómo lo detectarías con el registro de descargas?
- ¿Por qué no basta con "esconder" el enlace de los PDF?
Casos de prueba
- Registro sin código o con código incorrecto → rechazado.
/recursos/guia1.pdfsin sesión → redirige al inicio de sesión./recursos/../main.py→ no entrega el código (lo impidesend_from_directory).
12. 🏗️ Proyecto integrador del bloque avanzado — Etapa 10
(Proyecto creado para esta adaptación. Descripción completa en el tutorial del Día 70.)
La Plataforma del Club — Etapa 10 (hoy):
- Modelo
UsuarioconUserMixin, registro, inicio y cierre de sesión con Flask-Login y contraseñas con Werkzeug. - Reemplaza la clave temporal del Día 67: crear, editar y borrar noticias y actividades requiere haber iniciado sesión.
- Mensajes flash y navegación según la sesión en
base.html. SECRET_KEYen.env.
13. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Qué diferencia hay entre autenticación y autorización?
- ¿Por qué cifrar contraseñas es peor que aplicarles hash?
- ¿Qué es la sal y qué problema resuelve?
- ¿Por qué se prefieren funciones de hash lentas para contraseñas?
- ¿Qué hacen
login_user,current_usery@login_required?
Análisis de código
h = generate_password_hash("clave123")
print(h.split("$")[0])
print(check_password_hash(h, "clave123"), check_password_hash(h, "clave124"))¿Qué imprime cada línea (con Werkzeug 3.1)?
Predicción
Si dos usuarios se registran con "qwerty", ¿tendrán el mismo valor en la columna password? ¿Y si se usara hashlib.md5 sin sal?
Depuración
"Me registro bien, pero cuando inicio sesión siempre dice Password incorrect". Da dos causas posibles.
Modificación
Cambia el mensaje de error del inicio de sesión para que sea uno solo ("Correo o contraseña incorrectos") y explica qué ganas y qué pierdes.
Creación
Escribe una función fortaleza(contraseña) que devuelva "débil", "media" o "fuerte" según la longitud y la variedad de caracteres, y úsala como validador propio en el formulario de registro.
14. 🤖 Programar con apoyo de IA
Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.
Actividad 1 — Revisar un sistema de inicio de sesión. Pide a una IA un inicio de sesión con Flask. Revisa: ¿usa hash con sal?, ¿check_password_hash?, ¿login_view?, ¿la clave secreta fuera del código? Compara con la documentación de Flask-Login y de Werkzeug.
Actividad 2 — ¿MD5 para contraseñas? Pregunta a una IA si MD5 o SHA-256 "solo" sirven para guardar contraseñas. Contrasta su respuesta con las secciones 5 y 6 y con la guía de OWASP sobre almacenamiento de contraseñas (Password Storage Cheat Sheet).
Actividad 3 — El archivo "protegido". Muestra a una IA el proyecto del curso con el PDF en static/files/ y la ruta /download protegida. Pregúntale si el archivo está protegido. ¿Detecta el problema de la sección 2?
15. 🛠️ Correcciones técnicas al material original
| Archivo | Afirmación o comportamiento del curso | Observación | Tratamiento |
|---|---|---|---|
486, 491 |
Archivo secreto en static/files/ y ruta /download protegida |
El archivo se descarga igual por /static/files/cheat_sheet.pdf sin sesión (verificado) |
Nota técnica de la sección 2 |
486 |
send_from_directory para "descargar" |
Sin as_attachment=True el navegador lo abre (inline, verificado) |
Sección 2 |
490 |
pbkdf2:sha256 con salt_length=8 |
Funciona (1 000 000 iteraciones en Werkzeug 3.1); el valor por defecto actual, scrypt con sal de 16, es más robusto (verificado) | Nota técnica de la sección 7 |
489 |
"Rondas de sal" como volver a salar el hash | En bcrypt es un factor de costo (2^costo repeticiones internas) | Nota técnica de la sección 6 |
489 |
"bscript" (transcripción) | Es bcrypt | Sección 6 |
491 |
Proteger rutas con login_required |
Sin login_view, la respuesta es un 401 y no una redirección (verificado) |
Nota técnica de la sección 8 |
492 |
Mensajes distintos para correo inexistente y contraseña incorrecta | Permiten saber qué correos están registrados | Nota técnica de la sección 9 |
488 |
Cifras de velocidad de las GPU y tiempos del verificador de contraseñas | No verificadas; dependen del equipo y del año | Presentadas como afirmaciones del curso |
488 |
Técnicas de ataque | Se agrega el marco ético y legal (Ley 1273 de 2009) | Sección 5 |
16. 📋 Matriz de trazabilidad
| Contenido original | Archivo de origen | Concepto desarrollado | Ejercicio original | Reto adicional | Proyecto relacionado |
|---|---|---|---|---|---|
| Importancia de los usuarios, registrar e iniciar sesión, descarga para registrados | 482 |
Sección 0 | — | Autenticación contra autorización | Sección 10 |
| Por qué autenticar, asociar datos, restringir acceso, del "Cheeto" a la industria, estructura del sitio | 483 |
Sección 0 | — | — | Sección 10 |
Proyecto de inicio, users.db, recorrer el código |
484 |
Sección 1 | Preparar el proyecto | — | Sección 10 |
Registrar User, redirigir a secrets.html, "Hello " |
485 |
Sección 1 | Registro y saludo | — | Secciones 10 y 11 |
send_from_directory, /download, cheat_sheet.pdf |
486 |
Sección 2 | Descarga | Archivo fuera de static/, as_attachment |
Secciones 10 y 11 |
| Nivel 1 y 2, cifrado, Enigma, Turing, Bletchley Park, César, cryptii, María Estuardo, TalkTalk, Equifax, The Code Book, nivel 3 (hash), 377 = 13 × 29, MD5 | 487 |
Secciones 3 y 4 | — | César en Python; MD5 con hashlib |
Sección 10 |
| plaintextoffenders, Adobe, LinkedIn, hashes repetidos, tablas, diccionario, GPU, contraseñas largas, verificador, hackertyper | 488 |
Sección 5 | — | Ética y ley; cálculo del tiempo | Secciones 10 y 11 |
| Sal, hash con sal, bcrypt, 17 000 por segundo, 8 meses, rondas, ley de Moore | 489 |
Sección 6 | — | Factor de costo | Sección 10 |
generate_password_hash, pbkdf2:sha256, salt_length=8 |
490 |
Sección 7 | Hash con sal | Valores por defecto de Werkzeug 3.1 | Secciones 10, 11 y 12 |
Flask-Login, reto difícil, orden recomendado, pistas 1–7, UserMixin, mixins |
491 |
Sección 8 | Reto de Flask-Login | login_view, user_loader con None |
Secciones 10, 11 y 12 |
| Mensajes flash: correo inexistente, contraseña incorrecta, correo ya registrado | 492 |
Sección 9 | Retos 1–3 | Enumeración de usuarios | Secciones 10 y 11 |
Ocultar botones según la sesión, "(you are already logged in)", base.html |
493 |
Sección 9 | Estado en las plantillas | — | Secciones 10 y 12 |
| (No existe en el curso original) | — | — | — | Banco de recursos del curso | Sección 11 |
| (No existe en el curso original) | — | — | — | Plataforma del Club, etapa 10 | Sección 12 |
17. 📦 Evidencias de aprendizaje del Día 68
- El sitio con usuarios funcionando y la tabla de pruebas completa.
- Captura de
users.dben DB Browser con las contraseñas en forma de hash (de usuarios de prueba, no reales). - La demostración de que el PDF ya no se descarga por
/static/.... - El cálculo del tiempo de descifrado según la longitud (sección 5).
- El banco de recursos del curso (proyecto extra).
- La etapa 10 de la Plataforma del Club.
- Cuaderno de errores: contraseña en texto plano, comparar contraseñas con
==, falta deUserMixin, falta deuser_loader,login_requiredencima de la ruta, 401 sinlogin_view,Noneal buscar un correo inexistente, archivo privado enstatic/,SECRET_KEYen el código.