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
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:
- How do you validate a user’s identity?
- How do you keep that identity safe between requests?
- How do you store passwords without leaking them?
- 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
- User submits a form with username, email, and password.
- Server validates input — checks that the username is not taken, email is valid, and password meets minimum length.
- Password is hashed with a salt using a library like
werkzeug.security(Flask’s built-in) orpasslib. - User record is created in the database with the hash and other profile data.
- Optional email verification — send a confirmation link to activate the account.
- Redirect to login page with a success message.
Login flow
- User submits credentials.
- Server fetches user by username (or email).
- Hash comparison — hash the submitted password and compare with stored hash.
- If match, create a session and set a cookie with a random session ID.
- If no match, return an error (but don’t reveal whether the username or password was wrong — use a generic message).
- 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
sqlite3to 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_keyto sign cookies; without it, sessions fail. - Browser cookies — ensure your browser allows cookies for
localhost. - Secure flag — if you set
SESSION_COOKIE_SECURE = Trueover 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_hashandcheck_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_keyor 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
- Use email instead of username as the login identifier — change the query to fetch by email and add an email field to the form.
- Integrate password reset via email with a time-limited token stored in the database.
- 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.