Use Flask-Login to Add User Authentication to your Python Application

Deploy Flask-Login Python Apps on OpenShift

Flask does not provide user session management facilities so you are required to either roll your own or use an extension. Extensions are a way by which developers can extend Flask; this goes with Flask’s minimalist nature. In this post I’ll show how you can use the Flask-Login extension to add user authentication to your applications and deploy them on OpenShift.

In Part 1 of this blog series on building Python Flask applications we got our basic ‘to do’ app running on OpenShift. The basic ‘to do’ application allows us to store ‘to do’ items and mark them done when finished. Currently, anyone can view anybody else’s ‘to do’ items and make changes to them, so it makes sense to add user authentication to our application.

In Part 1 we used the Flask-SQLAlchemy extension, which added support for the SQLAlchemy ORM to our application. In this blog post we will use the Flask-Login extension, which provides user session management for Flask. It also provides functionality such as logging in, logging out, and remembering a user. You can view the complete list of Flask extensions here.

Prerequisites

Before we can start building the application, we’ll have to do a few set-up tasks:

  1. Basic Python knowledge is required.

  2. Sign up for an OpenShift Account. It is completely free and instant. Red Hat gives every user three free gears on which to run your applications. At the time of writing, the combined resources allocated for each user was 1.5GB of memory and 3GB of disk space.

  3. Install the RHC client tool on your machine. RHC is a Ruby gem so you need to have Ruby 1.8.7 or above on your machine. To install RHC, just type sudo gem install rhc on the command line. If you already have the Gem, make sure it is the latest one. To update RHC, execute the command sudo gem update rhc. For additional assistance setting up the RHC command-line tool, see the following page: https://openshift.redhat.com/community/developers/rhc-client-tools-install.

  4. Set up your OpenShift account using the command rhc setup. This command will help you create a namespace and upload your SSH keys to the OpenShift server.

The source code of the application that we will be developing in this blog post is on GitHub at https://github.com/shekhargulati/flask-login-openshift-quickstart.

Step 1 : Recap

Before we start developing the application we should be at the point where we left off in Part 1. If you have not read Part 1, I recommend reading it before continuing with this blog post.

Type the commands shown below in the terminal of your operating system. The application will be running at http://todo-domainname.rhloud.com. Please replace domainname with your domain name.

$ rhc app create todo python-2.7 postgresql-9.2
$ cd todo
$ git rm -rf wsgi/ setup.py setup.pyc setup.pyo 
$ git commit -am "deleted default source code"
$ git remote add upstream -m master https://github.com/shekhargulati/flask-openshift-template.git
$ git pull -s recursive -X theirs upstream master
$ git push

Step 2 : Add Dependencies in setup.py

This blog post will use Flask-Login for user session management, so we need to add it as a dependency in setup.py. Update the setup.py file with the one shown below.

from setuptools import setup
 
setup(name='TodoApp',
      version='1.0',
      description='Todo Application',
      author='Shekhar Gulati',
      author_email='',
      url='http://www.python.org/sigs/distutils-sig/',
     install_requires=['flask==0.10.1','flask-login==0.2.7','sqlalchemy==0.8.2','flask-sqlalchemy==1.0'],
     )

The key attribute in the code shown above is install_requires=[‘flask==0.10.1′,’flask-login==0.2.7′,’sqlalchemy==0.8.2′,’flask-sqlalchemy==1.0’]. We are using the current version of every package. The install_requires attribute is used to specify a list of strings that represent Python modules that your app needs. If you need other modules that are not listed, you can just add new elements to setup.py. The reason we pegged the app to a particular version is that this prevents the build from checking versions with every Git push. It also prevents a build from putting in a version that breaks our code without our knowledge.

Commit the change to the local Git repository.

$ git commit -am "updated and added dependency for Flask-Login"

Step 3 : Define User Model

For authentication we need a User model class that will store the username and password associated with a user. Open todoapp.py and add the following code to it.

class User(db.Model):
    __tablename__ = "users"
    id = db.Column('user_id',db.Integer , primary_key=True)
    username = db.Column('username', db.String(20), unique=True , index=True)
    password = db.Column('password' , db.String(10))
    email = db.Column('email',db.String(50),unique=True , index=True)
    registered_on = db.Column('registered_on' , db.DateTime)
 
    def __init__(self , username ,password , email):
        self.username = username
        self.password = password
        self.email = email
        self.registered_on = datetime.utcnow()

The code shown above defines a User class that extends the SQLAlchemy db.Model class. We have explicitly specified the name of the table as users. If we don’t specify any table name, the name of the table will be user, i.e. the name of the class. A user table would clash with the default user table in PostgreSQL, so you should explicitly mention the table name. The User model class has four attributes: username, password, email, and registered_on. We set the value of registered_on to the current date when the object is created.

Commit the change to the local Git repository.

$ git commit -am "added User model class"

Step 4 : Configure Flask-Login

To start using Flask-Login in our application we need to create an instance of LoginManager and initialize it with our application instance. LoginManager contains the code that makes application and Flask-Login work together.

from flask.ext.login import LoginManager
 
login_manager = LoginManager()
login_manager.init_app(app)

After you have created an instance of LoginManager and initialized it, you need to tell it which view should be used for login. The views are the handlers that respond to requests coming from clients such as web browsers. To configure that, write the following line in todoapp.py. This will redirect users to the login view whenever they are required to be logged in. We have not created the login view yet but will be doing that later in the post.

login_manager.login_view = 'login'

Another thing that LoginManager allows us to configure is how to load the user from an id. This is done by providing a user_loader callback. The function loads the user from the database.

@login_manager.user_loader
def load_user(id):
    return User.query.get(int(id))

Please note that ids in Flask-Login are always Unicode strings, so we need to convert them to integers before we can query the data using SQLAlchemy.

Commit the changes to the local Git repository.

$ git commit -am "Configured Flask-Login LoginManager"

Step 5 : Update User Model to work with Flask-Login

The User model class needs to implement four methods — is_authenticated() , is_active(), is_anonymous() , and get_id() — so that it can be used with Flask-Login. You can read more about them in the documentation.

class User(db.Model):
    __tablename__ = "users"
    id = db.Column('user_id',db.Integer , primary_key=True)
    username = db.Column('username', db.String(20), unique=True , index=True)
    password = db.Column('password' , db.String(10))
    email = db.Column('email',db.String(50),unique=True , index=True)
    registered_on = db.Column('registered_on' , db.DateTime)
 
    def __init__(self , username ,password , email):
        self.username = username
        self.password = password
        self.email = email
        self.registered_on = datetime.utcnow()
 
    def is_authenticated(self):
        return True
 
    def is_active(self):
        return True
 
    def is_anonymous(self):
        return False
 
    def get_id(self):
        return unicode(self.id)
 
    def __repr__(self):
        return '<User %r>' % (self.username)

Commit the changes to the local Git repository.

$ git commit -am "Updated User model class with Flask-Login methods"

Step 6 : Register User

The application needs to register users before they can log in, so the next step is to add the user registration functionality. The register function shown below responds to both ‘GET’ and ‘POST’ HTTP requests. If a ‘GET’ request comes, it will render register.html so that the user can enter registration details. When a user submits the form with registration data, a ‘POST’ request will be made and data will be persisted to the database. The user will then be redirected to the login page.

@app.route('/register' , methods=['GET','POST'])
def register():
    if request.method == 'GET':
        return render_template('register.html')
    user = User(request.form['username'] , request.form['password'],request.form['email'])
    db.session.add(user)
    db.session.commit()
    flash('User successfully registered')
    return redirect(url_for('login'))
 
@app.route('/login',methods=['GET','POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    return redirect(url_for('index'))

We also need to add two more templates — one for registration and another for login.

Create a new template register.html in the templates folder under wsgi.

{% extends "layout.html" %}
{% block body %}
  <form action="" method=post class="form-horizontal">
    <h2>Sign up for FlaskLogin(Todo) Application </h2>
    <div class="control-group">
        <div class="controls">
          <input type="text" id="username" name="username" class="input-xlarge"
            placeholder="Enter Username" value="{{ request.form.username }}"
            required>
        </div>
    </div>
 
    <div class="control-group">
        <div class="controls">
          <input type="password" id="password" name="password" class="input-xlarge"
            placeholder="Enter Password" value="{{ request.form.password }}"
            required>
        </div>
    </div>
 
    <div class="control-group">
        <div class="controls">
          <input type="email" id="email" name="email" class="input-xlarge"
            placeholder="Enter Email" value="{{ request.form.username }}"
            required>
        </div>
    </div>
 
    <div class="control-group">
        <div class="controls">
          <button type="submit" class="btn btn-success">Signup</button>
        </div>
    </div>
  </form>
{% endblock %}

Create a new template login.html in the templates folder under wsgi.

{% extends "layout.html" %}
{% block body %}
  <form action="" method=post class="form-horizontal">
    <h2>Signin to FlaskLogin(Todo) Application </h2>
    <div class="control-group">
        <div class="controls">
          <input type="text" id="username" name="username" class="input-xlarge"
            placeholder="Enter Username" required>
        </div>
    </div>
 
    <div class="control-group">
        <div class="controls">
          <input type="password" id="password" name="password" class="input-xlarge"
            placeholder="Enter Password" required>
        </div>
    </div>
 
    <div class="control-group">
        <div class="controls">
          <button type="submit" class="btn btn-success">Signin</button>
        </div>
    </div>
  </form>
{% endblock %}

Commit the changes to the local Git repository.

$ git add .
$ git commit -am "added templates for login and register also added register code"

You can test the registration changes by pushing the code to the OpenShift application gear.

$ git push

The application will be accessible at http://todo-domainname.rhcloud.com and you can register by going to http://todo-domainname.rhcloud.com/register. Please replace domainname with your OpenShift domain name.

Step 7 : Login Functionality

In the last step we added the bare minimum login functionality, which rendered a login view after registration. Now we will add functionality for the ‘POST’ requests that users will submit after entering a username and password. The code shown below first imports all the required functions. If the user makes a ‘GET’ request, login.html will be rendered. Otherwise, we will check if a user exists with the given username and password. If the user does not exist, we will flash an error message and redirect the user to the login page. If a user existed with the given username/password combination, we will call the login_user function from the Flask-Login extension and redirect the user to either the index page or the next page the user was intending to visit.

from flask import Flask,session, request, flash, url_for, redirect, render_template, abort ,g
from flask.ext.login import login_user , logout_user , current_user , login_required
 
@app.route('/login',methods=['GET','POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    username = request.form['username']
    password = request.form['password']
    registered_user = User.query.filter_by(username=username,password=password).first()
    if registered_user is None:
        flash('Username or Password is invalid' , 'error')
        return redirect(url_for('login'))
    login_user(registered_user)
    flash('Logged in successfully')
    return redirect(request.args.get('next') or url_for('index'))

To understand the concept of next page, let’s look at a simple example. Suppose you want to create a new ‘to do’ item but the new ‘to do’ view requires authentication. If you are not authenticated, Flask-Login will redirect you to the login page for authentication. Flask-Login will store the original request URL in the next page request attribute, and after a successful login, it will redirect you to the requested page.

Commit the changes to the local Git repository.

$ git commit -am "added login functionality"

Step 8 : Secure Views

So far, all of our views are open so anyone can access them without logging in. Views that require your users to be logged in can be decorated with the login_required decorator, as shown below. Now, if any user goes to http://todo-domainname.rhcloud.com, they will be redirected to the login page. The same holds true for new and show_or_update views.

@app.route('/')
@login_required
def index():
 
 
@app.route('/new', methods=['GET', 'POST'])
@login_required
def new():
 
@app.route('/todos/<int:todo_id>', methods = ['GET' , 'POST'])
@login_required
def show_or_update(todo_id):

Step 9 : Allow Users to Log out

Now that users are logged in to our application, we need to provide a view that allows them to log out from the application. This is done by adding a new view for ‘/logout’. In the logout view, we just call the logout_user() function of Flask-Login, which logs out users and cleans any cookies associated with their session.

@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index')) 

We also have to update layout.html to have a logout link, as shown below.

<div class="nav-collapse collapse">
            <ul class="nav">
                <li class="active"><a href="/">Home</a></li>
                {% if g.user.is_authenticated()%}
                    <li><a href="/new">New Todo</a></li>
                    <li><a href="{{ url_for('logout') }}">Logout</a></li>
                {% endif %}
                {% if not g.user.is_authenticated()%}
                    <li><a href="{{ url_for('login') }}">Login</a></li>
                    <li><a href="{{ url_for('register') }}">Register</a></li>
                {% endif %}
            </ul>
</div>

The g global is set up by Flask as a place to store and share data during the life of a request. We will store the logged in user in it, as shown below. Any functions decorated with before_request will run before the view function each time a request is received.

@app.before_request
def before_request():
    g.user = current_user

Commit the changes to the local Git repository.

$ git commit -am "added logout functionality"

Step 10 : Remember Me functionality

It is very easy to implement ‘remember me’ functionality with Flask-Login; you just need to pass remember=True in login_user. In our code, if a user checks the remember_me checkbox, we will store a cookie as shown below.

@app.route('/login',methods=['GET','POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
 
    username = request.form['username']
    password = request.form['password']
    remember_me = False
    if 'remember_me' in request.form:
        remember_me = True
    registered_user = User.query.filter_by(username=username,password=password).first()
    if registered_user is None:
        flash('Username or Password is invalid' , 'error')
        return redirect(url_for('login'))
    login_user(registered_user, remember = remember_me)
    flash('Logged in successfully')
    return redirect(request.args.get('next') or url_for('index'))

Commit the changes to the local Git repository.

$ git commit -am "added RememberMe functionality"

Step 11 : Add User Relationship to ‘To Do’ Items

We have now restricted anonymous users from accessing our ‘to do’ items, but logged in users can still view and update each other’s ‘to do’ items. To fix this issue, we need to define a relationship between User and Todo. A logged in user can create one or many ‘to do’ items. It is a one-to-many relationship, as shown below. In todoapp.py, add the User-Todo relationship as shown below.

class User(db.Model):
    __tablename__ = "users"
    id = db.Column('user_id',db.Integer , primary_key=True)
    todos = db.relationship('Todo' , backref='user',lazy='dynamic')
 
 
class Todo(db.Model):
    __tablename__ = 'todos'
    id = db.Column('todo_id', db.Integer, primary_key=True)
 
    user_id = db.Column(db.Integer, db.ForeignKey('users.user_id'))

The user_id field in the Todo model class was initialized as a foreign key, so that Flask-SQLAlchemy knows that this field will link to a user.

In the User model class we defined a one-to-many relationship using the db.relationship field. With this relationship in place, it becomes very easy to get all the ‘to do’ items of a user by accessing the user.todo member. The first argument to db.relationship indicates the ‘many’ side of a relationship. The backref is a simple way to also declare a new property on the Todo class. You can then also use todo.user to get to the user who created the ‘to do’ item. The lazy attribute defines when SQLAlchemy will load the data from the database.

The next change that we need to make is to persist User information when a Todo item is persisted. Update the new view as shown below.

@app.route('/new', methods=['GET', 'POST'])
@login_required
def new():
    if request.method == 'POST':
        if not request.form['title']:
            flash('Title is required', 'error')
        elif not request.form['text']:
            flash('Text is required', 'error')
        else:
            todo = Todo(request.form['title'], request.form['text'])
            todo.user = g.user
            db.session.add(todo)
            db.session.commit()
            flash('Todo item was successfully created')
            return redirect(url_for('index'))
    return render_template('new.html')

In the index view we should only see our own ‘to do’ items. Update the index view as shown below.

@app.route('/')
@login_required
def index():
    return render_template('index.html',
        todos=Todo.query.filter_by(user_id = g.user.id).order_by(Todo.pub_date.desc()).all()
    )

The last change that we need to make is to update the show_or_update view such that any user can view other users’ ‘to do’ items but they can’t update them. Replace the show_or_update code with the code shown below.

@app.route('/todos/<int:todo_id>', methods = ['GET' , 'POST'])
@login_required
def show_or_update(todo_id):
    todo_item = Todo.query.get(todo_id)
    if request.method == 'GET':
        return render_template('view.html',todo=todo_item)
    if todo_item.user.id == g.user.id:
        todo_item.title = request.form['title']
        todo_item.text  = request.form['text']
        todo_item.done  = ('done.%d' % todo_id) in request.form
        db.session.commit()
        return redirect(url_for('index'))
    flash('You are not authorized to edit this todo item','error')
    return redirect(url_for('show_or_update',todo_id=todo_id))

Commit the changes to the local Git repository.

$ git commit -am "added User Todo relationship"

Step 12 : Going live

Now that we have made all the changes, it is time to go live with our ‘to do’ application. To publish your app on OpenShift, just push the code to the application gear.

$ git push

Your application will be accessible at http://todo-domainname.rhcloud.com

Conclusion

In this blog post, we covered how developers can use the Flask-Login extension to add user authentication in their applications and deploy them to OpenShift. In the next blog post, we will further extend the application with another Flask extension. If you are looking to host your Python application, then give OpenShift a try.

Next Steps

Categories
Python
Tags
Comments are closed.