How to Set Up Secure User Authentication in Flask: A Comprehensive Guide
User authentication is a critical component of nearly every web application. Whether you‘re building an e-commerce site, a social network, or an enterprise system, you need a way to securely manage user accounts and control access to sensitive data and functionality.
While there are many different approaches to authentication, such as OAuth, OpenID, and SAML, implementing your own custom solution using a framework like Flask is still a common and viable option, especially for smaller applications or those with unique requirements.
In this comprehensive guide, we‘ll walk through how to set up basic password-based authentication in a Python web application using the Flask framework. We‘ll cover everything you need to know, from hashing user passwords and managing sessions to handling common security vulnerabilities and scaling your authentication system.
Understanding Authentication Fundamentals
Before we dive into the technical details, let‘s review some key concepts and terminology related to authentication.
At its core, authentication is the process of verifying the identity of a user or client. It answers the question, "Who are you?" This is distinct from authorization, which determines what actions an authenticated user is allowed to perform.
There are three main factors that can be used for authentication:
- Something you know (e.g. password, PIN)
- Something you have (e.g. mobile phone, hardware token)
- Something you are (e.g. fingerprint, facial recognition)
Most authentication systems rely on one or two of these factors. The more factors used, the stronger the authentication, but also the more friction for users. Balancing security and usability is an ongoing challenge.
Some common authentication schemes include:
-
Basic Auth: Users provide a username and password with each request. Credentials are sent in plain text, so SSL/TLS encryption is essential. Easy to implement but not very secure.
-
Session-based: Upon login, a session token is stored on the server and sent to the client as a cookie. Subsequent requests include the token to authenticate. Stateful on the server side.
-
Token-based (e.g. JWT): Similar to session-based but tokens are stateless and self-contained, with authentication info encoded directly in the token. Tokens must be signed to prevent tampering.
-
Passwordless: Instead of passwords, users authenticate with a magic link, one-time code, or biometrics. Reduces friction and avoids issues with weak or reused passwords.
According to the 2020 Verizon Data Breach Investigations Report, credential theft and brute force attacks were among the top threat actions for web application breaches. Properly implemented authentication can help mitigate these risks.
Implementation in Flask
Now that we have a good understanding of the fundamentals, let‘s see how to implement secure authentication in a Flask app. We‘ll use the Flask-Login extension for session management, Flask-Bcrypt for password hashing, and Flask-WTF for login and registration forms.
Project Setup
First, create a new project directory and set up a virtual environment:
$ mkdir flask-auth
$ cd flask-auth
$ python3 -m venv env
$ . env/bin/activate
Next, install the required dependencies:
(env) $ pip install Flask Flask-SQLAlchemy Flask-Migrate Flask-Login Flask-Bcrypt Flask-WTF
Create the basic project structure:
flask-auth/
app/
__init__.py
auth/
__init__.py
views.py
forms.py
models.py
routes.py
templates/
base.html
index.html
login.html
register.html
config.py
requirements.txt
Configuration
Add a config.py
file to hold your app settings:
import os
class Config:
SECRET_KEY = os.environ.get(‘SECRET_KEY‘) or ‘my_secret_key‘
SQLALCHEMY_DATABASE_URI = os.environ.get(‘DATABASE_URL‘) or \
‘sqlite:///app.db‘
SQLALCHEMY_TRACK_MODIFICATIONS = False
The SECRET_KEY
is used to sign cookies and should be kept secret. The SQLALCHEMY_DATABASE_URI
specifies the database connection. Here we use SQLite for simplicity but in production you‘d want to use a more robust database like PostgreSQL or MySQL.
Models
Next, define a User
model to represent your user accounts. In models.py
:
from datetime import datetime
from app import db, login_manager
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f‘<User {self.email}>‘
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
The User
model extends UserMixin
from Flask-Login, which provides default implementations for methods like is_authenticated
, is_active
, get_id()
, etc.
The set_password
and check_password
methods use Werkzeug‘s password hashing and checking functions. Storing passwords in plain text is never secure!
The @login_manager.user_loader
tells Flask-Login how to load a user from the database given their ID.
Forms
Now let‘s define our login and registration forms using Flask-WTF. In auth/forms.py
:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User
class LoginForm(FlaskForm):
email = StringField(‘Email‘, validators=[DataRequired(), Email()])
password = PasswordField(‘Password‘, validators=[DataRequired()])
remember_me = BooleanField(‘Remember Me‘)
submit = SubmitField(‘Sign In‘)
class RegistrationForm(FlaskForm):
email = StringField(‘Email‘, validators=[DataRequired(), Email()])
password = PasswordField(‘Password‘, validators=[DataRequired()])
password2 = PasswordField(
‘Repeat Password‘, validators=[DataRequired(), EqualTo(‘password‘)])
submit = SubmitField(‘Register‘)
The LoginForm
has fields for email, password, a "remember me" checkbox, and submit button.
The RegistrationForm
adds a password confirmation field. The EqualTo
validator ensures the two password fields match.
Views
With our models and forms ready, we can implement the views to handle login, logout, and registration.
In auth/views.py
:
from flask import render_template, url_for, redirect, request, flash
from flask_login import login_user, logout_user, login_required, current_user
from app import db
from app.auth import auth_bp
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User
@auth_bp.route(‘/login‘, methods=[‘GET‘, ‘POST‘])
def login():
if current_user.is_authenticated:
return redirect(url_for(‘main.index‘))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None or not user.check_password(form.password.data):
flash(‘Invalid email or password‘)
return redirect(url_for(‘auth.login‘))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get(‘next‘)
return redirect(next_page) if next_page else redirect(url_for(‘main.index‘))
return render_template(‘auth/login.html‘, form=form)
@auth_bp.route(‘/logout‘)
@login_required
def logout():
logout_user()
flash(‘You have been logged out.‘)
return redirect(url_for(‘main.index‘))
@auth_bp.route(‘/register‘, methods=[‘GET‘, ‘POST‘])
def register():
if current_user.is_authenticated:
return redirect(url_for(‘main.index‘))
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash(‘You are now registered. Please log in.‘)
return redirect(url_for(‘auth.login‘))
return render_template(‘auth/register.html‘, form=form)
The login
view checks if a user is already logged in and redirects if so. It then validates the submitted LoginForm
. If valid, it looks up the user by email, verifies the password using check_password
, logs in the user with login_user
, and redirects.
The logout
view simply calls logout_user
and redirects. The @login_required
decorator ensures only authenticated users can access this view.
The register
view is similar to login, except it creates a new User
, sets the hashed password, and saves it to the database before redirecting to login.
Protecting Routes
To require authentication for certain views, use the @login_required
decorator:
from flask_login import login_required
from app.auth import auth_bp
@auth_bp.route(‘/protected‘)
@login_required
def protected():
return ‘This is a protected page. Only logged in users can see it.‘
Unauthenticated users who try to access this route will be redirected to the login page.
Going Further
While this covers the basics of authentication in Flask, there‘s much more you can do to harden your system:
-
Use HTTPS everywhere. Never send credentials or tokens over unencrypted connections.
-
Implement password strength requirements and throttle failed login attempts to prevent brute forcing.
-
Add user email confirmation and secure password reset flows.
-
Support multi-factor authentication for increased security, especially for admin accounts.
-
Use JWT or OAuth2 instead of sessions for stateless, scalable authentication.
-
Monitor and alert on authentication-related events and metrics to detect suspicious activity.
Flask extensions like Flask-Security, Flask-Praetorian, and Flask-JWT can help with many of these features.
Conclusion
Authentication is a complex but critical part of most web applications. Getting it right requires careful design and attention to security best practices.
By leveraging Flask and its ecosystem of extensions, we can implement secure authentication without having to build everything from scratch.
However, authentication is only one part of application security. You also need to properly handle authorization, encrypt sensitive data, validate user inputs, and much more.
Ultimately, security is a never-ending process that requires constant vigilance and staying up to date with the latest threats and technologies. As software developers, it‘s our responsibility to do everything we can to protect our users and their data.