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_required decorator 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

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.