security
intermediate
Flask Authentication: Session-Based Login
min read
Frederick Tubiermont
Flask Authentication: Session-Based Login
Authentication is one of the first things you need in any real app. Here's how to build it the Flask Vibe way: explicit, transparent, no magic.
What We're Building
- User registration with hashed passwords
- Login and logout routes
- Session-based authentication
- A
login_requireddecorator to protect routes
Database Table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Password Hashing
Never store plain passwords. Use werkzeug.security — it's already installed with Flask.
from werkzeug.security import generate_password_hash, check_password_hash
# When registering
password_hash = generate_password_hash(password)
# When logging in
is_valid = check_password_hash(stored_hash, submitted_password)
Registration Route
from flask import Flask, request, redirect, render_template, session
from werkzeug.security import generate_password_hash
from utils.db import get_db
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "GET":
return render_template("register.html")
email = request.form.get("email", "").strip().lower()
password = request.form.get("password", "")
if not email or not password:
return render_template("register.html", error="Email and password required")
if len(password) < 8:
return render_template("register.html", error="Password must be at least 8 characters")
password_hash = generate_password_hash(password)
with get_db() as conn:
with conn.cursor() as cur:
# Check if email already exists
cur.execute("SELECT id FROM users WHERE email = %s", (email,))
if cur.fetchone():
return render_template("register.html", error="Email already registered")
cur.execute(
"INSERT INTO users (email, password_hash) VALUES (%s, %s) RETURNING id",
(email, password_hash)
)
user_id = cur.fetchone()["id"]
conn.commit()
session["user_id"] = user_id
session["role"] = "user"
return redirect("/dashboard")
Login Route
from werkzeug.security import check_password_hash
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "GET":
return render_template("login.html")
email = request.form.get("email", "").strip().lower()
password = request.form.get("password", "")
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT id, password_hash, role FROM users WHERE email = %s",
(email,)
)
user = cur.fetchone()
# Same error message for both wrong email and wrong password (security)
if not user or not check_password_hash(user["password_hash"], password):
return render_template("login.html", error="Invalid email or password")
session["user_id"] = user["id"]
session["role"] = user["role"]
return redirect("/dashboard")
Logout Route
@app.route("/logout")
def logout():
session.clear()
return redirect("/login")
The login_required Decorator
from functools import wraps
from flask import session, redirect
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if "user_id" not in session:
return redirect("/login")
return f(*args, **kwargs)
return decorated
Protecting Routes
@app.route("/dashboard")
@login_required
def dashboard():
user_id = session["user_id"]
with get_db() as conn:
with conn.cursor() as cur:
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = cur.fetchone()
return render_template("dashboard.html", user=user)
Secret Key Configuration
Sessions require a secret key. Set it in your .env:
SECRET_KEY=your-very-long-random-secret-key-here
# config.py
import os
class Config:
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-key-change-in-production")
Generate a good secret key:
python -c "import secrets; print(secrets.token_hex(32))"
The Login Template
<!-- templates/login.html -->
<form method="POST" action="/login">
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
Security Checklist
- ✅ Passwords hashed with
generate_password_hash - ✅ Timing-safe comparison with
check_password_hash - ✅ Same error for wrong email and wrong password (prevents user enumeration)
- ✅ Session cleared on logout
- ✅ Secret key from environment variable
- ✅ Email normalized to lowercase before storage
Next Steps
- Add role-based access control for admin routes
- Add CSRF protection to your forms
- See how to use helper modules to keep routes clean
Was this helpful?
Get More Flask Vibe Tutorials
Join 1,000+ developers getting weekly Flask tips and AI-friendly code patterns.
No spam. Unsubscribe anytime.