Flask Authentication: Login, Sessions, and Logout

• Python • By TryzTech Team
FlaskPythonBackendWeb Development

Authentication answers an important question on every request: which user is using the application? The answer cannot be checked only when the login form is submitted. The application must remember that identity for later requests, restrict selected pages, and remove it again when the user logs out.

Flask can store a small identifier such as user_id in the session. The browser carries the session cookie on later requests, and Flask verifies and reads its contents. The password never belongs there; it is used briefly to compare against a hash in the database.

Store a Hash, Not the Original Password

Add email and password_hash fields to the User model. These methods keep hash creation and verification in one place:

# File: models.py
from werkzeug.security import check_password_hash, generate_password_hash


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True, nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

generate_password_hash() creates a value that differs from the original password and includes a salt. During login, check_password_hash() checks the input without turning the hash back into the original password.

Do not apply plain SHA-256 directly to passwords. Password hashing needs a deliberately slow, salted algorithm that makes large-scale guessing more expensive.

Put Login and Logout in One Blueprint

The login route handles two needs: GET renders the form, while POST verifies credentials.

# File: routes/auth.py
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
from models import User, db

auth_bp = Blueprint("auth", __name__)


@auth_bp.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        email = request.form.get("email", "").strip().lower()
        password = request.form.get("password", "")
        query = db.select(User).where(User.email == email)
        user = db.session.execute(query).scalar_one_or_none()

        if user is None or not user.check_password(password):
            flash("The email or password is incorrect.", "error")
        else:
            session.clear()
            session["user_id"] = user.id
            return redirect(url_for("notes.index"))

    return render_template("auth/login.html")


@auth_bp.post("/logout")
def logout():
    session.clear()
    return redirect(url_for("auth.login"))

The email is normalized with .strip().lower() so whitespace and capitalization do not break the lookup. The error message is deliberately identical for an unknown email and an incorrect password. This keeps the response from revealing which accounts exist.

Calling session.clear() before storing user_id removes previous session data. After login, the session contains only the minimum identity needed. Other user data is still read from the database, so changes to a name or account status do not become stale inside the cookie.

Logout uses POST because it changes session state. The login template can use a small form:

<!-- File: templates/auth/login.html -->
<h1>Login</h1>

{% with messages = get_flashed_messages() %}
  {% for message in messages %}
    <p>{{ message }}</p>
  {% endfor %}
{% endwith %}

<form method="post" action="{{ url_for('auth.login') }}">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required>

  <label for="password">Password</label>
  <input id="password" name="password" type="password" autocomplete="current-password" required>

  <button type="submit">Login</button>
</form>

Load the User for Every Request

The session stores an ID, not a User object. Use before_app_request to query the user once at the start of a request and place it in g.user:

# File: routes/auth.py
from flask import g


@auth_bp.before_app_request
def load_logged_in_user():
    user_id = session.get("user_id")
    g.user = db.session.get(User, user_id) if user_id is not None else None

The g object lasts for only one request. Templates and routes can read g.user, but it is not stored for the next request. If the user has been deleted from the database, the query returns None, and the application no longer treats the old session as a valid account.

Protect Routes That Require Login

The following decorator redirects guests to the login page:

# File: auth_helpers.py
from functools import wraps
from flask import g, redirect, url_for


def login_required(view):
    @wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for("auth.login"))
        return view(**kwargs)

    return wrapped_view

Place it below the route decorator:

# File: routes/notes.py
from auth_helpers import login_required


@notes_bp.get("")
@login_required
def index():
    return "Notes for the signed-in user"

Decorator order matters. Flask registers the function returned by login_required, so the check runs every time the route is requested.

Where This Example Stops

The flow above demonstrates password hashing, sessions, a current user, protected routes, and logout. Production still needs additional work: load a random SECRET_KEY from the environment, use HTTPS and secure cookies, add CSRF protection, rate-limit login attempts, and provide a password-reset process.

While testing it, make sure plaintext passwords never reach logs or the database. Try opening /notes before login, after login, and after logout. Those three states show exactly how the session changes later requests.

Keep reading within the same topic.

Don't Miss Out

Get the latest tech articles, tips, and insights delivered to your inbox.