Compartir en Twitter
Go to Homepage

CÓMO AUTENTICAR USUARIOS EN FLASK CON SEGURIDAD

November 17, 2025

Introducción a la Autenticación Segura en Flask

La autenticación de usuarios es un componente esencial en el desarrollo de aplicaciones web modernas, especialmente cuando se manejan datos sensibles. Flask, un microframework de Python, ofrece flexibilidad para implementar sistemas de autenticación robustos. En este tutorial, exploraremos cómo crear una aplicación web con autenticación utilizando Flask y flask_login, incluyendo funcionalidades como registro, inicio de sesión, cierre de sesión y validaciones de formularios. Este artículo está diseñado para desarrolladores que buscan implementar sistemas de autenticación seguros en sus proyectos, con un enfoque en la estructura del código y las mejores prácticas de seguridad. A lo largo del tutorial, proporcionaremos ejemplos de código y explicaciones detalladas para garantizar una comprensión completa.

Configuración Inicial del Proyecto

Para comenzar, es necesario configurar un entorno de desarrollo adecuado. Recomendamos usar un entorno virtual para aislar las dependencias del proyecto. La estructura básica del proyecto incluirá un directorio principal con los archivos de la aplicación y un entorno virtual. A continuación, se muestra la estructura de directorios:

.
├── auth-app
│   ├── app.py
│   ├── database.db
│   ├── forms.py
│   ├── manage.py
│   ├── migrations
│   ├── models.py
│   ├── requirements.txt
│   ├── routes.py
│   ├── run
│   ├── static
│   └── templates
│       ├── auth.html
│       ├── base.html
│       └── index.html
└── venv

Para instalar las dependencias, crea un archivo requirements.txt con las siguientes librerías:

Flask==3.0.3
Flask-SQLAlchemy==3.1.1
Flask-Bcrypt==1.0.1
Flask-Migrate==4.0.7
Flask-Login==0.6.3
WTForms==3.1.2

Ejecuta el siguiente comando en tu terminal para instalar las dependencias dentro del entorno virtual:

pip install -r requirements.txt

Este paso asegura que todas las librerías necesarias estén disponibles, incluyendo Flask-SQLAlchemy para bases de datos, Bcrypt para el cifrado de contraseñas y flask_login para la gestión de sesiones.

Creación de la Aplicación con Factory Pattern

En Flask, el uso de una función factory para crear la aplicación es una práctica recomendada, especialmente en proyectos que escalan. Esta función, comúnmente llamada create_app, permite inicializar la aplicación y sus extensiones de manera modular. A continuación, se muestra el código para app.py:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_migrate import Migrate
from flask_login import LoginManager

db = SQLAlchemy()
bcrypt = Bcrypt()
migrate = Migrate()
login_manager = LoginManager()
login_manager.session_protection = "strong"
login_manager.login_view = "login"
login_manager.login_message_category = "info"

def create_app():
    app = Flask(__name__)
    app.secret_key = 'tu-clave-secreta'
    app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///database.db"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

    db.init_app(app)
    bcrypt.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)

    return app

En este código, configuramos la clave secreta, la URI de la base de datos SQLite y inicializamos las extensiones. La función create_app asegura que las extensiones como SQLAlchemy y flask_login se vinculen correctamente a la aplicación, evitando problemas en proyectos más grandes.

Definición del Modelo de Usuario

El modelo de usuario es la base para almacenar la información de los usuarios en la base de datos. Utilizaremos SQLAlchemy para definir un modelo User que incluya los campos esenciales: identificador, nombre de usuario, correo electrónico y contraseña. Además, integraremos UserMixin de flask_login para facilitar la gestión de sesiones. El archivo models.py se configura como sigue:

from flask_login import UserMixin
from app import db

class User(UserMixin, db.Model):
    __tablename__ = "user"
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    pwd = db.Column(db.String(300), nullable=False)

    def __repr__(self):
        return f'<User {self.username}>'

El uso de UserMixin proporciona métodos como is_authenticated() y get_id(), esenciales para la autenticación. El campo pwd almacenará la contraseña cifrada, asegurando que las contraseñas no se guarden en texto plano.

Configuración de la Base de Datos

Para crear la tabla de usuarios en la base de datos, utilizaremos Flask-Migrate. El archivo manage.py contendrá un script para inicializar y migrar la base de datos. A continuación, se muestra el código:

from app import create_app, db
from flask_migrate import init, stamp, migrate, upgrade
from models import User

def deploy():
    app = create_app()
    with app.app_context():
        db.create_all()
        init()
        stamp()
        migrate()
        upgrade()

if __name__ == "__main__":
    deploy()

Ejecuta el script con el siguiente comando:

python manage.py

Este comando crea la base de datos database.db y aplica las migraciones necesarias, asegurando que la tabla user esté lista para su uso.

Creación de Formularios con WTForms

Los formularios son cruciales para capturar y validar los datos de los usuarios. Utilizaremos WTForms para definir dos formularios: uno para el registro y otro para el inicio de sesión. El archivo forms.py contendrá las siguientes definiciones:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import InputRequired, Length, Email, EqualTo, Regexp, Optional
from wtforms import ValidationError
from models import User

class RegisterForm(FlaskForm):
    username = StringField(
        validators=[
            InputRequired(),
            Length(3, 20, message="El nombre debe tener entre 3 y 20 caracteres"),
            Regexp(
                "^[A-Za-z][A-Za-z0-9_.]*$",
                message="El nombre solo puede contener letras, números, puntos o guiones bajos"
            )
        ]
    )
    email = StringField(validators=[InputRequired(), Email(), Length(1, 64)])
    pwd = PasswordField(validators=[InputRequired(), Length(8, 72)])
    cpwd = PasswordField(
        validators=[
            InputRequired(),
            Length(8, 72),
            EqualTo("pwd", message="Las contraseñas deben coincidir")
        ]
    )

    def validate_email(self, email):
        if User.query.filter_by(email=email.data).first():
            raise ValidationError("El correo ya está registrado")

    def validate_username(self, username):
        if User.query.filter_by(username=username.data).first():
            raise ValidationError("El nombre de usuario ya está en uso")

class LoginForm(FlaskForm):
    email = StringField(validators=[InputRequired(), Email(), Length(1, 64)])
    pwd = PasswordField(validators=[InputRequired(), Length(8, 72)])
    username = StringField(validators=[Optional()])

El formulario de registro incluye validaciones para asegurar que el nombre de usuario y el correo electrónico sean únicos, mientras que el formulario de inicio de sesión valida el correo y la contraseña. Estas validaciones mejoran la seguridad en los formularios al prevenir errores comunes.

Definición de Rutas

Las rutas definen los puntos de acceso de la aplicación. En routes.py, configuraremos las rutas para la página principal, el registro, el inicio de sesión y el cierre de sesión. También definiremos un cargador de usuarios para flask_login. A continuación, se muestra el código:

from flask import render_template, redirect, url_for, flash, request
from app import create_app
from forms import LoginForm, RegisterForm
from models import User
from flask_login import login_user, logout_user, login_required, LoginManager
from flask_bcrypt import check_password_hash

app = create_app()
login_manager = LoginManager()
login_manager.init_app(app)

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

@app.route("/", methods=["GET", "POST"])
def index():
    return render_template("index.html", title="Inicio")

@app.route("/login/", methods=["GET", "POST"])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        try:
            user = User.query.filter_by(email=form.email.data).first()
            if user and check_password_hash(user.pwd, form.pwd.data):
                login_user(user)
                return redirect(url_for('index'))
            else:
                flash("Correo o contraseña incorrectos", "danger")
        except Exception as e:
            flash(str(e), "danger")
    return render_template("auth.html", form=form, btn_action="Iniciar Sesión")

@app.route("/register/", methods=["GET", "POST"])
def register():
    form = RegisterForm()
    if form.validate_on_submit():
        try:
            new_user = User(
                username=form.username.data,
                email=form.email.data,
                pwd=app.bcrypt.generate_password_hash(form.pwd.data)
            )
            db.session.add(new_user)
            db.session.commit()
            flash("Cuenta creada exitosamente", "success")
            return redirect(url_for("login"))
        except Exception as e:
            flash(str(e), "danger")
    return render_template("auth.html", form=form, btn_action="Registrar")

@app.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))

Estas rutas manejan la lógica de autenticación, incluyendo la validación de formularios, el cifrado de contraseñas y la gestión de sesiones. La función load_user asegura que flask_login pueda recuperar al usuario actual durante la sesión.

Diseño de la Interfaz con Plantillas

Las plantillas HTML definen la interfaz de usuario. Crearemos un archivo auth.html que servirá tanto para el formulario de registro como para el de inicio de sesión, utilizando Jinja2 para la lógica condicional. A continuación, se muestra el código:

<!DOCTYPE html>
<html lang="es">
    <head>
        <meta charset="UTF-8" />
        <title>{{ title }}</title>
        <link
            href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
            rel="stylesheet"
        />
    </head>
    <body>
        <div class="container mt-5">
            <form
                action="{{ request.path }}"
                method="POST"
                class="col-md-6 mx-auto"
            >
                {{ form.csrf_token }} {% with messages =
                get_flashed_messages(with_categories=true) %} {% if messages %}
                {% for category, message in messages %}
                <div
                    class="alert alert-{{ category }} alert-dismissible fade show"
                    role="alert"
                >
                    {{ message }}
                    <button
                        type="button"
                        class="btn-close"
                        data-bs-dismiss="alert"
                    ></button>
                </div>
                {% endfor %} {% endif %} {% endwith %} {% if request.path ==
                '/register/' %}
                <div class="mb-3">
                    {{ form.username(class_="form-control", placeholder="Nombre
                    de usuario") }} {% for error in form.username.errors %}
                    <small class="text-danger">{{ error }}</small>
                    {% endfor %}
                </div>
                {% endif %}

                <div class="mb-3">
                    {{ form.email(class_="form-control", placeholder="Correo
                    electrónico") }} {% for error in form.email.errors %}
                    <small class="text-danger">{{ error }}</small>
                    {% endfor %}
                </div>

                <div class="mb-3">
                    {{ form.pwd(class_="form-control", placeholder="Contraseña")
                    }} {% for error in form.pwd.errors %}
                    <small class="text-danger">{{ error }}</small>
                    {% endfor %}
                </div>

                {% if request.path == '/register/' %}
                <div class="mb-3">
                    {{ form.cpwd(class_="form-control", placeholder="Confirmar
                    contraseña") }} {% for error in form.cpwd.errors %}
                    <small class="text-danger">{{ error }}</small>
                    {% endfor %}
                </div>
                {% endif %}

                <button type="submit" class="btn btn-primary w-100">
                    {{ btn_action }}
                </button>

                <p class="mt-3 text-center">
                    {% if request.path != '/register/' %} ¿Nuevo aquí?
                    <a href="{{ url_for('register') }}">Crear cuenta</a> {% else
                    %} ¿Ya tienes cuenta?
                    <a href="{{ url_for('login') }}">Iniciar sesión</a>
                    {% endif %}
                </p>
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
    </body>
</html>

Esta plantilla utiliza Bootstrap 5 para un diseño responsivo y muestra mensajes de error o éxito mediante alertas. La lógica condicional de Jinja2 permite reutilizar la misma plantilla para ambos formularios, mejorando la eficiencia en el desarrollo.

Implementación de Seguridad en la Autenticación

La seguridad es una prioridad en cualquier sistema de autenticación. En este proyecto, implementamos varias medidas para proteger a los usuarios:

  1. Cifrado de contraseñas: Utilizamos Bcrypt para generar un hash seguro de las contraseñas antes de almacenarlas. Esto asegura que, incluso en caso de una brecha de datos, las contraseñas no sean legibles.

  2. Validación de formularios: Los formularios incluyen validaciones en el cliente y el servidor para prevenir entradas inválidas, como correos duplicados o contraseñas débiles.

  3. Protección CSRF: Flask-WTF agrega un token CSRF a los formularios, protegiendo contra ataques de falsificación de solicitudes entre sitios.

  4. Gestión de sesiones segura: Flask-Login configura la protección de sesiones en modo “strong”, lo que detecta cambios en la dirección IP o el agente de usuario, cerrando sesiones sospechosas.

Estas medidas aseguran que la aplicación cumpla con los estándares de seguridad modernos, protegiendo tanto a los usuarios como a los desarrolladores.

Ejecución y Pruebas de la Aplicación

Para ejecutar la aplicación, asegúrate de que el entorno virtual esté activado y ejecuta el siguiente comando:

python routes.py

Esto iniciará el servidor de desarrollo de Flask en http://localhost:5000. Puedes probar las funcionalidades visitando las rutas / (página principal), /login/ (inicio de sesión), /register/ (registro) y /logout (cierre de sesión). Durante las pruebas, verifica que:

  • El registro crea un nuevo usuario y redirige al inicio de sesión.
  • El inicio de sesión autentica correctamente y redirige a la página principal.
  • El cierre de sesión termina la sesión y redirige al inicio de sesión.
  • Los mensajes de error se muestran correctamente para entradas inválidas.

Conclusiones

En este tutorial, hemos construido una aplicación web con autenticación de usuarios utilizando Flask y flask_login. Desde la configuración del entorno hasta la implementación de rutas y plantillas, cada paso se diseñó para ser claro y seguro. La integración de Bcrypt para cifrado, validaciones de formularios y protección CSRF demuestra cómo Flask puede utilizarse para crear aplicaciones robustas. Este proyecto es un punto de partida ideal para desarrolladores que deseen explorar el desarrollo web con Python, ofreciendo una base sólida para agregar funcionalidades avanzadas como recuperación de contraseñas o autenticación multifactor en el futuro. Continúa experimentando con Flask para personalizar y escalar tus aplicaciones según las necesidades de tus proyectos.