Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 68: Autenticación — registro, inicio de sesión y contraseñas seguras

100 Días de Código en Python — Día 68: Autenticación — registro, inicio de sesión y contraseñas seguras

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 68 (nivel avanzado).

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, con users.db y static/files/cheat_sheet.pdf, y day-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 en static/files/ se puede descargar sin iniciar sesión por la URL de los estáticos.


Tabla de contenidos

  1. Antes de empezar: ¿por qué tener usuarios?
  2. El proyecto de inicio y el registro de usuarios
  3. Descargar archivos
  4. Niveles de seguridad: de texto plano a cifrado
  5. Hash: una función de un solo sentido
  6. Cómo se descifran contraseñas (para defenderse)
  7. Sal y funciones lentas
  8. Hash con sal usando Werkzeug
  9. Flask-Login: iniciar sesión y proteger rutas
  10. Mensajes flash y estado de sesión en las plantillas
  11. 🚀 Proyecto del día: el sitio con secretos y usuarios
  12. 🌟 Proyecto extra: el banco de recursos del curso
  13. 🏗️ Proyecto integrador del bloque avanzado — Etapa 10
  14. 🧩 Comprueba lo aprendido
  15. 🤖 Programar con apoyo de IA
  16. 🛠️ Correcciones técnicas al material original
  17. 📋 Matriz de trazabilidad
  18. 📦 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:

  1. Asociar datos a cada usuario (sus mensajes privados, sus recetas) y que nadie más los vea.
  2. 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

Python
@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:

  1. El enlace de secrets.html hace un GET a /download.
  2. En la ruta /download, usa send_from_directory() para enviar cheat_sheet.pdf al pulsar "Download Your File".

📖 Explicación conceptual (verificada)

Python
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_directory impide 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 lleva Content-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 en static/ a cualquiera: GET /static/files/cheat_sheet.pdf responde 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 de static/ (por ejemplo, en una carpeta privado/) 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:

Python
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 original

Ese 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)

Python
import hashlib

print(hashlib.md5(b"qwerty").hexdigest())   # d8578edf8458ce06fbc5bb76a58c5ca4
print(hashlib.md5(b"qwertz").hexdigest())   # cambia por completo con una sola letra

Propiedades 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:

  1. Hashes repetidos: si tres usuarios tienen el mismo hash, tienen la misma contraseña.
  2. Tabla de hashes: calcular el hash de las contraseñas más comunes (123456, qwerty, password, 111111…) y buscar coincidencias.
  3. 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.
  4. 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)

Python
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.

  1. Borra el usuario guardado sin hash.
  2. 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étodo pbkdf2:sha256 con salt_length=8.

📖 Explicación conceptual (verificada con Werkzeug 3.1.8)

Python
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$55fd3d2d4edbba6069dea4a4364f2a029147b4c13284cf809e2921578158eb2d

El 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:

  1. Configurar Flask-Login.
  2. Iniciar la sesión del usuario al registrarse.
  3. Proteger /secrets y /download.
  4. Escribir /login y /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)

Python
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 con SECRET_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 como current_user.
  • UserMixin agrega lo que Flask-Login necesita (is_authenticated, get_id()…).
  • @login_required va 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 /secrets sin sesión responde 401 Unauthorized, una página de error. Con login_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, usa db.session.get(...), que devuelve None si el usuario ya no existe. Si usaras get_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.

  1. 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.)
  2. Si la contraseña es incorrecta: mensaje flash y volver a /login.
  3. En /register, si el correo ya existe: redirigir a /login con 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)

Python
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"))
HTML
{% 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

  1. Registro con hash y sal; si el correo ya existe, mensaje y redirección.
  2. Inicio de sesión con mensajes de error; cierre de sesión.
  3. /secrets y /download solo para usuarios autenticados, con redirección al inicio de sesión.
  4. El archivo privado fuera de static/.
  5. Navegación que cambia según current_user.is_authenticated.

Restricciones técnicas

  • SECRET_KEY en .env, generada con secrets.token_hex(32).
  • generate_password_hash con los valores por defecto (o los del curso, justificando).
  • Nunca comparar contraseñas en texto plano.

Planificación

  1. 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

Python
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

  1. 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).
  2. Contraseñas con los valores por defecto de Werkzeug; mínimo 12 caracteres.
  3. Los PDF en una carpeta recursos/ fuera de static/, listados en /recursos y descargables en /recursos/<nombre> con login_required y send_from_directory.
  4. Registro de descargas (usuario, archivo, fecha) en la base de datos.
  5. 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.pdf sin sesión → redirige al inicio de sesión.
  • /recursos/../main.py → no entrega el código (lo impide send_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):

  1. Modelo Usuario con UserMixin, registro, inicio y cierre de sesión con Flask-Login y contraseñas con Werkzeug.
  2. Reemplaza la clave temporal del Día 67: crear, editar y borrar noticias y actividades requiere haber iniciado sesión.
  3. Mensajes flash y navegación según la sesión en base.html.
  4. SECRET_KEY en .env.

13. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Qué diferencia hay entre autenticación y autorización?
  2. ¿Por qué cifrar contraseñas es peor que aplicarles hash?
  3. ¿Qué es la sal y qué problema resuelve?
  4. ¿Por qué se prefieren funciones de hash lentas para contraseñas?
  5. ¿Qué hacen login_user, current_user y @login_required?

Análisis de código

Python
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.db en 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 de UserMixin, falta de user_loader, login_required encima de la ruta, 401 sin login_view, None al buscar un correo inexistente, archivo privado en static/, SECRET_KEY en el código.