User authentication in Flask with Flask-Security-Too
Adding user authentication to a web application makes it able to tell which user is making a request. It can then return different responses to different users. This is called "personalisation".
This is article 1 of 3 in this series of blog posts, where I'll show you how to work with Flask-Security-Too:
- User authentication in Flask with Flask-Security-Too (this article)
- User email confirmations with Flask-Security-Too
- Customising templates and emails of Flask-Security-Too
To help you understand user authentication, it's useful to remind ourselves of the separation between client and server. If you're developing a Flask application, then you're developing the server. Your clients are browsers or mobile apps making the requests to your application.
For this series, let's assume you are developing a Flask application that uses render_template
to send HTML to your browser clients.
Before learning about Flask-Security-Too, it's probably going to be helpful to learn how to code your own user authentication manually. It's not terribly difficult, and will really aid in understanding how Flask-Security-Too works. We've got two blog posts that I recommend reading in order:
Read those two and work through the examples provided. When you're done, you should have a Flask app that supports user signup and login. That's what we can replace most of our custom logic with Flask-Security-Too!
Add user registration and authentication using Flask-Security-Too
This is the bread and butter of Flask-Security-Too! Here's what you need:
- A database to store user data (username, password, that kind of thing).
- A few configuration options in your Flask app (secret key and password hashing salt).
- To actually initialise Flask-Security-too when you create your
Flask
object.
For our database interactions, I will use SQLAlchemy. We don't have any introductory blog posts on using SQLAlchemy, but our free REST APIs e-book does cover it in detail.
In any case, SQLAlchemy is relatively straightforward: you define a Python class which represents a table in your database. When you create new objects of said class, they can be inserted into the database as separate rows. You can also retrieve data from the database in the form of Python objects.
The aim of SQLAlchemy is to simplify working with data using Python by referring to everything as Python objects, rather than writing SQL yourself.
To work with SQLAlchemy using Flask apps, it's common to use the flask-sqlalchemy
library.
Let's begin by installing it and flask-security-too
. I'll add this to my requirements.txt
file:
flask
flask-sqlalchemy
flask-security-too
flask-mailman
bcrypt
python-dotenv
The python-dotenv
library is added to make it easier to load .env
files when running our Flask app. More on that shortly! The flask-mailman
library is used for sending confirmation emails (in part 2 of this series). The bcrypt
library is used for password hashing.
Install the libraries:
pip install -r requirements.txt
Now let's create the SQLAlchemy
object, which we will use to connect to the database. Inside database.py
, write the following:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
Next up, let's create our database models. These are the Python classes which represent tables in our database.
Inside a models
folder, create auth.py
and write the following:
from database import db
from flask_security import UserMixin, RoleMixin
from sqlalchemy import Boolean, DateTime, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, backref
class RolesUsers(db.Model):
__tablename__ = "roles_users"
id = Column(Integer(), primary_key=True)
user_id = Column("user_id", Integer(), ForeignKey("user.id"))
role_id = Column("role_id", Integer(), ForeignKey("role.id"))
class Role(db.Model, RoleMixin):
__tablename__ = "role"
id = Column(Integer(), primary_key=True)
name = Column(String(80), unique=True)
description = Column(String(255))
class User(db.Model, UserMixin):
__tablename__ = "user"
id = Column(Integer(), primary_key=True)
email = Column(String(255), unique=True)
username = Column(String(255), unique=True, nullable=True)
password = Column(String(255), nullable=False)
last_login_at = Column(DateTime())
current_login_at = Column(DateTime())
last_login_ip = Column(String(100))
current_login_ip = Column(String(100))
login_count = Column(Integer)
active = Column(Boolean())
premium = Column(Boolean())
fs_uniquifier = Column(String(255), unique=True, nullable=False)
confirmed_at = Column(DateTime())
roles = relationship(
"Role", secondary="roles_users", backref=backref("users", lazy="dynamic")
)
As you can see, this defines three tables: one for users, one for roles (similar to permissions), and one for linking users and roles. We won't be using roles in this tutorial, but Flask-Security-Too requires it.
Finally, in app.py
let's create our Flask app and set up Flask-Security-Too:
import os
from flask import Flask
from flask_security import SQLAlchemySessionUserDatastore, Security
from dotenv import load_dotenv
from database import db
from models.auth import User, Role
load_dotenv()
app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get(
"SECRET_KEY", "0aedgaii451cef0af8bd6432ec4b317c8999a9f8g77f5f3cb49fb9a8acds51d"
)
app.config["SECURITY_PASSWORD_SALT"] = os.environ.get(
"SECURITY_PASSWORD_SALT",
"ab3d3a0f6984c4f5hkao41509b097a7bd498e903f3c9b2eea667h16",
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECURITY_REGISTERABLE"] = True
uri = os.getenv("DATABASE_URL")
app.config["SQLALCHEMY_DATABASE_URI"] = uri
db.init_app(app)
user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role)
security = Security(app, user_datastore)
@app.route("/")
def home():
return "Hello, world!"
Now that we've got this, we need to set up our environment variables. At the very least, we need the DATABASE_URL
, to tell our Flask app which database to connect to.
In a new file called .env
, write this:
DATABASE_URL="sqlite:///data.db"
Now let's create our tables. In the console (having your virtual environment activated), type:
flask shell
And there, type:
>>> from app import app, db
>>> with app.app_context():
db.create_all()
This will create your database using the app configuration, which should create an instance
folder, and data.db
inside it. If later on you delete instance/data.db
, just re-run flask shell
and type those commands in to re-create it.
Now you can run your Flask app! First exit the shell (by pressing CTRL+D) and then type:
flask run
Now you can access the login and signup pages that Flask-Security-Too has added to your Flask app for you. For example, going to http://127.0.0.1:5000/login should show you something like this:
If you use the menu in that page to go to the "Register" page (or navigate manually to http://127.0.0.1:5000/register) then you'll see a very similar form, which adds a password confirmation field.
These pages already include error handling and cookie support for the "Remember Me" checkbox.
However, they don't look very good. To fix that, you can either add some CSS code to target the existing elements, or you can code your own login and register pages. We won't talk about that in this blog post, but leave a comment at the end if it's something you'd like me to write about!
Now that we've added user authentication, we can move on to email confirmation in the next article of this series. Thank you for reading, and I'll see you there!
If you'd like to learn more about web development using Flask, consider enrolling in our Web Developer Bootcamp with Flask and Python! It's a complete video course that covers building multiple web apps and deploying them, all using Flask.