The Flask Mega-Tutorial, Part V: User Logins

This is the fifth article in the series in which I document my experience writing web applications in Python using the Flask microframework.

The goal of the tutorial series is to develop a decently featured microblogging application that demonstrating total lack of originality I have decided to call microblog.

NOTE: This article was revised in September 2014 to be in sync with current versions of Python and Flask.

Here is an index of all the articles in the series that have been published to date:

Recap

In the previous chapter of the series we created our database and learned how to populate it with users and posts, but we haven't hooked up any of that into our app yet. And two chapters ago we've seen how to create web forms and left with a fully implemented login form.

In this article we are going to build on what we learned about web forms and databases and write our user login system. At the end of this tutorial our little application will register new users and log them in and out.

To follow this chapter along you need to have the microblog app as we left it at the end of the previous chapter. Please make sure the app is installed and running.

Configuration

As in previous chapters, we start by configuring the Flask extensions that we will use. For the login system we will use two extensions, Flask-Login and Flask-OpenID. Flask-Login will handle our users logged in state, while Flask-OpenID will provide authentication. These extensions are configured as follows (file app/__init__.py):

import os
from flask.ext.login import LoginManager
from flask.ext.openid import OpenID
from config import basedir

lm = LoginManager()
lm.init_app(app)
oid = OpenID(app, os.path.join(basedir, 'tmp'))

The Flask-OpenID extension requires a path to a temp folder where files can be stored. For this we provide the location of our tmp folder.

Python 3 Compatiblity

Unfortunately version 1.2.1 of Flask-OpenID (the current official version) does not work well with Python 3. Check what version you have by running the following command:

$ flask/bin/pip freeze

If you have a version newer than 1.2.1 then the problem is likely resolved, but if you have 1.2.1 and are following this tutorial on Python 3 then you have to install the development version from GitHub:

$ flask/bin/pip uninstall flask-openid
$ flask/bin/pip install git+git://github.com/mitsuhiko/flask-openid.git

Note that you need to have git installed for this to work.

Revisiting our User model

The Flask-Login extension expects certain methods to be implemented in our User class. Outside of these methods there are no requirements for how the class has to be implemented.

Below is our Flask-Login friendly User class (file app/models.py):

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    nickname = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    posts = db.relationship('Post', backref='author', lazy='dynamic')

    def is_authenticated(self):
        return True

    def is_active(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        try:
            return unicode(self.id)  # python 2
        except NameError:
            return str(self.id)  # python 3

    def __repr__(self):
        return '<User %r>' % (self.nickname)

The is_authenticated method has a misleading name. In general this method should just return True unless the object represents a user that should not be allowed to authenticate for some reason.

The is_active method should return True for users unless they are inactive, for example because they have been banned.

The is_anonymous method should return True only for fake users that are not supposed to log in to the system.

Finally, the get_id method should return a unique identifier for the user, in unicode format. We use the unique id generated by the database layer for this. Note that due to the differences in unicode handling between Python 2 and 3 we have to provide two alternative versions of this method.

User loader callback

Now we are ready to start implementing the login system using the Flask-Login and Flask-OpenID extensions.

First, we have to write a function that loads a user from the database. This function will be used by Flask-Login (file app/views.py):

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

Note how this function is registered with Flask-Login through the lm.user_loader decorator. Also remember that user ids in Flask-Login are always unicode strings, so a conversion to an integer is necessary before we can send the id to Flask-SQLAlchemy.

The login view function

Next let's update our login view function (file app/views.py):

from flask import render_template, flash, redirect, session, url_for, request, g
from flask.ext.login import login_user, logout_user, current_user, login_required
from app import app, db, lm, oid
from forms import LoginForm
from models import User

@app.route('/login', methods=['GET', 'POST'])
@oid.loginhandler
def login():
    if g.user is not None and g.user.is_authenticated():
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        session['remember_me'] = form.remember_me.data
        return oid.try_login(form.openid.data, ask_for=['nickname', 'email'])
    return render_template('login.html', 
                           title='Sign In',
                           form=form,
                           providers=app.config['OPENID_PROVIDERS'])

Notice we have imported several new modules, some of which we will use later.

The changes from our previous version are very small. We have added a new decorator to our view function. The oid.loginhandler tells Flask-OpenID that this is our login view function.

At the top of the function body we check if g.user is set to an authenticated user, and in that case we redirect to the index page. The idea here is that if there is a logged in user already we will not do a second login on top.

The g global is setup by Flask as a place to store and share data during the life of a request. As I'm sure you guessed by now, we will be storing the logged in user here.

The url_for function that we used in the redirect call is defined by Flask as a clean way to obtain the URL for a given view function. If you want to redirect to the index page you may very well use redirect('/index'), but there are very good reasons to let Flask build URLs for you.

The code that runs when we get a data back from the login form is also new. Here we do two things. First we store the value of the remember_me boolean in the flask session, not to be confused with the db.session from Flask-SQLAlchemy. We've seen that the flask.g object stores and shares data though the life of a request. The flask.session provides a much more complex service along those lines. Once data is stored in the session object it will be available during that request and any future requests made by the same client. Data remains in the session until explicitly removed. To be able to do this, Flask keeps a different session container for each client of our application.

The oid.try_login call in the following line is the call that triggers the user authentication through Flask-OpenID. The function takes two arguments, the openid given by the user in the web form and a list of data items that we want from the OpenID provider. Since we defined our User class to include nickname and email, those are the items we are going to ask for.

The OpenID authentication happens asynchronously. Flask-OpenID will call a function that is registered with the oid.after_login decorator if the authentication is successful. If the authentication fails the user will be taken back to the login page.

The Flask-OpenID login callback

Here is our implementation of the after_login function (file app/views.py):

@oid.after_login
def after_login(resp):
    if resp.email is None or resp.email == "":
        flash('Invalid login. Please try again.')
        return redirect(url_for('login'))
    user = User.query.filter_by(email=resp.email).first()
    if user is None:
        nickname = resp.nickname
        if nickname is None or nickname == "":
            nickname = resp.email.split('@')[0]
        user = User(nickname=nickname, email=resp.email)
        db.session.add(user)
        db.session.commit()
    remember_me = False
    if 'remember_me' in session:
        remember_me = session['remember_me']
        session.pop('remember_me', None)
    login_user(user, remember = remember_me)
    return redirect(request.args.get('next') or url_for('index'))

The resp argument passed to the after_login function contains information returned by the OpenID provider.

The first if statement is just for validation. We require a valid email, so if an email was not provided we cannot log the user in.

Next, we search our database for the email provided. If the email is not found we consider this a new user, so we add a new user to our database, pretty much as we have learned in the previous chapter. Note that we handle the case of a missing nickname, since some OpenID providers may not have that information.

After that we load the remember_me value from the Flask session, this is the boolean that we stored in the login view function, if it is available.

Then we call Flask-Login's login_user function, to register this is a valid login.

Finally, in the last line we redirect to the next page, or the index page if a next page was not provided in the request.

The concept of the next page is simple. Let's say you navigate to a page that requires you to be logged in, but you aren't just yet. In Flask-Login you can protect views against non logged in users by adding the login_required decorator. If the user tries to access one of the affected URLs then it will be redirected to the login page automatically. Flask-Login will store the original URL as the next page, and it is up to us to return the user to this page once the login process completed.

For this to work Flask-Login needs to know what view logs users in. We can configure this in the app's module initializer (file app/__init__.py):

lm = LoginManager()
lm.init_app(app)
lm.login_view = 'login'

The g.user global

If you were paying attention, you will remember that in the login view function we check g.user to determine if a user is already logged in. To implement this we will use the before_request event from Flask. Any functions that are decorated with before_request will run before the view function each time a request is received. So this is the right place to setup our g.user variable (file app/views.py):

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

This is all it takes. The current_user global is set by Flask-Login, so we just put a copy in the g object to have better access to it. With this, all requests will have access to the logged in user, even inside templates.

The index view

In a previous chapter we left our index view function using fake objects, because at the time we did not have users or posts in our system. Well, we have users now, so let's hook that up:

@app.route('/')
@app.route('/index')
@login_required
def index():
    user = g.user
    posts = [
        { 
            'author': {'nickname': 'John'}, 
            'body': 'Beautiful day in Portland!' 
        },
        { 
            'author': {'nickname': 'Susan'}, 
            'body': 'The Avengers movie was so cool!' 
        }
    ]
    return render_template('index.html',
                           title='Home',
                           user=user,
                           posts=posts)

There are only two changes to this function. First, we have added the login_required decorator. This will ensure that this page is only seen by logged in users.

The other change is that we pass g.user down to the template, instead of the fake object we used in the past.

This is a good time to run the application.

When you navigate to http://localhost:5000 you will instead get the login page. Keep in mind that to login with OpenID you have to use the OpenID URL from your provider. You can use one of the OpenID provider links below the URL text field to generate the correct URL for you.

As part of the login process you will be redirected to your provider's web site, where you will authenticate and authorize the sharing of some information with our application (just the email and nickname that we requested, no passwords or other personal information will be exposed).

Once the login is complete you will be taken to the index page, this time as a logged in user.

Feel free to try the remember_me checkbox. With this option enabled you can close and reopen your web browser and will continue to be logged in.

Logging out

We have implemented the log in, now it's time to add the log out.

The view function for logging out is extremely simple (file app/views.py):

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

But we are also missing a link to logout in the template. We are going to put this link in the top navigation bar which is in the base layout (file app/templates/base.html):

<html>
  <head>
    {% if title %}
    <title>{{ title }} - microblog</title>
    {% else %}
    <title>microblog</title>
    {% endif %}
  </head>
  <body>
    <div>Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        {% if g.user.is_authenticated() %}
        | <a href="{{ url_for('logout') }}">Logout</a>
        {% endif %}
    </div>
    <hr>
    {% with messages = get_flashed_messages() %}
    {% if messages %}
    <ul>
    {% for message in messages %}
        <li>{{ message }} </li>
    {% endfor %}
    </ul>
    {% endif %}
    {% endwith %}
    {% block content %}{% endblock %}
  </body>
</html>

Note how easy it is to do this. We just needed to check if we have a valid user set in g.user and if we do we just add the logout link. We have also used the opportunity to use url_for in our template.

Final words

We now have a fully functioning user login system. In the next chapter we will be creating the user profile page and will be displaying user avatars on them.

In the meantime, here is the updated application code including all the changes in this article:

Download microblog-0.5.zip.

See you next time!

Miguel

153 comments

  • #1 buxur said :

    Hey miguel so rush but when are you going to post the next chapter? Thx

  • #2 Miguel Grinberg said :

    buxur, I need a few more days to complete it. I was hoping I would get into a rhythm of one article per month, but I'm a bit behind.

  • #3 JonoB said :

    There are a few mistakes on this page: 1. its flask.ext. not flaskext. 2. You did not define a login_view from the LoginManager in __init-.py__ as follows lm.login_view = 'login'

  • #4 Miguel Grinberg said :

    @JonoB: thanks for your detailed review. Regarding #1 I confirmed that at least in my installation (as I described in the first tutorial post) I have login.py inside site-packages/flaskext, not site-packages/flask/ext. Regarding #2 you are absolutely correct. I have the change in the code, but forgot to mention it. This is now corrected.

  • #5 drew said :

    Greetings again Minguel, first and foremost thanks so much for providing this resource. I was hoping you might have insight as to why i get this error: /microblog/app/views.py", line 2, in <module> from flaskext.login import login_user, logout_user, current_user, login_required ImportError: No module named login Secondly, I’m new to web design and have a great interest in flask, are you aware of any resources which might better help me understand how to get off the ground?

  • #6 Miguel Grinberg said :

    @drew: the error likely means that the flask-login module isn't installed, or that it isn't installed in the right place. As far as learning resources, there isn't much out there that is specific to Flask, unfortunately. Since Flask is actually a pretty thin layer on top of regular Python, learning about general Python programming is very helpful, as is to learn about the HTTP protocol. Good luck!

  • #7 Jaco said :

    Hello there. Firstly let me concur. Thank you for highly informative, well written and accurate tutorial. Really enjoying finding my way through it. I also got the "No module named login" import error. Had a look, found the following. Under site-packages there is now a flask_login module. Changed the code to "from flask_login import LoginManager" and it worked. Looking forward to next installment. Thank you.

  • #8 Miguel Grinberg said :

    @Jaco: for some reason the flask-login sources got installed in a different place for you than for me. I have a login.py file inside "site-packages/flaskext", and this seems to agree with the Flask-Login documentation. In any case, I don't think it matters much, as long as you can import the module the rest should work in the same way. Thanks!

  • #9 jaco_ said :

    Hello again Miguel. All was going great up to this point. I checked the file flask_login and it contains the docs from the flask login extension as well as the LoginManager class. I'm running on Ubuntu 12.04 btw. However adding the view (with decorator) @lm.user_loader I get NameError 'lm' is not defined. Error occurs in last line of __init__.py when importing views. Not sure where to look now?

  • #10 jaco said :

    Hello Miguel. Doing some research i found this link http://flask.pocoo.org/docs/extensiondev/#extension-import-transition = right at the bottom there is a discussion on Extension Import Transition that you might find handy. Explains the problem I encountered. Still trying to figure out the rest though. Thanks again for the efforts and informatinve tutorials.

  • #11 jaco said :

    Hello again. Apologies for all the comments. Happy if you want to remove some. My error above "'lm' is not defined. Error occurs in last line of __init__.py when importing views." refers. I had the debug server up and running and it crashed as soon as I entered the @lm.user_loader (because I have not added the import lm at the top yet. Maybe you could just 'add' that import where you show that view? Anyway. looking forward to the next installments. Thank you.

  • #12 Miguel Grinberg said :

    @jaco: do you have a "from app import lm" at the top of views.py?

  • #13 Siros said :

    Thank you, a little notice here , flaskext.login , flaskext.openid does not work any more. it have to be flask.ext.login , flask.ext.openid. Thanks again for the details tut

  • #14 pod said :

    Flask 0.8 or later version from flaskext.login import LoginManager from flaskext.openid import OpenID change -> from flask.ext.login import LoginManager from flask.ext.openid import OpenID

  • #15 Andrey said :

    After the last listing you say: "We have also used the opportunity to use url_for in our template." Actually, you haven't. At least in snippet on this page, as zip archive contains correct version. Also, as it was already said, in latest versions of flask extension import system has been changed. This import problems are beacuse you and your readers are using different versions of flask and other modules. And they will be eliminated, if you require readers to download the same module versions as you have. This is very easy: just run "pip freeze > requirements.txt" and publish resulted file. Your users can recreate exact environment with "pip install -r requirements.txt". You could publish req file in the first step of your tutorial. Many thanks for this tutorial series!

  • #16 Miguel Grinberg said :

    Andrey, thanks for pointing out the url_for problem, I have corrected the template. As for the module import problems I don't think requiring a specific version of Flask and its extensions is the way to go, it does not make sense to me to require people to run old software just because that's what works for me. If you go read the documentation for the most recent versions of some of these extensions they still indicate flaskext is their root namespace. I know this is going to change at some point given that flaskext has been deprecated, but at least up until a couple of weeks ago all the most recent versions of the extensions I'm using in my project worked with the code as I have it published (on Windows, the platform I spend most of my time on, I guess I should check the others as well but I haven't). I routinely upgrade my flask virtual environment and try to keep things working on the latest stuff. Thanks for your comment.

  • #17 Edwin said :

    I'm getting an "ImportError: cannot import name lm"...Can anyone help me with that?. Thanks.

  • #18 Miguel Grinberg said :

    @Edwin: did you read all the comments in this article regarding the location of flask-login?

  • #19 Edwin said :

    Ok, sorry, just realized comment #12 was there...that was the issue. Thanks! Excellent tutorial by the way. Congrats.

  • #20 Adam said :

    I'm having the same problem as Jaco. Although I have successfully run... pip install flask-login All three of the following produce the "lm is not defined" error: from flask_login import LoginManager from flaskext.login import LoginManager from flask.ext.login import LoginManager despite having the same code in my __init__ and views.

  • #21 Miguel Grinberg said :

    @Adam: are you sure you are running run.py in the virtualenv's Python interpreter? The more likely cause for your problem is that you installed flask-login in a virtualenv but are running the application under another, or the global one.

  • #22 Don said :

    I'm at User Logins and so far this is one of the best tutorials I've ever done. I mean that. You took the time to do this right and it's great! Thank you.

  • #23 Steven Elliott said :

    setup_app() is deprecated. Instead: lm = LoginManager() lm.init_app(app) oid = OpenID(app, os.path.join(basedir, 'tmp'))

  • #24 Peter said :

    Hi Miguel. Awesome blog, but I get an error when I try to use OpenID to log in: AttributeError: 'NoneType' object has no attribute 'split' nickname = resp.email.split('@')[0] Looks like OpenID is not returning an email address, which is a bit of an issue. Any suggestions?

  • #25 joshua said :

    Good job! There are a few mistakes on this page: 1. from flaskext.login import LoginManager from flaskext.openid import OpenID 2. from flaskext.login import login_user, logout_user, current_user, login_required

Leave a Comment

Note: all comments are screened before they are published. Thank you for your patience!