Building a secure admin interface with Flask-Admin and Flask-Security

Implement authentication and authorization with Flask-Security

My team and I were recently presented the challenge of building a secure administrative interface for our application. We had already built the application’s frontend in React and the application’s backend in Python primarily using the Flask framework and associated extension packages. For the administrative interface, we chose to use the Flask-Admin package because Flask-Admin makes it quick and easy to build an interface on top of our existing data models. However, the task was to build a secure administrative interface, and, in classic Flask fashion, Flask-Admin leaves it up to the developer to choose our approach. Rather than reinventing the wheel and building authorization and authentication capabilities from the ground up, Flask provides some helpful extension packages like Flask-Login and Flask-Security that integrate rather smoothly with Flask-Admin.

In a separate blog, I explain how to get started with the Flask framework for Python and how to build a user interface with Flask. In this blog, I dive deeper into Flask by building an administrative interface with Flask-Admin that is secured using Flask-Security. To follow along, you can find the source code on my GitHub.

The basic building blocks of authorization and authentication are users and user roles where users represent the people trying to access our application and user roles represent the roles that we assign to those people to determine the parts of our application to which they have access. Therefore, our first step is to add the data models representing our users and user roles to our database.

In this exercise, we use a PostgreSQL database. To interact with this database we install SQLAlchemy, an object-relational mapper (ORM), in our application’s environment. If you are following along with the source code, you can easily install SQLAlchemy and all of the dependencies to run this application using the included requirements.txt file and the pip install -r requirements.txt command.

from flask_security import RoleMixin, UserMixin
from flask_sqlalchemy import SQLAlchemy
...db = SQLAlchemy(secureApp)roles_users_table = db.Table('roles_users',
db.Column('users_id', db.Integer(),
db.ForeignKey('users.id')),
db.Column('roles_id', db.Integer(),
db.ForeignKey('roles.id')))
class Users(db.Model, UserMixin):
id = db.Column(db.Integer(), primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(80))
active = db.Column(db.Boolean())
roles = db.relationship('Roles', secondary=roles_users_table,
backref='user', lazy=True)
class Roles(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
...

We first import the SQLAlchemy class from the Flask-SQLAlchemy package, create an instance of the class, and pass to it a reference to our Flask application. This gives us access to the Flask-SQLAlchemy interface which is essentially a declarative extension of SQLAlchemy.

We then use this interface to define a roles_users_table table which will be our helper table. We need this helper table because our users and our user roles will have a many-to-many relationship, i.e. one user can have many roles and one role can have many users. Flask-SQLAlchemy strongly encourages us to define this helper table as an instance of the Table class rather than as an instance of the Model class. Now that we have defined the relationship between users and user roles, we will define these two entities.

To represent our various users, we define an instance of the Model class . We implement some standard properties that a user would have like an email and password, but we also implement some interesting properties like active that administrators can use to activate and deactivate specific users. In addition, Flask-Security expects the Model class that we use to represent users to implement specific properties and methods, e.g. is_active and is_authenticated. Rather than define these methods ourselves, we direct our Users class to inherit them from the UserMixin class that Flask-Security provides which includes default implementations for all required properties and methods. This also gives us some piece of mind that our Users class will inherit any future required properties and methods that Flask-Security might implement.

Next, we define another instance of the Model class to represent our various user roles. Similar to our Users class, we implement the standard properties of a role. In addition, we direct our Roles class to inherit the RoleMixin class provided by Flask-Security. Similar to the UserMixin class, the RoleMixin class is the provided mixin for role model definitions.

To initialize Flask-Security, we need at least two parameters. The first is a reference to our Flask application. The second is a reference to an instance of a user datastore. A datastore is a repository for persistently storing and managing collections of data. As mentioned in the Adding users and roles to the database section above, this exercise implements a PostgreSQL database which will contain the user datastore and which we interact with via the SQLAlchemy interface. Therefore, the final piece to the puzzle is to extract our users and our user roles from the database using this interface. Luckily, Flask-Security provides the SQLAlchemyUserDatastore class to do this.

from flask_security import Security, SQLAlchemyUserDatastore...user_datastore = SQLAlchemyUserDatastore(db, Users, Roles)
security = Security(secureApp, user_datastore)
...

We create an instance of SQLAlchemyUserDatastore and pass to it a reference to our PostgreSQL database and our model classes defining our users and roles, i.e. Users and Roles.

In addition to initializing Flask-Security, SQLAlchemyUserDatastore provides a number of helpful methods representing standard identity management tasks. For example, the create_user method enables us to quickly add new users.

We are already familiar with adding data to a database, so, rather than using the built-in methods from SQLAlchemyUserDatastore, we could use the same practice to add new users.

@secureApp.before_first_request
def create_user():
db.session.add(Users(email='admin', password='admin')
db.session.commit()

But this generic approach is very limited in what it can do. Instead, we use the built-in create_user method to add new users.

@secureApp.before_first_request
def create_user():
user_datastore.create_user(email='admin', password='admin')
db.session.commit()

All we need to do is pass fields from our Users model class to the create_user method as keyword arguments and then commit the insert to our database. This method creates the new user with our desired credentials, sets the active field of our Users model to True, and hashes the given password so that what is stored in the database is an encrypted representation of the user’s actual password. In addition, this method returns the newly created user enabling us to perform additional identity management. For example, my use case might be to add a user to the database but set their active status to False until approved by a database administrator. This is quick and easy to do using the built-in methods from SQLAlchemyUserDatastore.

@secureApp.before_first_request
def create_user():
first_user = user_datastore.create_user(email='admin',
password='admin')
user_datastore.toggle_active(first_user)
db.session.commit()

This is also syntactically easier for our fellow developers to understand what this code does as create_user is explicit whereas session.add is vague and likely to be used in multiple other places in our code.

In my code, I use the before_first_request decorator provided by Flask which registers a function to run right before the first request to our application. I use this to drop all of the tables in the database, rebuild new tables, and create a default user when we call ‘localhost:5000’ to run the application.

The whole reason that we implemented Flask-Security for this exercise was to ensure that only authenticated users were able to manage the data in our database. In this step, we will implement this by creating a view of the data from our User data model and modifying it with our own access control rules.

from flask_admin.contrib.sqla import ModelView
from flask_security import current_user
...class UserModelView(ModelView):
def is_accessible(self):
return (current_user.is_active and
current_user.is_authenticated)
def _handle_view(self, name):
if not self.is_accessible():
return redirect(url_for('security.login'))
...

Flask-Admin lets us define our own access control rules on any view class by overriding the is_accessible method. Flask-Security provides a proxy for the current user with the current_user object which we can use to determine whether a user is logged in and active (i.e. has not been denied access). We set our access control rules to hide our UserModelView from the navigation menu for any user that is not logged in and is not active.

In addition, we will want to handle the case where someone might know the URL endpoint of our UserModelView, e.g. ‘localhost:5000/admin/users’, and tries to access the view by going directly to that URL. Because of the access control rules that we already applied, they will not have access to the view, but they will run into an unpleasant 403 Forbidden error message. Rather then leave them hanging, a better user experience would be to redirect them to our login view where they can at least attempt to authenticate. To achieve this, Flask-Admin provides the _handle_view lifecycle method which is executed before calling any view method. In this method, we add logic to check if is_accessible is False meaning that the user is not in compliance with our access control rules and, if satisfied, redirect them to the login view.

Everything appears to be running fine, but we notice that when we click ‘Login’ or ‘Register’ we seem to leave our beautiful Flask-Admin experience and enter some horrendous, unstyled markdown page.

This is a poor user experience. We want the login view and the register view that we got when we installed Flask-Security to embed seamlessly into the experience that we built with Flask-Admin. To do this, we need to override the default templates that Flask-Security provided us for these views. The first step is to create a folder named security within our application’s templates folder and add a template with the same name for the template you wish to override. In this case, we are overriding the security/login_user.html template and the security/register_user.html template.

mkdir templates/security
touch templates/security/login_user.html
touch templates/security/register_user.html

In our security/login_user.html template, we first have the login template extend the Flask-Admin base template.

{% extends 'admin/master.html' %}

If we leave the login template like this and try to our our application the Jinja template engine will start throwing errors.

jinja2.exceptions.UndefinedError: 'admin_base_template' is undefined

This is because any template that extends admin/master.html needs access to a few environment variables which the Flask-Security templates does not need. We therefore need to inject these new variables into our overridden security/login_user.html template using the context of that template. We can achieve this using a context processor. A content processor is a function that returns a dictionary. The keys and values of this dictionary are then merged with the template context.

...@security.context_processor
def security_context_processor():
return dict(
admin_base_template = admin.base_template,
admin_view = admin.index_view,
h = admin_helpers,
get_url = url_for
)
...

The code above creates variables (eg. admin_base_template) and defines them in the template by giving them a value (e.g. admin.base_template).

All templates are passed a template context object that includes the objects and values that are passed to the template by our main Flask application context processor. The template context object for the built-in templates from Flask-Security also include a form object for the view and the Flask-Security extension object. We can add more values to the template context by specifying a context processor.

In our security/login_user.html template, we can now add the content. Our content is derived from the default login_user.html template with small modifications.

{% extends 'admin/master.html' %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
{{ super() }}
<div class="container">
<div>
<h1>Login</h1>
<div class="well">
<form action="{{ url_for_security('login') }}" method="POST"
name="login_user_form">
{{ login_user_form.hidden_tag() }}
{{ render_field_with_errors(login_user_form.email) }}
{{ render_field_with_errors(login_user_form.password) }}
{{ render_field(login_user_form.next) }}
{{ render_field(login_user_form.submit, class="btn btn-
primary") }}
</form>
<p>
Not yet signed up? Please <a href="{{
url_for('security.register') }}">register for an
account</a>.
</p>
</div>
</div>
</div>
{% endblock %}

We will do the same for our security/register_user.html template and derive the content from the default register_user.html template with small modifications.

{% extends 'admin/master.html' %}
{% from "security/_macros.html" import render_field_with_errors,
render_field %}
{% block body %}
{{ super() }}
<div class="container">
<div>
<h1>Register</h1>
<div class="well">
<form action="{{ url_for_security('register') }}"
method="POST" name="register_user_form">
{{ register_user_form.hidden_tag() }}
{{ render_field_with_errors(register_user_form.email) }}
{{ render_field_with_errors(register_user_form.password) }}
{% if register_user_form.password_confirm %}
{{ render_field_with_errors(register_user_form.password_confirm) }}
{% endif %}
{{ render_field(register_user_form.submit, class="btn btn-primary") }}
</form>
<p>Already signed up? Please
<a href="{{ url_for('security.login') }}">log in</a>.
</p>
</div>
</div>
</div>
{% endblock body %}

When we run our application again after overriding the default login and register templates, we see that these views now appear as a part of the same Flask-Admin experience as our other views.

Now we have implemented authorization and authentication views that are seamlessly integrated with our Flask-Admin administrative interface!

To see Flask-Admin and Flask-Security in action, clone the source code from my GitHub of the application that we built throughout this blog and run it on your local machine.

Colin Kraczkowsky recently returned to web development after exploring the craft of product management. Colin’s professional history includes working in both enterprise and start up environments to code web and mobile applications, launch new products, build mockups and prototypes, analyze metrics, and continuously innovate.

In his spare time, Colin can be found checking out the latest Alite camp kit for a weekend away in Big Sur, planning his next line down a mountain at Kirkwood, or surfing the Horror section on Netflix. Colin is currently located in San Francisco, California.

Connect with Colin — https://www.linkedin.com/in/colinkraczkowsky

Problem solver wielding JavaScript and Python as my tools. Builder of RESTful web services and progressive web applications. Scholar of the newly possible.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store