How to implement user_loader callback in Flask-Login

20,481

Solution 1

You will need to load the user object from the DB upon every request. The strongest reason for that requirement is that Flask-Login will check the authentication token every time to ensure its continuing validity. The calculation of this token may require parameters stored on the user object.

For example, suppose a user has two concurrent sessions. In one of them, the user changes their password. In subsequent requests, the user must be logged out of the second session and forced to login anew for your application to be secure. Think of the case where the second session is stolen because your user forgot to log out of a computer - you want a password change to immediately fix the situation. You might also want to give your admins the ability to kick a user out.

For such forced logout to happen, the authentication token stored in a cookie must 1) be based in part on the password or something else that changes each time a new password is set; 2) be checked before running any view, against the latest known attributes of the user object - which are stored in the DB.

Solution 2

I do share your concerns Edmond: hitting database each time when one needs to know user's role or name is insane. Best way would be to store your User object in session or even application-wide cache which gets updated from the DB each couple of minutes. I personally use Redis for that (that way website can be run by multiple threads/processes while using single cache entry point). I just make sure Redis is configured with password and non-default port, and any confidential data (like user hashes etc) are stored there in an encrypted form. Cache can be populated by a separate script running on specified interval, or separate thread can be spawned in Flask. Note: Flask-Session can be also configured to use (the same) redis instance to store session data, in that case instance with 'bytes' datatype will be needed, for a regular cache you might often go with instance type which automatically translates bytes into strings (decode_responses=True).

Solution 3

Here is my code, another User as data mapping object provide query_pwd_md5 method.

User login:

@app.route('/users/login', methods=['POST'])
def login():
    # check post.
    uname = request.form.get('user_name')
    request_pwd = request.form.get('password_md5')

    user = User()
    user.id = uname

    try:
        user.check_pwd(request_pwd, BacktestUser.query_pwd_md5(
            uname, DBSessionMaker.get_session()
        ))
        if user.is_authenticated:
            login_user(user)
            LOGGER.info('User login, username: {}'.format(user.id))
            return utils.serialize({'userName': uname}, msg='login success.')
        LOGGER.info('User login failed, username: {}'.format(user.id))
        return abort(401)
    except (MultipleResultsFound, TypeError):
        return abort(401)

User class:

class User(UserMixin):
"""Flask-login user class.
"""

def __init__(self):
    self.id = None
    self._is_authenticated = False
    self._is_active = True
    self._is_anoymous = False

@property
def is_authenticated(self):
    return self._is_authenticated

@is_authenticated.setter
def is_authenticated(self, val):
    self._is_authenticated = val

@property
def is_active(self):
    return self._is_active

@is_active.setter
def is_active(self, val):
    self._is_active = val

@property
def is_anoymous(self):
    return self._is_anoymous

@is_anoymous.setter
def is_anoymous(self, val):
    self._is_anoymous = val

def check_pwd(self, request_pwd, pwd):
    """Check user request pwd and update authenticate status.

    Args:
        request_pwd: (str)
        pwd: (unicode)
    """
    if request_pwd:
        self.is_authenticated = request_pwd == str(pwd)
    else:
        self.is_authenticated = False
Share:
20,481
Edmond Burnett
Author by

Edmond Burnett

Software Engineer who likes code, Linux, and open-source technologies. A prior background in DevOps and Systems. Stuff I'm currently using includes: Python, JavaScript, Node.js, React, Linux, Cassandra, PostgreSQL, VS Code, and Vim.

Updated on July 09, 2022

Comments

  • Edmond Burnett
    Edmond Burnett almost 2 years

    I'm attempting to use Flask and the Flask-Login extension to implement user authentication in a Flask app. The goal is to pull user account information from a database and then log in a user, but I'm getting stuck; however, I've narrowed it down to a particular part of Flask-Login behavior.

    According to the Flask-Login documentation, I need to create a user_loader "callback" function. The actual purpose and implementation of this function has had me confused for a few days now:

    You will need to provide a user_loader callback. This callback is used to reload the user object from the user ID stored in the session. It should take the Unicode ID of a user, and return the corresponding user object. For example:

    @login_manager.user_loader
    def load_user(userid):
        return User.get(userid)
    

    Now, say I want the user to enter a name and password into a form, check against a database, and log in the user. The database stuff works fine and is no problem for me.

    This 'callback' function wants to be passed a user ID #, and return the User object (the contents of which I'm loading from a database). But I don't really get what it's supposed to be checking/doing, since the user IDs are all pulled from the same place anyway. I can 'sort-of' get the callback to work, but it seems messy/hackish and it hits the database with every single resource that the browser requests. I really don't want to check my database in order to download favicon.ico with every page refresh, but flask-login seems like it's forcing this.

    If I don't check the database again, then I have no way to return a User object from this function. The User object/class gets created in the flask route for logging in, and is thus out of scope of the callback.

    What I can't figure out is how to pass a User object into this callback function, without having to hit the database every single time. Or, otherwise figure out how to go about doing this in a more effective way. I must be missing something fundamental, but I've been staring at it for a few days now, throwing all kinds of functions and methods at it, and nothing is working out.

    Here are relevant snippets from my test code. The User class:

    class UserClass(UserMixin):
         def __init__(self, name, id, active=True):
              self.name = name
              self.id = id
              self.active = active
    
         def is_active(self):
              return self.active
    

    The function I made to return the user object to Flask-Login's user_loader callback function:

    def check_db(userid):
    
         # query database (again), just so we can pass an object to the callback
         db_check = users_collection.find_one({ 'userid' : userid })
         UserObject = UserClass(db_check['username'], userid, active=True)
         if userObject.id == userid:
              return UserObject
         else:
              return None
    

    The 'callback', which I don't totally understand (must return the User object, which gets created after pulling from database):

    @login_manager.user_loader
    def load_user(id):
         return check_db(id)
    

    The login route:

    @app.route("/login", methods=["GET", "POST"])
    def login():
         if request.method == "POST" and "username" in request.form:
              username = request.form["username"]
    
              # check MongoDB for the existence of the entered username
              db_result = users_collection.find_one({ 'username' : username })
    
              result_id = int(db_result['userid'])
    
              # create User object/instance
              User = UserClass(db_result['username'], result_id, active=True)
    
              # if username entered matches database, log user in
              if username == db_result['username']:
                   # log user in, 
                   login_user(User)
                   return url_for("index"))
              else:
                   flash("Invalid username.")
          else:
               flash(u"Invalid login.")
          return render_template("login.html")
    

    My code 'kinda' works, I can log in and out, but as I said, it must hit the database for absolutely everything, because I have to provide a User object to the callback function in a different namespace/scope from where the rest of the login action takes place. I'm pretty sure I'm doing it all wrong, but I can't figure out how.

    The example code provided by flask-login does it this way, but this only works because it's pulling the User objects from a global hard-coded dictionary, not as in a real-world scenario like a database, where the DB must be checked and User objects created after the user enters their login credentials. And I can't seem to find any other example code that illustrates using a database with flask-login.

    What am missing here?