The Flask Mega-Tutorial, Part V: User Logins (2012)

Posted by
on under

(Great news! There is a new version of this tutorial!)

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.

An Update Regarding the State of OpenID

It's been more than three years ago that I wrote this article. Back then OpenID seemed like a nice authentication method that was gaining a lot of traction, but in 2015 there are better alternatives, and OpenID is not as widely deployed as it used to be.

I do not have plans to update this tutorial in the near future, as I have written extensively about other authentication methods elsewhere. When you follow this tutorial keep in mind that Google, which was the most prominent OpenID provider in 2012, has dropped support for this protocol completely. My recommendation is to use a Yahoo account to test OpenID in this tutorial. I have a few personal projects that still use OpenID and I use Yahoo as a provider with good results.

As far as real-world authentication, I do not think it is a good idea to use OpenID, given the lack of support. I have a few resources for you that can help you create a more modern authentication experience:

  • My Flask book covers a traditional username and password implementation, complete with user registration, password reminders and resets.
  • My OAuth Authentication with Flask blog article describes in detail how to implement OAuth authentication, which has much wider support than OpenID. With this method you can implement "Login with Facebook" type functionality. The article demonstrates how to login with Facebook and Twitter. Others, such as Google, LinkedIn, etc. can be implemented easily with the same technique.

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_login import LoginManager
from flask_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 properties and methods to be implemented in our User class. Outside of these 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')

    @property
    def is_authenticated(self):
        return True

    @property
    def is_active(self):
        return True

    @property
    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 property 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 property should return True for users unless they are inactive, for example because they have been banned.

The is_anonymous property 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_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

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

217 comments
  • #101 Cloom said

    Thanks for an awesome tutorial! I found it while looking to learn about flask with a view to comparing it against node.js. Still working though it, but I certainly haven't found any node.js tutorials that match the level of this example.

    One note: I got the following error when attempting to run run.py:
    File "./run.py", line 2, in <module>
    from app import app
    File "/home/colmmchugh/python/microblog/app/init.py", line 6, in <module>
    from config import basedir
    ImportError: cannot import name basedir

    To fix this (for me), I made the following changes:
    1) Add this to config.py
    BASEDIR = basedir
    2) Access BASEDIR via app.config in init.py:
    oid = OpenID(app, os.path.join(app.config['BASEDIR'], 'tmp))

    (& remove the 'from config import basedir' statement from init.py)

    I realize this is almost two years later, so a lot has probably changed but in case anyone else has a similar issue. And thanks again!

  • #102 Miguel Grinberg said

    @Cloom: your config.py should have the following at the top:

    import os
    basedir = os.path.abspath(os.path.dirname(file))

    This is what makes basedir available for import in other modules.

  • #103 Travis said

    I seem to be having some issues with the call to
    return oid.try_login(form.openid.data, ask_for = ['nickname', 'email'])

    It is throwing a TypeError: 'NoneType' object is not iterable. At first I thought maybe form.openid was actually NULL, but I used flash to output what was being passed to try_login() and it was what I was expecting.

  • #104 Miguel Grinberg said

    @Travis: I need to see the complete stack trace of the error.

  • #105 Travis said

    The stack trace is as follows:
    Traceback (most recent call last):
    File "/Users/Tr_Heath20/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1701, in call
    return self.wsgi_app(environ, start_response)
    File "/Users/Tr_Heath20/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1689, in wsgi_app
    response = self.make_response(self.handle_exception(e))
    File "/Users/Tr_Heath20/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1687, in wsgi_app
    response = self.full_dispatch_request()
    File "/Users/Tr_Heath20/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1360, in full_dispatch_request
    rv = self.handle_user_exception(e)
    File "/Users/Tr_Heath20/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1358, in full_dispatch_request
    rv = self.dispatch_request()
    File "/Users/Tr_Heath20/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1344, in dispatch_request
    return self.view_functionsrule.endpoint
    File "/Users/Tr_Heath20/microblog/flask/lib/python2.7/site-packages/flask_openid.py", line 480, in decorated
    return f(args, *kwargs)
    File "/Users/Tr_Heath20/microblog/app/views.py", line 51, in login
    return oid.try_login(form.openid.data, ask_for = ['nickname', 'email'])
    File "/Users/Tr_Heath20/microblog/flask/lib/python2.7/site-packages/flask_openid.py", line 518, in try_login
    for key in ask_for_optional:
    TypeError: 'NoneType' object is not iterable

  • #106 Sexy Naruto said

    @Travis
    there is bug in OPENID extension , in try_login function...
    except ask_for there is also ask_for_optional parameter (which should be optional lol but obviously it doesnt work if u doesnt add to it atleast empty list)
    so to make it work u must do
    return oid.try_login(form.openid.data, ask_for = ['nickname', 'email'],ask_for_optional=[])
    here is openid's doc http://packages.python.org/Flask-OpenID/

    and here is traceback:
    Traceback (most recent call last):
    File "/home/gaucan/webdev/microblog2/flask/lib/python2.7/site-packages/flask/app.py", line 1836, in call
    return self.wsgi_app(environ, start_response)
    File "/home/gaucan/webdev/microblog2/flask/lib/python2.7/site-packages/flask/app.py", line 1820, in wsgi_app
    response = self.make_response(self.handle_exception(e))
    File "/home/gaucan/webdev/microblog2/flask/lib/python2.7/site-packages/flask/app.py", line 1403, in handle_exception
    reraise(exc_type, exc_value, tb)
    File "/home/gaucan/webdev/microblog2/flask/lib/python2.7/site-packages/flask/app.py", line 1817, in wsgi_app
    response = self.full_dispatch_request()
    File "/home/gaucan/webdev/microblog2/flask/lib/python2.7/site-packages/flask/app.py", line 1477, in full_dispatch_request
    rv = self.handle_user_exception(e)
    File "/home/gaucan/webdev/microblog2/flask/lib/python2.7/site-packages/flask/app.py", line 1381, in handle_user_exception
    reraise(exc_type, exc_value, tb)
    File "/home/gaucan/webdev/microblog2/flask/lib/python2.7/site-packages/flask/app.py", line 1475, in full_dispatch_request
    rv = self.dispatch_request()
    File "/home/gaucan/webdev/microblog2/flask/lib/python2.7/site-packages/flask/app.py", line 1461, in dispatch_request
    return self.view_functionsrule.endpoint
    File "/home/gaucan/webdev/microblog2/flask/lib/python2.7/site-packages/flask_openid.py", line 480, in decorated
    return f(args, *kwargs)
    File "/home/gaucan/webdev/microblog2/app/views.py", line 43, in login
    return oid.try_login(form.openid.data,ask_for=['nickname','email'])
    File "/home/gaucan/webdev/microblog2/flask/lib/python2.7/site-packages/flask_openid.py", line 518, in try_login
    for key in ask_for_optional:
    TypeError: 'NoneType' object is not iterable

  • #107 Travis said

    Thanks! That fixed that problem. I'm getting a valid id_res from the providers, however is_authenticated() never is set to True, it always False.

  • #108 Travis said

    After using a series of Flash() statements I was able to debug it myself. In the views.py file, there is a check to see if resp.email is None or if it is blank, it Flash()'s a error and then returns to the login page. Well, somehow, that return statement was being interpreted as OUTSIDE of that IF. What happened is it would always return to Login without ever running the login_user function.

    To fix it I simply removed the whitespace before the line and redid the whitespace.

  • #109 jason said

    Hello Miguel,

    When I try to log in I get an error. I choose an OpenID provider, it takes me to their site and then when I enter my username and password instead of being redirected back to the index I get the below error.

    app.py"
    return self.wsgi_app(environ, start_response)
    response = self.make_response(self.handle_exception(e))
    response = self.full_dispatch_request()
    rv = self.handle_user_exception(e)
    rv = self.dispatch_request()
    return self.view_functionsrule.endpoint

    flask_openid.py", line 487, in decorated
    openid_response, self.extension_responses))

    TypeError: 'NoneType' object is not callable

    Any ideas? I have spent hours today troubleshooting this and I am kind of stuck.

    Thanks.

  • #110 Miguel Grinberg said

    @jason: based on your error it appears you have not defined an "after_login" handler. This is the function that uses the "oid.after_login" decorator from Flask-OpenID.

  • #111 naren said

    Hi Miguel. Thanks to your posts I have been able to learn about flask with ease. I was wondering if you could next post the procedure to run a sample code in the background indefinitely.

  • #112 jack said

    Thanx miguel. Your tutorials are very helpful.

  • #113 Dan said

    Hi Miguel
    I’m getting a bit confused by alternative approaches to the user login approach.
    For the remember_me functionality:
    Your method seems to be introducing a new attribute to the flask.session object, ie. Session[‘remember_me’]=form.remember_me.data
    The Flask-login docs and another tutorial I’ve seen uses the remember_me argument of the login_user function, ie.
    login_user(user, remember = form.remember_me.data)
    Also with recording the user for the session:
    You used the before_request() | g.user=current_user approach but I have also seen tutorials use
    session['signed'] = True
    session['username']= user.username statements within the login function.

    There seems to be a mix of approach, sometimes using the session object and introducing new attributes to store your user data, and sometimes using the flask-login functions to do it all for you.
    Do you have any comment on the two approaches?
    Dan

  • #114 Miguel Grinberg said

    @Dan: putting the "remember_me" in the session is necessary in my case because the OpenID authentication is asynchronous. At the time I get the form POST request I have to save this value somewhere, so that I can pass it to login_user() later when OpenID invokes the callback route. If you use regular username/password where you can validate the credentials right in the POST request then I would pass it directly to login_user() as you saw elsewhere.

    The g.user bit is really not necessary, you can really access current_user directly, and in fact that is how I'm doing it these days.

    I'm not sure what the signed/username things written to the session are, but in any case, with almost everything in Flask you have the freedom to do things your way, so this is likely a personal preference of the developer who wrote that other tutorial. You should look at that solution, my solution, and any others that you find and then use that to develop the solution that works best for you. That is the Flask way!

  • #115 Dan said

    Thanks Miguel
    Yes, I've moved away from the openID solution so that makes a bit more sense now.
    Thanks for clarifying,
    Dan

  • #116 Elon said

    Hi Miguel, I'm following along with this quite nicely without many problems; thanks for being clear and instructive. Can you tell me: is it important if the order of your view definitions matters? i.e. could I have def index() be at the very bottom if I wanted, and would that cause any unintended problems?

  • #117 Miguel Grinberg said

    @Elon: the order in which you define the view functions does not matter at all. In fact you can also define your view functions in multiple modules and nothing changes still.

  • #118 Sam said

    Naive question about openauth: On the login page, I can specify any openauth provider. What's to prevent me from creating a fake openauth provider that returns someone elses email in "resp" as a way to hack into someones account? When you retrieve the user from the database, don't you have to check that the domain of the email address returned matches the openauth domain? Or is that built in?

    Great articles, learning a lot!

  • #119 anand said

    I am having issues while importing the extension
    I get the following error:

    Traceback (most recent call last):
    File "run.py", line 2, in <module>
    from app import app
    File "/Users/anandsharma/Documents/Dev/python/flasktut/app/init.py", line 14, in <module>
    from app import views, models, lm
    File "/Users/anandsharma/Documents/Dev/python/flasktut/app/views.py", line 3, in <module>
    from app import app, db, lm, oid
    ImportError: cannot import name lm

    Can anyone help? I was able to run everything until this

  • #120 Miguel Grinberg said

    @Sam: After you enter the OpenID the application redirects to a page on the provider's web site where the user must authenticate. If you can't login with the provider you will not be given access to the application.

  • #121 Miguel Grinberg said

    @anand: the "lm" symbol is the LoginManager instance, create in app/init.py. Check this file, it must be missing there.

  • #122 Gaurav Gupta said

    Hi Miguel, while following this login tutorial, I am getting below error :

    Traceback (most recent call last):
    File "./run.py", line 3, in <module>
    from app import app
    File "/Users/gagupta/gaurav/flask_learning/microblog/app/init.py", line 15, in <module>
    from app import views, models
    File "/Users/gagupta/gaurav/flask_learning/microblog/app/views.py", line 3, in <module>
    from app import app, db, lm, oid
    ImportError: cannot import name lm

    I have tried checking above solution given to anand but still I am not able to find any issues in code. Can you please guide

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

    app = Flask(name)
    app.config.from_object('config')
    db = SQLAlchemy(app)

    from app import views, models

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

  • #123 Miguel Grinberg said

    @Gaurav: you have circular dependencies. Move the line "from app import views, models" in your init.py to the bottom (after the LoginManager and OpenID objects are created) and then you will be fine.

  • #124 Joel Clipperton said

    I'm not sure what's going on here. Only the AIM and Flickr OpenID options work when trying to log in. The others just return to the microblog login page.

  • #125 Nandhini said

    Greetings again Miguel, first and foremost thanks so much for providing this resource.
    once i completed the login view,and try to execute ./run.py i get the following error:
    Traceback (most recent call last):
    File "./run.py", line 2, in <module>
    from app import app
    File "/home/user/microblog/app/init.py", line 9, in <module>
    lm.init_app(app)
    NameError: name 'app' is not defined
    could you please help me with this.
    thanks -Nandhini