Flask Authentication and Sessions

Flask sessions, Flask-Login basics, and a complete login-protected route example.

Flask sessions

Flask's session object stores small amounts of data across requests from the same browser, backed by a signed (not encrypted) cookie — the values are readable if decoded, but tampering with them invalidates the signature, so a request with a modified session cookie is rejected rather than trusted.

Python
from flask import Flask, session, redirect, url_for, request

app = Flask(__name__)
app.secret_key = "change-this-to-a-real-secret-in-production"  # required to sign session cookies

@app.route("/login", methods=["POST"])
def login():
    username = request.form["username"]
    session["username"] = username
    return redirect(url_for("dashboard"))

@app.route("/dashboard")
def dashboard():
    if "username" not in session:
        return redirect(url_for("login_form"))
    return f"Welcome, {session['username']}!"

@app.route("/logout")
def logout():
    session.pop("username", None)
    return redirect(url_for("login_form"))

app.secret_key is what signs the session cookie — without it, session[...] raises a RuntimeError the moment you try to use it. Because the cookie itself holds the (signed, not encrypted) data, don't put anything genuinely sensitive in a plain Flask session — a password, a credit card number — only identifiers like a user ID.

Flask-Login basics

Hand-checking "username" in session on every protected view doesn't scale. Flask-Login is the standard extension for this: it manages the logged-in user across requests, provides a @login_required decorator, and integrates with session under the hood so you don't manage it directly.

Bash
pip install flask-login
Python
from flask import Flask
from flask_login import LoginManager, UserMixin

app = Flask(__name__)
app.secret_key = "change-this-to-a-real-secret-in-production"

login_manager = LoginManager(app)
login_manager.login_view = "login_form"   # where to redirect an unauthenticated request

class User(UserMixin):
    def __init__(self, id, username, password_hash):
        self.id = id
        self.username = username
        self.password_hash = password_hash

users_by_id = {"1": User(id="1", username="ada", password_hash="...")}

@login_manager.user_loader
def load_user(user_id):
    return users_by_id.get(user_id)

UserMixin supplies the properties Flask-Login needs from a user object (is_authenticated, is_active, get_id()) so a plain class (or a database model) can plug into the login system with minimal boilerplate. user_loader is the one function Flask-Login calls on every request to reload a User object from the ID it stored in the session.

A complete login-protected route example

Python
from flask import Flask, render_template, request, redirect, url_for
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from werkzeug.security import check_password_hash, generate_password_hash

app = Flask(__name__)
app.secret_key = "change-this-to-a-real-secret-in-production"
login_manager = LoginManager(app)
login_manager.login_view = "login_form"

class User(UserMixin):
    def __init__(self, id, username, password_hash):
        self.id = id
        self.username = username
        self.password_hash = password_hash

users_by_id = {
    "1": User(id="1", username="ada", password_hash=generate_password_hash("s3cret-pass")),
}
users_by_username = {u.username: u for u in users_by_id.values()}

@login_manager.user_loader
def load_user(user_id):
    return users_by_id.get(user_id)

@app.route("/login", methods=["GET", "POST"])
def login_form():
    if request.method == "POST":
        user = users_by_username.get(request.form["username"])
        if user and check_password_hash(user.password_hash, request.form["password"]):
            login_user(user)
            return redirect(url_for("dashboard"))
        return "Invalid credentials", 401
    return render_template("login.html")

@app.route("/dashboard")
@login_required
def dashboard():
    return f"Welcome, {current_user.username}!"

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

login_user(user) records the user's ID in the session (via Flask-Login, not by touching session directly) and marks the request's current_user as authenticated. @login_required on /dashboard and /logout redirects an anonymous visitor to login_view ("login_form") automatically — the view body never even runs for them. current_user is a context-local proxy (the same pattern as Flask's own request) always pointing at the currently logged-in user, or an anonymous placeholder if nobody's logged in. check_password_hash/generate_password_hash (from Werkzeug, already a Flask dependency) hash and verify passwords — never store or compare a plaintext password directly.

Common mistakes

  • Storing a plaintext password (or comparing with ==) instead of hashing it with generate_password_hash/check_password_hash — a compromised user store then directly exposes real passwords.
  • Forgetting app.secret_key — every attempt to read or write session (directly, or via Flask-Login) raises a RuntimeError until it's set.
  • Putting sensitive data directly into session — the cookie is signed against tampering but not encrypted, so anything stored there is readable by anyone who can read the cookie, including the user's own browser.

Interview questions

Q: Is data stored in a Flask session encrypted? No — it's signed, not encrypted. Flask serializes the session dict and signs it with app.secret_key, so any client-side tampering invalidates the signature and the whole session is rejected. But the underlying values are just base64-encoded, not encrypted, so nothing genuinely sensitive (a password, a card number) belongs directly in a session value — only identifiers like a user ID.

Q: What does Flask-Login provide that hand-rolling session-based auth with a plain dict doesn't? It standardizes the whole login lifecycle — a current_user proxy available anywhere in the request (mirroring how request works), a @login_required decorator that redirects unauthenticated requests to a configured login view automatically, and a user_loader hook that reloads the right user object from the session on every request. Hand-rolling the same behavior with a raw session["user_id"] dict means re-implementing all of that — the redirect-on-failure logic, the "who is currently logged in" lookup — in every view that needs it.