2014-02-15

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:

  1. Getting Started
  2. Server Side Backbone Sync
  3. Dataset Persistence
  4. Custom Configuration
  5. Testing Todo API
  6. 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.