Flask TodoMVC: Login
This is the seventh article in the Flask TodoMVC tutorial, a series that creates a Backbone.js backend with Flask for the TodoMVC app. In this article, we will add user authentication using the Flask-Security extension, focusing on setup and login. In doing so, we will define models for users and roles and discuss SQLAlchemy relationships.
Previous articles in this series:
- Getting Started
- Server Side Backbone Sync
- Dataset Persistence
- Custom Configuration
- Testing Todo API
- SQLAlchemy Persistence
We will begin with where we left off in the previous article. If you would like to follow along, but do not have the code, it is available on GitHub.
# Optional, use your own code if you followed the previous article $ git clone https://github.com/kevinbeaty/flask-todomvc $ cd flask-todomvc $ git checkout -b login sqlalchemy
Let's get started.
Introduction
Flask-Security is an extension that quickly adds authentication, authorization, registration and password recovery to your app. It combines several other popular Flask extensions, including: Flask-Login for authentication, Flask-Principal for role based authorization, Flask-WTF for form validation, Flask-Mail for sending registration, confirmation and reset password emails and Flask-Script for command line user management scripts.
It also supports password encryption using passlib and secure key generation for token based authentication, optional account activation, and password recovery using itsdangerous.
If that weren't enough, it adds a data store abstraction layer to persist and query your users and roles and automatically track user login activity and confirmation. Current supported data stores include Flask-SQLAlchemy, Flask-MongoEngine and Flask-Peewee, but you can easily create a custom store if so desired. (How about dataset?)
Flask-Security makes things easy to get started, but may seem too feature rich or opinionated for some use cases. Like all Flask extensions, take what you want and leave the rest. If, at some point, you decide Flask-Security conventions are getting in the way, at the very least it's a good example of how to combine several common extensions and provide security to your app. I've found working with Flask-Security a very pleasant way to add some of the most important features common to many apps. I'd highly recommend it.
OK, enough talk. Let's get this thing installed.
$ pip install Flask-Security py-bcrypt
Flask-Security supports password encryption but requires a backend to define the algorithm. We will use py-bcrypt so we install that as well.
Let's also get the imports necessary for this article out of the way.
# server.py from flask_security import ( Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin, login_required) from flask_security.utils import encrypt_password
We will explain all of these imports as we progress through this article. Let's begin by defining models for users and roles.
Models
Flask-Security requires the addition of two models: users and roles. The user model stores login credential and optional audit information (create time, last login time, IP address, etc.) for all individuals who have access to your app. A role can be thought of as a group of users that may have elevated privileges. A user with the "admin" role would have access to the administration section of an app, a "manager" could view financial reports, etc. Roles are usually defined at the application level.
You may need to provide finer grain access control, e.g. allow a manager to track time for personnel within her own department, but not have access to other departments. In this case, you would add additional models that are scoped to your department models and provide authorization with finer control. If you find yourself wanting more granular control, take a closer look at the Flask-Principal extension, the extension that Flask-Security uses to provide role based authorization.
We may dive further into authorization in the future, but today we are going to focus on setting up Flask-Security and adding authentication to our todo list page.
Roles
Let's start with defining the role model.
class Role(db.Model, RoleMixin): __tablename__ = 'roles' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String, unique=True) description = db.Column(db.String)
Most of this should look familiar if you followed the previous article. Roles belong
to the roles table. We added a primary key id, role name (e.g. 'admin') and a human readable
description. We defined the name
column as unique as we will identify roles by name in
our code. This is one of several options supported by SQLAlchemy when defining columns.
We inherit from both db.Model
and the RoleMixin
we imported from Flask-Security. This may
seem a little strange if you haven't seen it before. We won't get into the details, or debate
the merits and pitfalls of multiple inheritance, but I do believe that mixins are a valid use
case. They allow you to append functionality to an existing class hierarchy. They are also
sometimes used as a "marker interface" to identify objects that could, for example, be serialized
in a certain way.
In this case, consider db.Model
your base class and the RoleMixin
a mechanism to mixin
convenience methods defined outside the model hierarchy. Currently Flask-Security only
mixes in __eq__
and __neq__
to define role equality based on the unique role name.
Users
Now, let's add our user model.
class User(db.Model, UserMixin): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String, unique=True) password = db.Column(db.String) active = db.Column(db.Boolean) roles = db.relationship( 'Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'))
Again, most of this is familiar. We identify the users table, define an id primary key, add columns for the email address and password and an active flag.
When using Flask-Security, users are identified by email address. The email address is the login name. This same email address is used to send forgot password and confirmation emails, if those features are enabled. Only active users that identify the correct email address and password will be allowed to login.
Note that you can add custom columns at will to your user model. You may, for example, want to include contact or organization hierarchy details. Flask-Security also supports optional features to confirm or track users. If you would like to enable these features, be sure to include the additional columns.
We also inherit the UserMixin
imported from Flask-Security. This currently mixes in all methods
required by the user class of Flask-Login in addition to has_role
for checking whether a
user has a role identified by name or instance, and a get_auth_token
method to be used for
optional token based authentication.
The roles attribute warrants further discussion. It is our first encounter with SQLAlchemy relationships.
Relationships
As we discussed, there is a relationship between users and roles. There may be more than one user with the same role, e.g. you could have multiple ninjas, rockstars and hipsters in your Web 3.0 startup. A user identifies individuals. Each individual could have zero or more roles. You, of course, are a ninja rockstar, but not all that hip (you just know what you're talking about, that's all), so you would have two roles.
So a user could have several roles, and a role could be associated with multiple users. This type of relationship is known as a "Many to Many" and requires a join table that includes foreign keys to identify each side of the relationship. Since we don't want to treat this table as its own entity (modeled association), we can define the table directly.
roles_users = db.Table( 'roles_users', db.Column('user_id', db.Integer, db.ForeignKey('users.id')), db.Column('role_id', db.Integer, db.ForeignKey('roles.id')))
This creates the join table necessary to create a Many to Many relationship between users
and roles. We call it roles_users
to reflect the relationship. (We could have called it
users_roles
as well, but this distinction is mostly arbitrary.) We then identify two integer columns
with foreign keys to the appropriate column and table. Notice that here we used the table
and column name, not the model name.
Other relationships include "Many to One" or "One to Many" (depending on which side you consider holds the relationship). If you wanted to store multiple addresses for your users, and ignore or don't care that two users might share the same address, your address model would have a "Many to One" relationship with your user, and your user model would have a "One to Many" relationship with addresses.
A less common relationship is "One to One". You could use this, for example, to associate a
user object with a contact model. The contact could contain all contact information that make
up an address book. The user could be associated with an existing contact by defining a contact_id
on the user model. In this way, you could associate users to your address book, but not require
all contacts to have login credentials.
Take a look again at the relationship defined in the User model.
roles = db.relationship( 'Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'))
This defines the relationship between users and roles. We are adding a roles
attribute
to our User model. Accessing this attribute will list all roles associated with the user.
The association is to the Role
model, which we identify as the first argument. We specify
our join table defined above to setup the Many to Many relationship.
Finally we specify a backref
named users
on the Roles model. This will add an attribute
to any role instance that retrieves all users with the given role through a users
attribute.
Since we specify the backref relationship as dynamic, we will not get a list of roles, but a
SQLAlchemy query that we can further filter. We could find, for example, all enabled managers by
first retrieving the role and then filtering by active, i.e. manager_role.filter_by(active=True).all()
.
You could also specify the relationship itself as dynamic, not just the backref. See the SQLAlchemy documentation on dynamic relationships for further information.
Setup
With our models defined, we are finally ready to initialize the Flask-Security extension. The extension requires an app and a data store. Lucky for us, Flask-Security has built in support for Flask-SQLAlchemy.
user_datastore = SQLAlchemyUserDatastore(db, User, Role) security = Security(app, user_datastore)
The data store is initialized with our Flask-SQLAlchemy db
and the user and role models
we defined above. The extension is then initialized with our app and data store.
We also have to update our configuration to setup password encryption and secure cookie storage.
# config/default.py SECRET_KEY = u'Gh\x00)\xad\x7fQx\xedvx\xfetS-\x9a\xd7\x17$\x08_5\x17F' SECURITY_PASSWORD_HASH = 'bcrypt' SECURITY_PASSWORD_SALT = SECRET_KEY
When we login to our app, Flask-Security will store the user id in a session so subsequent
requests will not require a login. Flask stores all session data in a signed cookie, which
requires SECRET_KEY
to be configured. It is a good idea to use a random string for your
secret key and change it for different installations. This example used os.urandom(24)
to
generate a random string. Run on your app to create your own.
We also setup password encryption using bcrypt and set the salt to the secret key. We do not want to store the passwords in plain text in our user table. If anyone gains access to our database, we do not want to leak passwords associated with a user email address, especially since too many users reuse the same password across multiple sites. Since passwords are encrypted, we will never know what password the user provided simply by looking at the database. Specifying the password hash algorithm and salt is all we need to setup encryption using Flask-Security.
Before we require login, we should setup a user that can login. We will add user registration
in a future article, but for now, let's create a single user in init_db
.
def init_db(): with app.app_context(): db.create_all() if not User.query.first(): user_datastore.create_user( email='kevin@example.com', password=encrypt_password('password')) db.session.commit()
Querying the user model for the first entry will return a single user or None
if it
does not exist. Remember, db.create_all
will create any tables that do not exist, so
our users and roles tables will be created the first time we start the app. If we don't
yet have a user, we create one using a convenience method on the user data store.
We need to commit our session since we made changes to the database.
We run the method within a Flask application context. We are using encrypt_password
from Flask-Security which requires a context to be setup. An application context is
created by Flask automatically for each request and proxies are setup to be valid within
this context. Since we are calling init_db
standalone, we explicitly create an app context.
We could alternatively use @app.before_first_request
.
Requiring login
Now that we have Flask-Security initialized, the hard work is done. All we have
left to do is enforce login by using the login_required
decorator.
@app.route('/') @login_required def index(): todos = Todo.query.all() ...
Go ahead and start your app and navigate to localhost:8000
. You should be redirected to
the login page. Login with the credentials you setup in init_db
to visit the todo list.
At this point we should decorate all API endpoints with @login_required
because, without
that protection, anyone could still modify our database by requests to the Todo API. This
is exactly what our tests are doing. We are going to delay protecting the API and fixing
the tests for a future article.
Our login page isn't very pretty. If you want to logout, currently you need to visit
localhost:8000/logout
. We will address these issues in a future article.
Conclusion
In this article, we added user authentication to our Todo list using Flask-Security. While doing this, we added additional models for users and roles and discussed SQLAlchemy relationships.
Our app is getting a little cluttered, in the next article we will clean up a little and focus on modularization.
That completes our integration of authentication from Flask-Security. If you made it this far you should follow me on Twitter and GitHub.
The code is available on GitHub with tag login or compared to previous article.