Table of contents
User authentication is important for protecting sensitive information and resources from unauthorized access. It helps ensure that only authorized users can access and make changes to data, and helps prevent unauthorized users from gaining access to sensitive information.
There are different methods for implementing user authentication, including password-based authentication, token-based authentication, and so on.
In this tutorial, you will learn how to set up basic user authentication – that is password-based authentication – in your Flask application.
Project Demo
Here’s what the final output will look like:
The link to the GitHub repository is available at the end of the tutorial. Feel free to check it out whenever you're stuck.
Prerequisites
Before you get started with the tutorial, make sure you have the following requirements satisfied:
Working knowledge of Python
Python 3.8+ installed on your system
Basic knowledge of Flask and Flask Blueprints
Get Your Tools Ready
You'll need a few external libraries in this project. Let's learn more about them and install them one by one.
But before we install them, let's create a virtual environment and activate it.
First, start with creating the project directory and navigating to it like this:
mkdir flask-basic-auth
cd flask-basic-auth
We are going to create a virtual environment using venv
. Python now ships with a pre-installed venv
library. So, to create a virtual environment, you can use the below command:
python -m venv env
The above command will create a virtual environment named env. Now, we need to activate the environment using this command:
source env/Scripts/activate
To verify if the environment has been activated or not, you can see (env)
in your terminal. Now, we can install the libraries.
Flask is a simple, easy-to-use microframework for Python that helps you build scalable and secure web applications.
Flask-Login provides user session management for Flask. It handles the common tasks of logging in, logging out, and remembering your users’ sessions over extended periods of time.
Flask-Bcrypt is a Flask extension that provides bcrypt hashing utilities for your application.
Flask-WTF is a simple integration of Flask and WTForms that helps you create forms in Flask.
Flask-Migrate is an extension that handles SQLAlchemy database migrations for Flask applications using Alembic. The database operations are made available through the Flask command-line interface.
Flask-SQLAlchemy is an extension for Flask that adds support for SQLAlchemy to your application. It helps you simplify things using SQLAlchemy with Flask by giving you useful defaults and extra helpers that make it easier to perform common tasks.
Flask-Testing extension provides unit testing utilities for Flask.
Python Decouple helps you use environment variables in your Python project.
To install the above-mentioned libraries, run the following command:
pip install Flask Flask-Login Flask-Bcrypt Flask-WTF FLask-Migrate Flask-SQLAlchemy Flask-Testing python-decouple
This tutorial was verified with Python V3.11, Flask V2.2.2, Flask-Login V0.6.0, Flask-Bcrypt V1.0.1, Flask-WTF V1.0.1, Flask-SQLAlchemy V2.5.1 and, Flask-Testing V0.8.1.
How to Set Up the Project
Let’s start by creating a src
directory:
mkdir src
The first file will be the __init__.py
file for the project:
from decouple import config
from flask import Flask
from flask_bcrypt import Bcrypt
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config.from_object(config("APP_SETTINGS"))
bcrypt = Bcrypt(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# Registering blueprints
from src.accounts.views import accounts_bp
from src.core.views import core_bp
app.register_blueprint(accounts_bp)
app.register_blueprint(core_bp)
In the above script, we created a Flask app called app
. We use the __name__
argument to indicate the app's module or package so that Flask knows where to find other files such as templates. You also set the configuration of the app using an environment variable called APP_SETTINGS
. You'll export it later.
To use Flask-Bcrypt, Flask-SQLAlchemy, and Flask-Migrate in your application, you just need to create objects of the Bcrypt
, SQLAlchemy
and Migrate
classes from the flask_bcrypt
, flask_sqlalchemy
and, flask_migrate
libraries, respectively.
You've also registered blueprints called accounts_bp
and core_bp
in the application. You'll define them later in the tutorial.
In the root directory of the project (that is, outside the src
directory), create a file called config.py
. We'll store the configurations for the project in this file. Within the file, add the following content:
from decouple import config
DATABASE_URI = config("DATABASE_URL")
if DATABASE_URI.startswith("postgres://"):
DATABASE_URI = DATABASE_URI.replace("postgres://", "postgresql://", 1)
class Config(object):
DEBUG = False
TESTING = False
CSRF_ENABLED = True
SECRET_KEY = config("SECRET_KEY", default="guess-me")
SQLALCHEMY_DATABASE_URI = DATABASE_URI
SQLALCHEMY_TRACK_MODIFICATIONS = False
BCRYPT_LOG_ROUNDS = 13
WTF_CSRF_ENABLED = True
DEBUG_TB_ENABLED = False
DEBUG_TB_INTERCEPT_REDIRECTS = False
class DevelopmentConfig(Config):
DEVELOPMENT = True
DEBUG = True
WTF_CSRF_ENABLED = False
DEBUG_TB_ENABLED = True
class TestingConfig(Config):
TESTING = True
DEBUG = True
SQLALCHEMY_DATABASE_URI = "sqlite:///testdb.sqlite"
BCRYPT_LOG_ROUNDS = 1
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
DEBUG = False
DEBUG_TB_ENABLED = False
In the above script, we have created a Config
class and defined various attributes inside that. Also, we have created different child classes (as per different stages of development) that inherit the Config
class.
Notice that we're using a few environment variables like SECRET_KEY
and DATABASE_URL
. Create a file named .env
in the root directory and add the following content there:
export SECRET_KEY=fdkjshfhjsdfdskfdsfdcbsjdkfdsdf
export DEBUG=True
export APP_SETTINGS=config.DevelopmentConfig
export DATABASE_URL=sqlite:///db.sqlite
export FLASK_APP=src
export FLASK_DEBUG=1
Apart from the SECRET_KEY
and DATABASE_URL
, we've also exported APP_SETTINGS
, DEBUG
, FLASK_APP
, and FLASK_DEBUG
.
The APP_SETTINGS
refers to one of the classes we created in the config.py
file. We set it to the current stage of the project.
The value of FLASK_APP
is the name of the package we have created. Since the app is in the development stage, you can set the values of DEBUG
and FLASK_DEBUG
to True
and 1
, respectively.
Run the following command to export all the environment variables from the .env
file:
source .env
Next, you'll create a CLI application of the app so that you can later add custom commands such as test
and create_admin
in order to test the application and create admin, respectively.
Create a manage.py
file in the root directory of the application and add the following code:
from flask.cli import FlaskGroup
from src import app
cli = FlaskGroup(app)
if __name__ == "__main__":
cli()
Now, your basic application is ready. You can run it using the following command:
python manage.py run
How to Create Blueprints for Accounts and Core
As mentioned earlier, you'll use the concepts of blueprints in the project. Let's create two blueprints – accounts_bp
and core_bp
– in this section.
First, create a directory called accounts
like this:
mkdir accounts
cd accounts
Next, add an empty __init__.py
file to covert it into a Python package. Now, create a views.py
file inside the package where you'll store all your routes related to user authentication.
touch __init__.py views.py
Add the following code inside the views.py
file:
from flask import Blueprint
accounts_bp = Blueprint("accounts", __name__)
In the above script, you have created a blueprint called accounts_bp
for the accounts
package.
Similarly, you can create a core
package in the root directory, and add a views.py
file.
mkdir core
cd core
touch __init__.py views.py
Now, add the following code inside the views.py
file:
from flask import Blueprint
core_bp = Blueprint("core", __name__)
Note: If you're new to Flask Blueprints, make sure you go through this tutorial to learn more about how it works.
How to Create a User Model
Let's create a models.py
file inside the accounts
package.
touch src/accounts/models.py
Inside the models.py
file, add the following code:
from datetime import datetime
from src import bcrypt, db
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
created_on = db.Column(db.DateTime, nullable=False)
is_admin = db.Column(db.Boolean, nullable=False, default=False)
def __init__(self, email, password, is_admin=False):
self.email = email
self.password = bcrypt.generate_password_hash(password)
self.created_on = datetime.now()
self.is_admin = is_admin
def __repr__(self):
return f"<email {self.email}>"
In the above code, you created a User
model by inheriting the db.Model
class. The User
model consists of the following fields:
id
: stores the primary key for theusers
tableemail
: stores the email of the userpassword
: stores the hashed password of the usercreated_on
: stores the timestamp when the user was createdis_admin
: stores whether the user is admin or not
In the constructor of the class, you set the fields. Notice the password field where you generate the hash of the password using the bcrypt
object imported from the app.
How to Add Flask-Login
The most important part of Flask-Login is the LoginManager
class that lets your application and Flask-Login work together.
In the src/__init__.py
file, add the following code:
from decouple import config
from flask import Flask
from flask_login import LoginManager # Add this line
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config.from_object(config("APP_SETTINGS"))
login_manager = LoginManager() # Add this line
login_manager.init_app(app) # Add this line
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# Registering blueprints
from src.accounts.views import accounts_bp
from src.core.views import core_bp
app.register_blueprint(accounts_bp)
app.register_blueprint(core_bp)
In the above script, you created and initialized the login manager in your app.
Next, you need to provide a user_loader
callback. This callback is used to reload the user object from the user ID stored in the session. It should take the ID of a user, and return the corresponding user object.
from src.accounts.models import User
@login_manager.user_loader
def load_user(user_id):
return User.query.filter(User.id == int(user_id)).first()
The User
model should implement the following properties and methods:
is_authenticated
: This property returns True if the user is authenticated.is_active
: This property returns True if this is an active user (the account is activated)is_anonymous
: This property returns True if this is an anonymous user (actual users return False).get_id()
: This method returns a string that uniquely identifies this user, and can be used to load the user from theuser_loader
callback.
Now, you don't need to implement these explicitly. Instead, the Flask-Login provides a UserMixin
class that contains the default implementations for all of these properties and methods. You just need to inherit it in the following way:
from datetime import datetime
from flask_login import UserMixin # Add this line
from src import bcrypt, db
class User(UserMixin, db.Model): # Change this line
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
created_on = db.Column(db.DateTime, nullable=False)
is_admin = db.Column(db.Boolean, nullable=False, default=False)
def __init__(self, email, password, is_admin=False):
self.email = email
self.password = bcrypt.generate_password_hash(password)
self.created_on = datetime.now()
self.is_admin = is_admin
def __repr__(self):
return f"<email {self.email}>"
You can also customize the default login process in the src/__init__.py
file.
The name of the login view can be set as LoginManager.login_view
. The value refers to the function name that will handle the login process.
login_manager.login_view = "accounts.login"
To customize the message category, set LoginManager.login_message_category
:
login_manager.login_message_category = "danger"
How to Add Templates and Static Files
Let's create a CSS file called styles.css
inside the src/static
folder:
.error {
color: red;
margin-bottom: 5px;
text-align: center;
}
a {
text-decoration: none;
}
Let's also create the basic templates inside the src/templates
folder. Create a _base.html
file and add the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flask User Management</title>
<!-- meta -->
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- styles -->
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
<link rel="stylesheet" href="{{url_for('static', filename="styles.css")}}">
{% block css %}{% endblock %}
</head>
<body>
{% include "navigation.html" %}
<div class="container">
<br>
<!-- messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="row">
<div class="col-md-4"></div>
<div class="col-md-4">
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{message}}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
<div class="col-md-4"></div>
</div>
{% endif %}
{% endwith %}
<!-- child template -->
{% block content %}{% endblock %}
</div>
<!-- scripts -->
<script src="https://code.jquery.com/jquery-3.6.1.min.js" type="text/javascript"></script>
<!-- JavaScript Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
{% block js %}{% endblock %}
</body>
</html>
The _base.html
is the parent HTML file that will be inherited by the other templates. We have added Bootstrap 5 support in the above file. We are also making use of Flask Flashes to show Bootstrap alerts in the app.
Let's also create a navigation.html
file that contains the navbar of the app:
<!-- Navigation -->
<header class="p-3 text-bg-dark">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><a href="{{ url_for('core.home') }}" class="nav-link px-2 text-secondary">Home</a></li>
</ul>
<div class="text-end">
{% if current_user.is_authenticated %}
<a href="{{ url_for('accounts.logout') }}"><button type="button" class="btn btn-danger me-2">Logout</button></a>
{% else %}
<a href="{{ url_for('accounts.login') }}"><button type="button" class="btn btn-outline-light me-2">Login</button></a>
<a href="{{ url_for('accounts.register') }}"><button type="button" class="btn btn-success">Sign up</button></a>
{% endif %}
</div>
</div>
</div>
</header>
Note that we have not yet created the views used above.
How to Create the Homepage
In this section, you'll first create a view function for the homepage inside the core/views.py
file. Add the following code there:
from flask import Blueprint, render_template
from flask_login import login_required
core_bp = Blueprint("core", __name__)
@core_bp.route("/")
@login_required
def home():
return render_template("core/index.html")
Notice that we have used the blueprint to add the route. We also added a @login_required
middleware to prevent access from unauthenticated users.
Next, let's create an index.html
file inside the templates/core
folder, and add the following code:
{% extends "_base.html" %}
{% block content %}
<h1 class="text-center">Welcome {{current_user.email}}!</h1>
{% endblock %}
The HTML page will just have a welcome message for authenticated users.
How to Implement User Registration
First of all, we'll create a registration form using Flask-WTF. Create a forms.py
file inside the accounts
package and add the following code:
from flask_wtf import FlaskForm
from wtforms import EmailField, PasswordField
from wtforms.validators import DataRequired, Email, EqualTo, Length
from src.accounts.models import User
class RegisterForm(FlaskForm):
email = EmailField(
"Email", validators=[DataRequired(), Email(message=None), Length(min=6, max=40)]
)
password = PasswordField(
"Password", validators=[DataRequired(), Length(min=6, max=25)]
)
confirm = PasswordField(
"Repeat password",
validators=[
DataRequired(),
EqualTo("password", message="Passwords must match."),
],
)
def validate(self):
initial_validation = super(RegisterForm, self).validate()
if not initial_validation:
return False
user = User.query.filter_by(email=self.email.data).first()
if user:
self.email.errors.append("Email already registered")
return False
if self.password.data != self.confirm.data:
self.password.errors.append("Passwords must match")
return False
return True
The RegisterForm
extends the FlaskForm
class and contains three fields – email
, password
, and confirm
. We have added different validators such as DataRequired
, Length
, Email
, and EqualTo
to the respective fields.
We also defined a validate()
method that is automatically called when the form is submitted.
Inside the method, we first perform the initial validation provided by FlaskForm. If that is successful, we perform our custom validation such as checking whether the user is already registered and matching the password with the confirmed password. If there are any errors, we append the error message in the respective fields.
Let's use this form in the views.py
to create a function to handle the registration process.
from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_login import login_user
from src import db
from src.accounts.models import User
from .forms import RegisterForm
@accounts_bp.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
flash("You are already registered.", "info")
return redirect(url_for("core.home"))
form = RegisterForm(request.form)
if form.validate_on_submit():
user = User(email=form.email.data, password=form.password.data)
db.session.add(user)
db.session.commit()
login_user(user)
flash("You registered and are now logged in. Welcome!", "success")
return redirect(url_for("core.home"))
return render_template("accounts/register.html", form=form)
In the above function, notice that we're using the blueprint to add the route for the function. Initially, we check if a user is already authenticated using the is_authenticated
property. If it is, we redirect it to the homepage with a message.
If no user is authenticated, we first create an instance of the RegisterForm
class. If the request method is GET, we render an HTML file with the form. Otherwise, we check if the form has valid inputs using the validate_on_submit()
method.
If the inputs are valid, we create an instance of the User
class with the email and password provided by the user and add it to the database.
Next, we log in the user using the login_user()
method that accepts the user
object. We also flash a success message and redirect the user to the homepage.
Now, let's use this form inside the HTML file. Create a accounts
directory inside the templates
folder and add a new file called register.html
inside it. Add the following code:
{% extends "_base.html" %}
{% block content %}
<div class="row">
<div class="col-md-4"></div>
<div class="col-md-4">
<main class="form-signin w-100 m-auto">
<form role="form" method="post" action="">
{{ form.csrf_token }}
<h1 class="h3 mb-3 fw-normal text-center">Please register</h1>
<div class="form-floating">
{{ form.email(placeholder="email", class="form-control mb-2") }}
{{ form.email.label }}
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endfor %}
{% endif %}
</div>
<div class="form-floating">
{{ form.password(placeholder="password", class="form-control mb-2") }}
{{ form.password.label }}
{% if form.password.errors %}
{% for error in form.password.errors %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endfor %}
{% endif %}
</div>
<div class="form-floating">
{{ form.confirm(placeholder="Confirm Password", class="form-control mb-2") }}
{{ form.confirm.label }}
{% if form.confirm.errors %}
{% for error in form.confirm.errors %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endfor %}
{% endif %}
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Sign up</button>
<p class="text-center mt-3">Already registered? <a href="{{ url_for('accounts.login') }}">Login now</a></p>
</form>
</main>
</div>
<div class="col-md-4"></div>
</div>
{% endblock %}
In the above code, we created an HTML form where we make use of the form
instance that contains the form fields with their labels and errors. We have used a accounts.login
view function that doesn't exist yet.
How to Implement User Login and Logout
First, let's create a login form in the accounts/forms.py
file:
class LoginForm(FlaskForm):
email = EmailField("Email", validators=[DataRequired(), Email()])
password = PasswordField("Password", validators=[DataRequired()])
The form is similar to the registration form but it has only two fields – email
and password
.
Next, let's create a view function to handle the login process inside the accounts/views.py
file:
from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_login import login_user
from src import bcrypt, db
from src.accounts.models import User
from .forms import LoginForm, RegisterForm
@accounts_bp.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
flash("You are already logged in.", "info")
return redirect(url_for("core.home"))
form = LoginForm(request.form)
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and bcrypt.check_password_hash(user.password, request.form["password"]):
login_user(user)
return redirect(url_for("core.home"))
else:
flash("Invalid email and/or password.", "danger")
return render_template("accounts/login.html", form=form)
return render_template("accounts/login.html", form=form)
Similar to the registration view function, we first check if a user is already authenticated using the is_authenticated
property. If it is, we redirect it to the homepage with a message.
If not authenticated, we create an instance of the login form. If the request method is GET, we simply render a login.html
file with the form. Otherwise, the form is validated.
During the validation, we use the check_password_hash
method from the Flask-Bcrypt
library to match the hashed passwords. If the passwords match, we log in the user using the login_user()
method and redirect to the homepage. Otherwise, we flash an error message and render the same HTML page.
Now, let's create a login.html
file inside the templates/accounts
folder:
{% extends "_base.html" %}
{% block content %}
<div class="row">
<div class="col-md-4"></div>
<div class="col-md-4">
<main class="form-signin w-100 m-auto">
<form role="form" method="post" action="">
{{ form.csrf_token }}
<h1 class="h3 mb-3 fw-normal text-center">Please sign in</h1>
<div class="form-floating">
{{ form.email(placeholder="email", class="form-control mb-2") }}
{{ form.email.label }}
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endfor %}
{% endif %}
</div>
<div class="form-floating">
{{ form.password(placeholder="password", class="form-control mb-2") }}
{{ form.password.label }}
{% if form.password.errors %}
{% for error in form.password.errors %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endfor %}
{% endif %}
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
<p class="text-center mt-3">New User? <a href="{{ url_for('accounts.register') }}">Register now</a></p>
</form>
</main>
</div>
<div class="col-md-4"></div>
</div>
{% endblock %}
The login form is similar to the registration form but with just two fields for the email and password.
Logging out the user is a very simple process. You just need to create a view function for it inside the accounts/views.py
file:
from flask_login import login_required, login_user, logout_user
@accounts_bp.route("/logout")
@login_required
def logout():
logout_user()
flash("You were logged out.", "success")
return redirect(url_for("accounts.login"))
The Flask-Login
library contains a logout_user
method that removes the user from the session. We used the @login_required
decorator so that only authenticated users can log out.
How to Run the Completed App for the First Time
Now that your application is ready (without the tests), you can first migrate the database and then run the app.
- To initialize the database (create a migration repository), use the command:
flask db init
- To migrate the database changes, use the command:
flask db migrate
- To apply the migrations, use the command:
flask db upgrade
Since this is the first time we're running our app, you'll need to run all the above commands. Later, whenever you make changes to the database, you'll just need to run the last two commands.
After that, you can run your application using the command:
python manage.py run
How to Add Unit Tests to the App
Now that we have all the features ready, create a tests
folder in the root directory and convert it into a package by adding an empty __init__.py
file.
How to Create a Base TestCase
Let's create a base test case that will be extended by the other test cases. Create a base_test.py
file inside the tests
package, and add the following code:
import os
from flask_testing import TestCase
from src import app, db
from src.accounts.models import User
class BaseTestCase(TestCase):
def create_app(self):
app.config.from_object("config.TestingConfig")
return app
def setUp(self):
db.create_all()
user = User(email="ad@min.com", password="admin_user")
db.session.add(user)
db.session.commit()
def tearDown(self):
db.session.remove()
db.drop_all()
testdb_path = os.path.join("src", "testdb.sqlite")
os.remove(testdb_path)
The BaseTestCase
class extends the TestCase
class and implements the following three methods:
The
create_app()
method is a required method that should return a Flask instance. If you don’t definecreate_app()
,NotImplementedError
will be raised. Notice that we're using theTestingConfig
in this case.The
setUp()
method is called before running any test. In this method, we create all the database tables. Additionally, we also create a user so that we can play with it later.The
tearDown()
method is called after running all the test cases. So, in this method, we'll clean up all the test data.
How to Write Tests for Forms
In the above sections, we created two forms – RegisterForm
and LoginForm
. Let's test these forms in a new test file named test_forms.py
inside the tests
package.
import unittest
from base_test import BaseTestCase
from src.accounts.forms import LoginForm, RegisterForm
class TestRegisterForm(BaseTestCase):
def test_validate_success_register_form(self):
# Ensure correct data validates.
form = RegisterForm(email="new@test.com", password="example", confirm="example")
self.assertTrue(form.validate())
def test_validate_invalid_password_format(self):
# Ensure incorrect data does not validate.
form = RegisterForm(email="new@test.com", password="example", confirm="")
self.assertFalse(form.validate())
def test_validate_email_already_registered(self):
# Ensure user can't register when a duplicate email is used
form = RegisterForm(
email="ad@min.com", password="admin_user", confirm="admin_user"
)
self.assertFalse(form.validate())
class TestLoginForm(BaseTestCase):
def test_validate_success_login_form(self):
# Ensure correct data validates.
form = LoginForm(email="ad@min.com", password="admin_user")
self.assertTrue(form.validate())
def test_validate_invalid_email_format(self):
# Ensure invalid email format throws error.
form = LoginForm(email="unknown", password="example")
self.assertFalse(form.validate())
if __name__ == "__main__":
unittest.main()
The TestRegisterForm
class defines three test methods to test the validate
method of the RegisterForm
class.
The first test method tests that the form validates with correct input data. The second test method tests that the form does not validate with an invalid password format. And the third test method tests that the form does not validate when a duplicate email is used to register.
The TestLoginForm
class defines two test methods to test the validate
method of the LoginForm
class. The first test method tests that the form validates with correct input data, and the second test method tests that the form does not validate with an invalid email format.
How to Test the User Model
Let's now test the User
model in a new file named test_models.py
inside the tests
package.
import datetime
import unittest
from base_test import BaseTestCase
from flask_login import current_user
from src import bcrypt
from src.accounts.models import User
class TestUser(BaseTestCase):
def test_user_registration(self):
# Ensure user registration behaves correctly.
with self.client:
self.client.get("/logout", follow_redirects=True)
self.client.post(
"/register",
data=dict(
email="test@user.com", password="test_user", confirm="test_user"
),
follow_redirects=True,
)
user = User.query.filter_by(email="test@user.com").first()
self.assertTrue(user.id)
self.assertTrue(user.email == "test@user.com")
self.assertFalse(user.is_admin)
def test_get_by_id(self):
# Ensure id is correct for the current/logged in user
with self.client:
self.client.get("/logout", follow_redirects=True)
self.client.post(
"/login",
data=dict(email="ad@min.com", password="admin_user"),
follow_redirects=True,
)
self.assertTrue(current_user.id == 1)
def test_created_on_defaults_to_datetime(self):
# Ensure that registered_on is a datetime
with self.client:
self.client.get("/logout", follow_redirects=True)
self.client.post(
"/login",
data=dict(email="ad@min.com", password="admin_user"),
follow_redirects=True,
)
user = User.query.filter_by(email="ad@min.com").first()
self.assertIsInstance(user.created_on, datetime.datetime)
def test_check_password(self):
# Ensure given password is correct after unhashing
user = User.query.filter_by(email="ad@min.com").first()
self.assertTrue(bcrypt.check_password_hash(user.password, "admin_user"))
self.assertFalse(bcrypt.check_password_hash(user.password, "foobar"))
def test_validate_invalid_password(self):
# Ensure user can't login when the pasword is incorrect
with self.client:
self.client.get("/logout", follow_redirects=True)
response = self.client.post(
"/login",
data=dict(email="ad@min.com", password="foo_bar"),
follow_redirects=True,
)
self.assertIn(b"Invalid email and/or password.", response.data)
if __name__ == "__main__":
unittest.main()
The TestUser
class defines four test methods to test various aspects of the User
model class.
The first test method tests the user registration process by posting a registration request to the server with the
client
object, which is a Flask test client. The test verifies that a new user is correctly added to the database and that the user's attributes are correctly set.The second test method tests the
get_by_id
method, which is a helper method to get the user object from the database by its id. The test logs in a user and verifies that the current user's id is correct.The third test method tests that the
created_on
attribute of the user object is adatetime
object.The fourth test method tests the
check_password
method, which is a helper method to check the user's password. The test verifies that the method correctly checks a correct and an incorrect password.The fifth test method tests the login process by posting a login request to the server with the
client
object and verifies that the server responds with an error message when the password is incorrect.
How to Test the Routes
Let's now test the routes in a new file named test_routes.py
inside the tests
package.
import unittest
from base_test import BaseTestCase
from flask_login import current_user
class TestPublic(BaseTestCase):
def test_main_route_requires_login(self):
# Ensure main route requres logged in user.
response = self.client.get("/", follow_redirects=True)
self.assertTrue(response.status_code == 200)
self.assertIn(b"Please log in to access this page", response.data)
def test_logout_route_requires_login(self):
# Ensure logout route requres logged in user.
response = self.client.get("/logout", follow_redirects=True)
self.assertIn(b"Please log in to access this page", response.data)
class TestLoggingInOut(BaseTestCase):
def test_correct_login(self):
# Ensure login behaves correctly with correct credentials
with self.client:
response = self.client.post(
"/login",
data=dict(email="ad@min.com", password="admin_user"),
follow_redirects=True,
)
self.assertTrue(current_user.email == "ad@min.com")
self.assertTrue(current_user.is_active)
self.assertTrue(response.status_code == 200)
def test_logout_behaves_correctly(self):
# Ensure logout behaves correctly, regarding the session
with self.client:
self.client.post(
"/login",
data=dict(email="ad@min.com", password="admin_user"),
follow_redirects=True,
)
response = self.client.get("/logout", follow_redirects=True)
self.assertIn(b"You were logged out.", response.data)
self.assertFalse(current_user.is_active)
if __name__ == "__main__":
unittest.main()
The TestPublic
class defines two test methods to test the access control of certain routes.
The first test method tests that the main route requires a logged-in user by attempting to access it with the client
object, which is a Flask test client. The test verifies that the server responds with a login prompt. The second test method tests that the logout route also requires a logged-in user.
The TestLoggingInOut
class defines two test methods to test the login and logout functionality.
The first test method tests the login process by posting a login request to the server with the client
object and verifies that the server responds with a successful login. The second test method tests the logout process by posting a logout request to the server with the client
object. It then verifies that the server responds with a logout message and that the user is no longer logged in.
How to Run the Tests
Now that we have all the tests ready, we are ready to run the testcases. But before that, as mentioned in the beginning, we'll need to add a command in the manage.py
file to run the tests.
import unittest
@cli.command("test")
def test():
"""Runs the unit tests without coverage."""
tests = unittest.TestLoader().discover("tests")
result = unittest.TextTestRunner(verbosity=2).run(tests)
if result.wasSuccessful():
return 0
else:
return 1
The command runs the unit tests in the tests
package and displays the results in the terminal.
You use the unittest.TestLoader().discover()
method to discover and run all the unit tests in the tests
package. You use the unittest.TextTestRunner()
method to run the unit tests and to print the results to the terminal. The verbosity
argument controls the level of detail in the output.
If all the unit tests pass, the test
command returns a exit code of 0. If any of the unit tests fail, the command returns a exit code of 1.
Now, you can run all the tests using the command:
python manage.py test
This will give the below output:
test_validate_invalid_email_format (test_forms.TestLoginForm) ... ok
test_validate_success_login_form (test_forms.TestLoginForm) ... ok
test_validate_email_already_registered (test_forms.TestRegisterForm) ... ok
test_validate_invalid_password_format (test_forms.TestRegisterForm) ... ok
test_validate_success_register_form (test_forms.TestRegisterForm) ... ok
test_check_password (test_models.TestUser) ... ok
test_created_on_defaults_to_datetime (test_models.TestUser) ... ok
test_get_by_id (test_models.TestUser) ... ok
test_user_registration (test_models.TestUser) ... ok
test_validate_invalid_password (test_models.TestUser) ... ok
test_correct_login (test_routes.TestLoggingInOut) ... ok
test_logout_behaves_correctly (test_routes.TestLoggingInOut) ... ok
test_logout_route_requires_login (test_routes.TestPublic) ... ok
test_main_route_requires_login (test_routes.TestPublic) ... ok
----------------------------------------------------------------------
Ran 14 tests in 19.577s
OK
Features to Add to Your Application
Here are some extra things you can add to your application. Note that these are optional.
How to Create an Admin
Similar to the test
command, you can add a create_admin
command to create an admin in your application. Add the following code inside the manage.py
file:
import getpass
@cli.command("create_admin")
def create_admin():
"""Creates the admin user."""
email = input("Enter email address: ")
password = getpass.getpass("Enter password: ")
confirm_password = getpass.getpass("Enter password again: ")
if password != confirm_password:
print("Passwords don't match")
return 1
try:
user = User(email=email, password=password, is_admin=True)
db.session.add(user)
db.session.commit()
except Exception:
print("Couldn't create admin user.")
The command prompts the user to enter an email address and a password for the admin user. The password is entered using the getpass
module, which hides the password input from the terminal. The command then checks if the entered password and the confirmed password match. If the passwords don't match, the command prints an error message and returns a exit code of 1.
If the passwords match, the command creates a new User
object with the entered email address, password, and the is_admin
attribute set to True
. The command then adds the user object to the database session and commits the changes to the database. If an exception is raised during this process, the command prints an error message.
You can run the below command to create one:
python manage.py create_admin
Output:
> python manage.py create_admin
Enter email address: admin@myapp.com
Enter password:
Enter password again:
Admin with email admin@myapp.com created successfully!
How to Create Error Pages
Our application can get errors at any time. The most common errors that we get are Unauthorized (401), Not Found (404), and Server Error (500).
Let's create an errors
directory inside the templates
directory and create three HTML pages as below:
- 401.html
{% extends "_base.html" %}
{% block content %}
<h1>401</h1>
<p>Run along!</p>
<p><em>Return <a href="{{url_for('core.home')}}">Home</a>?</em></p>
{% endblock %}
- 404.html
{% extends "_base.html" %}
{% block content %}
<h1>404</h1>
<p>There's nothing here!</p>
<p><em>Return <a href="{{url_for('core.home')}}">Home</a>?</em></p>
{% endblock %}
- 500.html
{% extends "_base.html" %}
{% block content %}
<h1>500</h1>
<p>Something's wrong! We are on the job.</p>
<p><em>Return <a href="{{url_for('core.home')}}">Home</a>?</em></p>
{% endblock %}
Next, we need to add error handlers for these errors. Open the src/__init__.py
file and add the following code at the bottom of the file:
@app.errorhandler(401)
def unauthorized_page(error):
return render_template("errors/401.html"), 401
@app.errorhandler(404)
def page_not_found(error):
return render_template("errors/404.html"), 404
@app.errorhandler(500)
def server_error_page(error):
return render_template("errors/500.html"), 500
This above code snippet registers error handler functions for the HTTP error codes 401, 404, and 500 in a Flask application. An error handler function is a function that is called when an error occurs in the application.
The error handler functions are decorated with the @app.errorhandler
decorator, which registers them with the Flask application. The decorator takes an error code as an argument, and the function is called when the error code is raised.
Each error handler function returns a rendered template and the error code as a response to the client. The templates are HTML files located in the errors
folder and contain the content to be displayed to the user for each error. The error code is passed as an argument to the render_template
function to determine which template to render.
Wrapping up
In this tutorial, you learned how to set up basic user authentication in your Flask app. You also wrote a few test cases in order to test the functionalities.
Here's the link to the GitHub repository. Feel free to check it out whenever you're stuck.
Recommended next steps
You can add more security such as email verification, or token-based authentication in the app.
You can add a "forgot password" feature in the application.
You can add more testcases in order to test the app more thoroughly.
Thank you for reading. I hope you found this article useful. You can follow me on Twitter.