Flask Login

Flask provides an extension to manage the user, tracking whether a person has been authenticated within the session. This post will introduce that concept.

Flask Login

Flask provides an extension to manage the user, tracking whether a person has been authenticated within the session. This post will introduce that concept.

Flask-Login is able to work with any number of authentication schemes. It is not necessary to specify one yet. I am going to postpone deciding what the implementation will be. For now, I will just extend the existing functionality of the Anonymous Agile Estimator to use this extension.

Flask-Login provides management of the user sessions, making a current_user available in the model used by the template and views. This is stored in the user session, but abstracts us away from worrying about the implementation.

Install and Configure

First we must install the plug-in with pip install flask-login and then add it to the estimator module __init__ file so that it can be initialised with the create_app function.

First import the manager class:

from flask_login import LoginManager

Then create an instance. Note that this points to a controller function called login in the Blueprint web that has not been created yet. None of the pages have flagged as requiring authentication yet, so this will not break anything at the moment.

login_manager = LoginManager()
login_manager.login_view = 'web.login'

The last step of including this plug-in is to initialise it with the application within the create_app function. This is similar to the way that both Bootstrap and SqlAlchemy plug-ins were added to the application.

login_manager.init_app(app)

To test this I temporarily add the following block of HTML to the index.html page. Before any of the above code, an error was thrown but when everything is wired up correctly, I see the text in the second part of the if statement.

{% if current_user.is_authenticated %}
	<p> authenticated user</p>
{% else %}
	<p> non-authenticated user but the field current_user is being accessible</p>
{% endif %}

Any page can now check `current_user.is_authenticated` to at least verify that the user has successfully logged in.

## Logging In

Not sure where the best place to put the next piece of code. In *Flask Web Development*, Miguel Grinberg suggests that this belongs in with the database model module. I am placing it into the controller for now, as it has access to the `login_manager`. The file is going to need re-factoring soon to make it smaller and move some functionality into services, but will do for now.

The application needs to know how to load a user from the database. We can use a decorator associated with the `login_manager` created above to mark a function as the loader.

```python
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

Now the functions that require it can be marked with a decorator to indicate that the user should be logged on. For example:

@web.route('/creategroup', methods=['POST'])
@login_required
def create_group():
…

I am going to need to create a login view and template that responds to both GET and POST requests. This will replace the existing accept_nickname controller function. Note that there is no real verification going on. Only a nickname is requested, but this is where any authentication will be done eventually.

One further change is that the user needs to be created at this point. Previously the nickname was only stored in the DB when it was first needed, for example, after creating a group.

def find_or_create_user(nickname):
	user = User.query.filter_by(nickname=nickname).first()
	if user == None:
		user = User(nickname)
		db.session.add(user)
		db.session.commit()
	return user

I’ve placed the login_required decorator on the create_group view, which only accepts a POST request. By default, the redirect will give the client a response code of 302. That will cause the client to send a GET on the same address, which means loading the page will not work.

To get the posting of a new group to work, I would need to redirect and tell the client browser to resend the same information. This can be done by returning a 307 response code rather than 302, indicating to clients to resend the same data. However, because the login page is being shown following an initial redirect from the login_required decorator which uses a 302, I cannot get this to work at the moment.

To work around this, I will add a GET version of any POST only views and make sure that the view returns the user back to a reasonable place. In the case of create_group, the form data will not validate and it will return the home page and thus presenting the page from which groups can be created. The declaration becomes:

@web.route('/creategroup', methods=['GET',POST'])
@login_required
def create_group():
…

In reality, this is not a problem that should affect most users. It is unlikely that they would have access to the form in order to send an unauthenticated POST to an endpoint protected by login_required. This situation only really exists in testing a partially protected application during development. Or using Postman or curl to connect to the website.

To log the user into the application, I’ve replaced the accept_nickname function with the following.

@web.route('/login', methods=['GET', 'POST'])
def login():
	form = NickNameForm()
	if form.validate_on_submit():
		nickname = form.name.data
		# this is where real authentication should be done
		user = find_or_create_user(nickname)
		login_user(user)
		next = request.args.get('next')
		if next is None or not next.startswith('/'):
			next = url_for('web.index')
		return redirect(next)
		error_message = "Unable to log you in."
		return render_template('generic-error.html', error_message=error_message, back_url=next), 400
	return render_template('login.html', form=form)

Note on line 8 the call to login_user(user) which is a new import from flask_login. This is what marks the client as authenticated and logged in. There are additional parameters available for this to allow the setting of things such as staying logged in with or without an expiry. For now, it is sufficient to simply login.

The template needs to change also. Originally accept_nickname was responding to POST requests at the root of the application. It now needs to send the nickname form to /login.

{{ wtf.quick_form(form, action="/login") }}

Logging out is easy also. Another import is required: logout_user and this call needs to be made from the logging out function. The session.pop('nickname') is obsolete and has been removed.

@web.route('/confirm-logout', methods=['GET', 'POST'])
def confirm_logout():
	if request.method == 'GET':
		return render_template('confirm-logout.html')
	logout_user()
	return redirect(url_for('web.index'))

Any part of the code that uses the nickname session variable need to change to use the current_user field. The model object this represents is the User as defined above, with extra functions available through the extension of UserMixin. So to access the nickname, use current_user.nickname.

For example the navigation bar code, held in a base template needs to change as follows. The bar should only show certain options if the user is logged in.

{% if current_user.is_authenticated %}
<li><a href="/">My Groups</a></li>
<li><a href="/confirm-logout">Sign Out {{ current_user }}</a></li>
{% endif %}

Note that here I did not reference the nickname attribute of the object. The object has defined the __repr__ function to return the nickname field.

More on UserMixin

The key thing that I like about this approach is that the User model that was already in use is enhanced by extending the UserMixin class from Flask-Login, with little or no impact to its functionality. Here is a brief summary of what is possible.

First up is that everything that is on the model is available as normal by simply accessing the property. No more loading the user from the database; simply import current_user and access it anywhere.

If the user has not been authenticated, then you cannot, obviously, access any of the fields. Instead you can use one of these additional properties to verify that the user has authenticated.

  • is_authenticated will return true if the user has logged in and been authenticated
  • is_active can be used to identify users that are not active. Perhaps they have no validated an email address, or their account has been suspended
  • is_anonymous should be the opposite of is_authenticated and is the base state before the user logs in

To take advantage of this plug-in, practically no change had to be made to the underlying user model, simply adding the UserMixin super class and add the small amount of configuration shown above. This means it can slot in nicely with most use cases.

One thing to ask is that since the application no longer puts a session variable containing the user nickname, what is stored in the user’s browser cookies? Will any sensitive data from the user object be saved? Flask cookies are not a good place to store sensitive data.

Let’s look at the cookie to see what’s stored in it. There are a number of ways to do this depending on the browser. In Safari, you can use the Inspect Element option from the context menu. On the Storage tab the cookie is visible and can be copied.

viewing-a-cookie

This one is base 64 encoded, but the leading period indicates that it is also compressed.

session	.eJwlz0lqAzEQQNG7aO2FVIOq5Ms0pRpICCTQba9C7m5DDvDh_d921JnXR7s_zmfe2vEZ7d6WWy4CIZEVC6t8IvAKAUgvR02XNCJxtKiR7l2rck_APXJJjY2BHXNsJiHmTQqqQAtKVcWVFw3eSsFRBQBqfUvvPbYDWLs1v846Hj9f-f328PRSDjNliM4TZS4u7CUaZIjImpi2393zyvN_gtrfC9P5Pug.XPu6ng.fBY63VfAHY0QlV8dFX-1a-6GD6U	localhost	/	Session	266 B		✓	

Taking everything from the first period up to but not including the second period, and adding three padding characters (needed for the base64 encoding), you can decode this with the following in the Python shell.

>>> import base64
>>> import zlib
>>> zlib.decompress(base64.urlsafe_b64decode('.eJwlz0lqAzEQQNG7aO2FVIOq5Ms0pRpICCTQba9C7m5DDvDh_d921JnXR7s_zmfe2vEZ7d6WWy4CIZEVC6t8IvAKAUgvR02XNCJxtKiR7l2rck_APXJJjY2BHXNsJiHmTQqqQAtKVcWVFw3eSsFRBQBqfUvvPbYDWLs1v846Hj9f-f328PRSDjNliM4TZS4u7CUaZIjImpi2393zyvN_gtrfC9P5Pug==='))

Which yields the following JSON that only contains the id of the user.

{
    "_fresh": true,
    "_id": "9cae94274779d93ffc63259d722ecfc38ec7ea447c3adf1ecc08ffeb623b1e97f1b3d303e1b547455b482882492f8887c859415b84d5dff2228a0b7000dbc22a",
    "csrf_token": "56cf85daa852d05637695f30f78d4a33358e3eab",
    "user_id": "4"
}

Flask-Login uses the function decorated with login_manager.user_loader to load the user object from the database whenever it needs to be refreshed reducing the amount of database code you need to write.

Conclusion

Flask-Login just manages the user within the browser session for you. It is agnostic on how the user is authenticated, allowing you to use any method you like to provide this. It is simple to add and does not impact the code by imposing a particular structure.