Understanding Flask-Login Tokens Tutorial

Posted November 20, 2012 in development

When I was trying to implement Flask-Login into my first Flask application I had difficulty understanding from the flask-login docs how to implement the Authorization Tokens required to use the Remember Me feature. I found another flask plugin package called Flask-Security, which I used to base the below example off of. This example extends the basic example that the Flask-Login Documentation gives and adds support for the get_auth_token and the token_loader methods.

What is what?

  • Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions.  Simply Flask is a lightweight framework used for serving dynamic web pages (applications) on the internet.  More importantly Flask is Fun.  This example uses Flask version 0.9.
  • Flask-Lofin is a Python module which helps add user session management for Flask.  This example uses Flask-Login version 0.1.3.
  • itsdangerous is a Python module which helps securely sign cookies in this example.  This example uses itsdangerous version 0.17.

lask-Login Alternative Tokens

In order to implement Flask-Login alternative tokens, which are recommended by Flask-Login you need to implement two methods, get_auth_token in your User Class and token_loader callback method.

get_auth_token

In our user class we need to implement a get_auth_token method which will return a secure token string which will be stored in a cookie on the users computer.  The cookie will be used when a user returns to the your site.  Flask-Login will load the token and ask us to decode and return a User class with the token_loader function.  Because the token is stored on the users computer we need to make it secure.  We will use itsdangerous to do that for us.  We will combine the username and hashed password into a list then pass that to itsdangerous to encrypt using our flask secret_key.

We store the password hash so that if a user is logged in on multiple computers/browsers and changes their password, it will invalidate the cookie token.

class User(UserMixin):
    def get_auth_token(self):
        """
        Encode a secure token for cookie
        """
        data = [str(self.id), self.password]
        return login_serializer.dumps(data)

token_loader

The token_loader callback needs to take the token string passed to it and decode it.  We also use this method to enforce the expiration date of the token as explained in the code below.  Because we stored both the username and password hash in the token, once we decode it we need to check and that the username and password match.

@login_manager.token_loader
def load_token(token):
    """
    Flask-Login token_loader callback. 
    The token_loader function asks this function to take the token that was 
    stored on the users computer process it to check if its valid and then 
    return a User Object if its valid or None if its not valid.
    """

    #The Token itself was generated by User.get_auth_token.  So it is up to 
    #us to known the format of the token data itself.  

    #The Token was encrypted using itsdangerous.URLSafeTimedSerializer which 
    #allows us to have a max_age on the token itself.  When the cookie is stored
    #on the users computer it also has a exipry date, but could be changed by
    #the user, so this feature allows us to enforce the exipry date of the token
    #server side and not rely on the users cookie to exipre. 
    max_age = app.config["REMEMBER_COOKIE_DURATION"].total_seconds()

    #Decrypt the Security Token, data = [username, hashpass]
    data = login_serializer.loads(token, max_age=max_age)

    #Find the User
    user = User.get(data[0])

    #Check Password and return user or None
    if user and data[1] == user.password:
        return user
    return None

Security

  • Its important to note that the Flask Session Cookie and the Flask-Login Cookie are vulnerable to attack.  Although the cookies are encrypted and relatively safe from attack a user who is sniffing network traffic can easily copy the cookies and impersonate the user.  The only way to prevent this kind of attack is to use secure sockets (https) when sending back and forth the cookies.  This example does not cover that scope.
  • This example does use a password salt and hash to store the users passwords.   It is important to never store a users plain text password, that way if your system is ever compromised someone can still not access users data even with the stored password hash. Wikipedia), readwrite.com

Complete Working Example

# flask_login_app.py
"""
flask_login_app
==============

An example of how to implement get_auth_token and token_loader of Flask-Login.
This example builds on the excellent docs of flask-login which clearly explains
how to setup basic session based Authentication. 

This example uses the python module itsdangerous to handle the encryption and
decryption of the remember me token. 

Flask Version: 0.9
Flask-Login Version: 0.1.3
itsdangerous Version: 0.17

Author: Christopher Ross
Site: http://blog.thecircuitnerd.com/flask-login-tokens/
Version: 0.1a
"""
from datetime import timedelta
import md5

from flask import Flask, request, redirect, render_template
from flask_login import (LoginManager, login_required, login_user, 
                         current_user, logout_user, UserMixin)
from itsdangerous import URLSafeTimedSerializer

app = Flask(__name__)
app.debug = True
app.secret_key = "a_random_secret_key_$%#!@"

#Login_serializer used to encryt and decrypt the cookie token for the remember
#me option of flask-login
login_serializer = URLSafeTimedSerializer(app.secret_key)

#Flask-Login Login Manager
login_manager = LoginManager()

class User(UserMixin):
    """
    User Class for flask-Login
    """
    def __init__(self, userid, password):
        self.id = userid
        self.password = password

    def get_auth_token(self):
        """
        Encode a secure token for cookie
        """
        data = [str(self.id), self.password]
        return login_serializer.dumps(data)

    @staticmethod
    def get(userid):
        """
        Static method to search the database and see if userid exists.  If it 
        does exist then return a User Object.  If not then return None as 
        required by Flask-Login. 
        """
        #For this example the USERS database is a list consisting of 
        #(user,hased_password) of users.
        for user in USERS:
            if user[0] == userid:
                return User(user[0], user[1])
        return None

def hash_pass(password):
    """
    Return the md5 hash of the password+salt
    """
    salted_password = password + app.secret_key
    return md5.new(salted_password).hexdigest()

@login_manager.user_loader
def load_user(userid):
    """
    Flask-Login user_loader callback.
    The user_loader function asks this function to get a User Object or return 
    None based on the userid.
    The userid was stored in the session environment by Flask-Login.  
    user_loader stores the returned User object in current_user during every 
    flask request. 
    """
    return User.get(userid)

@login_manager.token_loader
def load_token(token):
    """
    Flask-Login token_loader callback. 
    The token_loader function asks this function to take the token that was 
    stored on the users computer process it to check if its valid and then 
    return a User Object if its valid or None if its not valid.
    """

    #The Token itself was generated by User.get_auth_token.  So it is up to 
    #us to known the format of the token data itself.  

    #The Token was encrypted using itsdangerous.URLSafeTimedSerializer which 
    #allows us to have a max_age on the token itself.  When the cookie is stored
    #on the users computer it also has a exipry date, but could be changed by
    #the user, so this feature allows us to enforce the exipry date of the token
    #server side and not rely on the users cookie to exipre. 
    max_age = app.config["REMEMBER_COOKIE_DURATION"].total_seconds()

    #Decrypt the Security Token, data = [username, hashpass]
    data = login_serializer.loads(token, max_age=max_age)

    #Find the User
    user = User.get(data[0])

    #Check Password and return user or None
    if user and data[1] == user.password:
        return user
    return None

@app.route("/logout/")
def logout_page():
    """
    Web Page to Logout User, then Redirect them to Index Page.
    """
    logout_user()
    return redirect("/")

@app.route("/login/", methods=["GET", "POST"])
def login_page():
    """
    Web Page to Display Login Form and process form. 
    """
    if request.method == "POST":
        user = User.get(request.form['username'])

        #If we found a user based on username then compare that the submitted
        #password matches the password in the database.  The password is stored
        #is a slated hash format, so you must hash the password before comparing
        #it.
        if user and hash_pass(request.form['password']) == user.password:
            login_user(user, remember=True)
            return redirect(request.args.get("next") or "/")        

    return render_template("login.html")

@app.route("/")
def index_page():
    """
    Web Page to display The Main Index Page
    """
    user_id = (current_user.get_id() or "No User Logged In")
    return render_template("index.html", user_id=user_id)

@app.route("/restricted/")
@login_required
def restricted_page():
    """
    web page which is restricted and requires the user to be logged in. 
    """
    #this is just to display the username in the template not required as part
    #of any Flask-Login requirements. 
    user_id = (current_user.get_id() or "No User Logged In")
    return render_template("restricted.html", user_id=user_id)

if __name__ == "__main__":

    #Create a quick list of users (username, password).  The password is stored
    #as a md5 hash that has also been salted.  You should never store the users
    #password and only store the password after it has been hashed. 
    USERS = (("user1", hash_pass("pass1")),
          ("user2", hash_pass("pass2"))
          )

    #Change the duration of how long the Remember Cookie is valid on the users
    #computer.  This can not really be trusted as a user can edit it. 
    app.config["REMEMBER_COOKIE_DURATION"] = timedelta(days=14)

    #Tell the login manager where to redirect users to display the login page
    login_manager.login_view = "/login/"
    #Setup the login manager. 
    login_manager.setup_app(app)    

    #Run the flask Development Server
    app.run()