Flask Login & Signup with Sessions and Dashboards

Authentication is the backbone of every real web app. In this post we build it from scratch using only Flask and the standard library — no flask-login, no JWT, just good old sessions.

The User Table

CREATE TABLE users (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    username   TEXT    NOT NULL UNIQUE,
    email      TEXT    NOT NULL UNIQUE,
    password   TEXT    NOT NULL,
    role       TEXT    DEFAULT 'user',
    created_at TEXT    DEFAULT (datetime('now'))
);

Secure Password Hashing

Never store plain text passwords. Use PBKDF2:

import hashlib, os

def hash_password(password: str) -> str:
    salt = os.urandom(32)
    key  = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 310_000)
    return salt.hex() + ':' + key.hex()

def verify_password(stored: str, candidate: str) -> bool:
    salt_hex, key_hex = stored.split(':')
    salt = bytes.fromhex(salt_hex)
    key  = hashlib.pbkdf2_hmac('sha256', candidate.encode(), salt, 310_000)
    return key.hex() == key_hex

Signup Route

@app.route('/signup', methods=['GET', 'POST'])
def signup():
    if request.method == 'POST':
        username = request.form['username'].strip()
        email    = request.form['email'].strip().lower()
        password = request.form['password']

        if len(password) < 8:
            flash('Password must be at least 8 characters.', 'error')
            return redirect(url_for('signup'))

        db = get_db()
        if db.execute('SELECT id FROM users WHERE email=?', (email,)).fetchone():
            flash('Email already registered.', 'error')
            return redirect(url_for('signup'))

        db.execute(
            'INSERT INTO users (username, email, password) VALUES (?,?,?)',
            (username, email, hash_password(password))
        )
        db.commit()
        flash('Account created! Please log in.', 'success')
        return redirect(url_for('login'))

    return render_template('signup.html')

Login & Session

from flask import session

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        email    = request.form['email'].strip().lower()
        password = request.form['password']

        user = get_db().execute(
            'SELECT * FROM users WHERE email=?', (email,)
        ).fetchone()

        if user and verify_password(user['password'], password):
            session.permanent = True
            session['user_id']   = user['id']
            session['username']  = user['username']
            session['user_role'] = user['role']
            return redirect(url_for('dashboard'))

        flash('Invalid credentials.', 'error')

    return render_template('login.html')

@app.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('login'))

Protecting Routes

from functools import wraps

def login_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if 'user_id' not in session:
            return redirect(url_for('login'))
        return f(*args, **kwargs)
    return decorated

def admin_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if session.get('user_role') != 'admin':
            abort(403)
        return f(*args, **kwargs)
    return decorated

@app.route('/dashboard')
@login_required
def dashboard():
    user = get_db().execute(
        'SELECT * FROM users WHERE id=?', (session['user_id'],)
    ).fetchone()
    return render_template('dashboard.html', user=user)

The Dashboard Template

{% extends "base.html" %}
{% block content %}
<h1>Welcome back, {{ user.username }}!</h1>
<p>Role: <strong>{{ user.role }}</strong></p>
<a href="{{ url_for('logout') }}">Log out</a>
{% endblock %}

That's a complete, production-ready auth system in under 80 lines of Python.