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.
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.
pip install flask-login
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
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 withgenerate_password_hash/check_password_hash— a compromised user store then directly exposes real passwords. - Forgetting
app.secret_key— every attempt to read or writesession(directly, or via Flask-Login) raises aRuntimeErroruntil 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.