Building a secure user authentication system is the cornerstone of any modern web application, and Flask makes it surprisingly straightforward. In this guide you’ll learn how to create a full‑featured authentication flow—from registration and login to protected routes and password hashing—using pure Python, Flask extensions, and best‑in‑class security practices. Whether you’re a beginner looking for a step‑by‑step tutorial or an experienced developer polishing a production‑ready solution, this article covers everything you need to know to implement a robust Flask user authentication system.
Why Flask Is Ideal for Custom Authentication
Flask is a lightweight micro‑framework that gives you full control over the components you add. This flexibility means you can start with a simple username/password login and later extend it with OAuth, JWT, or multi‑factor authentication without rewriting the core logic. Moreover, Flask’s extensive ecosystem—including Flask‑Login, Flask‑WTF, and Flask‑Bcrypt—provides battle‑tested building blocks that keep your code clean and secure.
Project Setup: Getting the Basics Right
1. Create a virtual environment
python -m venv venv
source venv/bin/activate # On Windows use `venv\Scripts\activate`
2. Install required packages
pip install Flask Flask-Login Flask-WTF Flask-Bcrypt Flask-SQLAlchemy
3. Directory layout
app.py– main application entry pointmodels.py– database modelsforms.py– WTForms definitionstemplates/– HTML templates (login.html, register.html, dashboard.html)static/– CSS and JavaScript assets
Defining the User Model with SQLAlchemy
The user model stores essential information such as the username, email, and a securely hashed password. Using Flask‑Bcrypt ensures passwords are never saved in plain text.
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
db = SQLAlchemy()
bcrypt = Bcrypt()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), unique=True, nullable=False)
email = db.Column(db.String(150), unique=True, nullable=False)
password_hash = db.Column(db.String(60), nullable=False)
def set_password(self, password):
self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
def check_password(self, password):
return bcrypt.check_password_hash(self.password_hash, password)
# Required by Flask-Login
def get_id(self):
return str(self.id)
Integrating Flask‑Login for Session Management
Flask‑Login handles user session tracking, “remember me” functionality, and protects routes with a simple decorator. First, initialize the extension in app.py:
from flask import Flask
from flask_login import LoginManager
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your‑strong‑secret‑key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db.init_app(app)
bcrypt.init_app(app)
login_manager = LoginManager()
login_manager.login_view = 'login' # Redirect unauthenticated users
login_manager.login_message_category = 'info'
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
Creating Registration and Login Forms with Flask‑WTF
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from models import User
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=20)])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('Confirm Password',
validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Sign Up')
def validate_username(self, username):
if User.query.filter_by(username=username.data).first():
raise ValidationError('That username is already taken.')
def validate_email(self, email):
if User.query.filter_by(email=email.data).first():
raise ValidationError('An account with this email already exists.')
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
remember = BooleanField('Remember Me')
submit = SubmitField('Login')
Registration Route: Storing a Secure Password
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, current_user
from models import User, db
from forms import RegistrationForm
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data,
email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Your account has been created! You can now log in.', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
Login Route: Verifying Credentials
from flask_login import login_user, logout_user, login_required
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember.data)
next_page = request.args.get('next')
flash('Logged in successfully.', 'success')
return redirect(next_page) if next_page else redirect(url_for('dashboard'))
else:
flash('Login failed. Check email and password.', 'danger')
return render_template('login.html', form=form)
Protecting Views with the @login_required Decorator
Any route that should be accessible only to authenticated users can be wrapped with @login_required. For example, a simple dashboard:
@app.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html')
Logout Endpoint
@app.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('login'))
Optional: JSON Web Tokens (JWT) for API Authentication
If you need token‑based authentication for a RESTful API, PyJWT or Flask-JWT-Extended can be added without disturbing the session‑based flow.
- Install the extension:
pip install Flask-JWT-Extended - Configure a secret key and token expiration.
- Create login endpoint that returns
access_tokenandrefresh_token. - Protect API routes with
@jwt_required().
Using JWT keeps your API stateless and works well with mobile clients or single‑page applications.
Security Best Practices You Should Never Skip
- Use HTTPS in production to encrypt credentials in transit.
- Store only password hashes—never raw passwords. Bcrypt with a work factor of at least 12 is recommended.
- Implement rate limiting (e.g., Flask‑Limiter) to mitigate brute‑force attacks.
- Validate and sanitize user input using WTForms validators to prevent injection attacks.
- Enable CSRF protection—Flask‑WTF does this automatically for form submissions.
- Set secure cookie flags (
Secure,HttpOnly,SameSite) via Flask’s session configuration. - Use a strong secret key and rotate it periodically.
Testing Your Authentication Flow
Before deploying, run a quick sanity check:
- Register a new user and verify the entry appears in the database with a hashed password.
- Attempt to log in with correct and incorrect credentials to ensure flash messages behave as expected.
- Navigate directly to
/dashboardwhile logged out—Flask‑Login should redirect you to the login page. - Use tools like Postman or curl to test JWT endpoints if you implemented them.
Deploying to Production
When you’re ready to go live, follow these deployment tips:
- Run the app behind a WSGI server such as Gunicorn or uWSGI.
- Set
SESSION_COOKIE_SECURE = True
Leave a Reply