User Registration and Login Flow

Build a user registration and login flow in Python web development — hands-on steps, troubleshooting, and what to study next.

Focus: build a user registration and login flow

Sponsored

You’re building a web app, and everything works locally — until you need real users to sign up and log in. That’s when the panic hits: where do you store passwords? How do you keep sessions secure? Why does your login keep failing with a 500 error? Every web developer faces this wall, and the difference between a hobby project and a production-ready app often comes down to how well you build a user registration and login flow. In this lesson, you’ll learn the core concepts, implement a complete flow with Flask and SQLite, and walk away knowing exactly how to handle the edge cases that break most beginners.

The problem this lesson solves

Without a solid registration and login flow, your web app is either unusable (no way to personalize data) or a security nightmare (passwords stored as plain text, sessions that anyone can forge). You need to answer four questions:

  1. How do you validate a user’s identity?
  2. How do you keep that identity safe between requests?
  3. How do you store passwords without leaking them?
  4. How do you protect users from common attacks like session hijacking?

A naive implementation that just checks a username and password in a database list will work for a demo, but it fails the moment you deploy — because attackers can intercept, guess, or steal credentials. The real problem is not just writing code, but writing code that is secure by design.

Core concept / mental model

Think of a user registration and login flow as a digital handshake with a membership card.

  • Registration is the initial enrollment: the user gives you their details (username, email, password), and you verify they are human (optional CAPTCHA), then store a credential — not the password itself, but a one-way hash.
  • Login is the verification: the user presents their card (username + password), you check the hash, and if it matches, you issue a session token (like a temporary membership card) that the browser stores in a cookie.
  • Session management is the expiration and renewal: you set a timeout so the card expires, and you include security flags to prevent theft.

A key mental model is the hash function as a one-way door: you can go from password to hash, but you can never go back. Even if your database leaks, the attacker only gets hashes, which are computationally infeasible to reverse if you use a strong algorithm like bcrypt or Argon2.

Definitions you’ll need:

  • Password hash — output of a one-way function with salt.
  • Salt — random data added to the password before hashing to prevent rainbow table attacks.
  • Session — server-side record that links a user to a session ID.
  • Cookie — client-side storage for the session ID.
  • CSRF — Cross-Site Request Forgery, an attack that forces a user to perform unwanted actions.

How it works step by step

Here’s the logical flow you’ll implement, broken into causes and effects:

Registration flow

  1. User submits a form with username, email, and password.
  2. Server validates input — checks that the username is not taken, email is valid, and password meets minimum length.
  3. Password is hashed with a salt using a library like werkzeug.security (Flask’s built-in) or passlib.
  4. User record is created in the database with the hash and other profile data.
  5. Optional email verification — send a confirmation link to activate the account.
  6. Redirect to login page with a success message.

Login flow

  1. User submits credentials.
  2. Server fetches user by username (or email).
  3. Hash comparison — hash the submitted password and compare with stored hash.
  4. If match, create a session and set a cookie with a random session ID.
  5. If no match, return an error (but don’t reveal whether the username or password was wrong — use a generic message).
  6. On logout, delete the session and clear the cookie.

Why this order? Because each step builds on the previous: you can’t verify a login without a stored hash, and you can’t have a session without a successful verification. The cause is valid credentials, the effect is a session.

Hands-on walkthrough

Let’s build a minimal but complete registration and login flow with Flask and SQLite. We’ll use werkzeug.security for hashing (it’s already a dependency of Flask) and Flask’s session management.

Setup and models

First, create a virtual environment and install Flask:

python -m venv venv
source venv/bin/activate  # on Windows: venv\Scripts\activate
pip install flask

Now create app.py and set up the app and database:

from flask import Flask, render_template, request, redirect, url_for, session, flash
from werkzeug.security import generate_password_hash, check_password_hash
import sqlite3
from functools import wraps

app = Flask(__name__)
app.secret_key = 'your-secret-key-change-in-production'  # used to sign session cookies

# Database helper
def get_db():
    conn = sqlite3.connect('users.db')
    conn.row_factory = sqlite3.Row
    return conn

# Create table on startup
with get_db() as db:
    db.execute('''
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            username TEXT UNIQUE NOT NULL,
            email TEXT UNIQUE NOT NULL,
            password_hash TEXT NOT NULL
        )
    ''')

Registration route

Here’s the registration handler with validation and hashing:

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

        # Basic validation
        if not username or not email or not password:
            flash('All fields are required.')
            return redirect(url_for('register'))
        if len(password) < 8:
            flash('Password must be at least 8 characters long.')
            return redirect(url_for('register'))

        # Check if username or email already exists
        with get_db() as db:
            if db.execute('SELECT id FROM users WHERE username = ?', (username,)).fetchone():
                flash('Username already taken.')
                return redirect(url_for('register'))
            if db.execute('SELECT id FROM users WHERE email = ?', (email,)).fetchone():
                flash('Email already registered.')
                return redirect(url_for('register'))

            # Hash the password and store the user
            password_hash = generate_password_hash(password)
            db.execute('INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)',
                       (username, email, password_hash))

        flash('Registration successful! Please log in.')
        return redirect(url_for('login'))
    return render_template('register.html')

Login and logout routes

Now the login route with session management, plus a decorator to protect pages:

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

        with get_db() as db:
            user = db.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()

        if user and check_password_hash(user['password_hash'], password):
            # Create session
            session.clear()
            session['user_id'] = user['id']
            session['username'] = user['username']
            flash('Logged in successfully.')
            return redirect(url_for('dashboard'))
        else:
            flash('Invalid username or password.')  # generic message for security
            return redirect(url_for('login'))
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.clear()
    flash('You have been logged out.')
    return redirect(url_for('login'))

# Decorator to require login
def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            flash('Please log in to access this page.')
            return redirect(url_for('login'))
        return f(*args, **kwargs)
    return decorated_function

@app.route('/dashboard')
@login_required
def dashboard():
    return f'Hello, {session["username"]}! <a href="{url_for("logout")}">Logout</a>'

Expected output

When you run flask run and navigate to /register, fill the form, you’ll see a success message and be redirected to /login. After logging in, you’ll land on /dashboard with your username. If you try to access /dashboard without logging in, you’ll be redirected to /login with an error.

Pro tip: Always use session.clear() before setting new session data to prevent session fixation attacks.

Compare options / when to choose what

You have several choices for implementing authentication in Python web development. Here’s a comparison:

Option Effort Security Features When to choose
DIY with Flask session Low Good (if done right) Minimal Learning, simple apps, internal tools
Flask-Login Medium Good Session management, user loading Classic Flask apps, when you need user sessions
Flask-Security High Excellent Roles, permissions, token auth, email confirm Production apps with complex needs
Auth0 / OAuth Low (integration) Excellent (external) Social login, MFA, SSO When you want to outsource auth, enterprise apps
JWT-based REST API Medium Good (if done right) Stateless, mobile API Single-page apps, mobile backends

For most applications, starting with your own implementation (as above) is a great way to learn the fundamentals. Then, when you need more features (e.g., “forgot password”, email verification), consider switching to a library like Flask-Login or Flask-Security. For production, never roll your own crypto — use established libraries.

Variations you might encounter:

  • Email instead of username — many apps use email as the login identifier.
  • Password reset flow — add a route that sends a token to the user’s email.
  • Two-factor authentication — integrate TOTP for extra security.

Troubleshooting & edge cases

Here are common issues you’ll hit and how to solve them:

“Invalid username or password” even though inputs are correct

  • Check the database — is the password hash stored correctly? Use sqlite3 to inspect.
  • Whitespace — strip input before hashing. Compare with the value you submitted.
  • Case sensitivity — usernames might be case-sensitive; decide on LOWER(username).

Session not persisting across requests

  • Secret key — Flask requires a secret_key to sign cookies; without it, sessions fail.
  • Browser cookies — ensure your browser allows cookies for localhost.
  • Secure flag — if you set SESSION_COOKIE_SECURE = True over HTTP, cookies won’t be set. Use it only over HTTPS.

Registration fails with “UNIQUE constraint failed”

  • Race condition — two requests with the same username. Add exception handling with try/except sqlite3.IntegrityError.
  • Case sensitivity — use UPPER(username) in the database for uniqueness.

Passwords stored as plain text (security bug)

  • Never store plain text. Use generate_password_hash and check_password_hash.
  • If you already did — migrate by re-hashing on next login.

“Remember me” not working

  • Implement persistent sessions by storing a random token in a cookie and a database. Flask session is browser-session only.

What you learned & what's next

You now understand how to build a user registration and login flow: you learned why password hashing and session management are critical, and you implemented a complete flow with Flask and SQLite. You can now:

  • Explain the core idea behind registration and login.
  • Complete a practical exercise to implement the flow.
  • Identify and fix common pitfalls like session fixation and insecure storage.

In the next lesson, you’ll learn how to integrate Flask-Login to extend this flow with features like “remember me” and user roles, or dive into REST API authentication with JWT for a JSON-based backend. Either way, you have a solid foundation to build secure, real-world web applications.

Keep practicing — try adding a password reset flow or email verification on your own.

Practice recap

Now try it yourself: extend the code by adding a profile page that displays the user's email, and implement a 'forgot password' feature that sends a reset link to the user's email (use a fake email console for testing). This will solidify your understanding of sessions and token-based flows.

Common mistakes

  • Storing plain-text passwords in the database instead of using a secure hash like bcrypt or Argon2.
  • Not validating input (e.g., stripping whitespace) before hashing, causing unexpected login failures.
  • Using a predictable secret_key or hardcoding it in production, leading to session forgery.
  • Revealing whether the username or password was wrong in the error message, which aids attackers in enumerating users.
  • Forgetting to clear the session before setting new values, allowing session fixation attacks.

Variations

  1. Use email instead of username as the login identifier — change the query to fetch by email and add an email field to the form.
  2. Integrate password reset via email with a time-limited token stored in the database.
  3. Switch to JWT (JSON Web Tokens) for stateless API authentication instead of server-side sessions.

Real-world use cases

  • A band practice tracker web app where members sign up, log in, and view their assigned songs for the week.
  • A small online store where customers create accounts to track their order history and save shipping addresses.
  • A freelance developer building a client portal for a design agency, with restricted access to project files.

Key takeaways

  • A registration/login flow is the bridge between anonymous and authenticated users in any web app.
  • Always hash passwords with a modern algorithm and a salt — never store or transmit plain text.
  • Sessions must be managed securely: use a strong secret key, set cookie flags, and clear before login.
  • Validate all input on the server side and provide generic error messages to avoid user enumeration.
  • Start with a DIY session-based flow to learn fundamentals, then adopt libraries like Flask-Login for production features.
  • Debug common authentication issues systematically: check database state, whitespace, and cookie flags.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.