is52027c lab 15: logins
For the next few weeks you should work on the sql version of the mytwit app.
Today we are going to add authenticated login to our mock twitter application ('mytwit'). To do this we will be using Flask sessions, which are cryptographically signed cookies (which means no one can change them).
Ths steps we will follow are:
- create a simple login that checks the username & password from a form and sets a session
- use that session to limit access to add, edit & delete functions
- replace our simple session with the flask-login extension
- use password hashing with a salt (uses the hashlib library)
Scripts for these steps will be added to the repo on Friday.
Note that to keep the main app cleaner, you should now put specific parts of the code in separate files:
-
all the database code should go in a file called dbhelper.py, which is imported at the start of the app
from dbhelper import DBHelper
-
the wtfforms definitions should go in a file called forms.py, and are also imported at the top of the app:
from forms import addTwitForm, editTwitForm, loginForm
task 1: simple login
in the lab 15 repo this will be version _7 of the script
we will be using flask's sessions: you can read about them in the flask quickstart http://flask.pocoo.org/docs/0.12/quickstart/#sessions.
as before, our app needs a value for app.secret_key (so it can do the necessary encryptions)
note that you should use flask-wtf for your forms.
constructing a simple login involves several steps:
- create a login form, which submits to a route like /login
- create a flask view to handle that route
- create a database method that compares the username & password from the form with values from the database
- in the view, use a database method to check the username & password
- if they match, create a session (e.g. session['user_id'] = user_id), flash a 'success' message, and redirect to the index view
- if not, flash an 'unsuccessful' message and re-display the form
- also create a /logout route and a view that logs the user out (e.g. session.pop('user_id', None) - see quickstart for more info)
i also suggest that when the user is logged in their logged in status is displayed on every page. you can do this through the base template as the 'session' object is available in jinja by default (http://flask.pocoo.org/docs/0.12/templating/). to make this simple, as well as storing the user_id in the session, i also stored the username.
task 2: restrict access based on logged-in status
in the lab 15 repo this will be version _8 of the script
now you can restrict access to key functions using the session.
make it so that only logged in users can add, edit or delete twits.
an easy way to do this is to check if the session is set, and if not return an http authorisation error:
if not session.get('username'):
abort(401)
task 3: use flask-login
in the lab 15 repo this will be version _9 of the script
now we will rewrite the login process to use the flask-login extension: you can find documentation for this extension here: https://flask-login.readthedocs.io/en/latest/
it may take a bit of thought to wrap your hed around what flask-login is doing. in a nushell, it just manages login sessions for you. we'll take a closer look at the logic in the lecture; in the mean time, you should focus on implementing it.
first, we need to install the extension:
pip install --user flask-login
key points for implementing flask-login:
import the flask-login modules:
from flask_login import LoginManager, login_required
from flask_login import login_user, logout_user
create an instance of login manager before you instantiate the app itself:
login_manager = LoginManager()
initiate that with the app after it has been created
login_manager.init_app(app)
You will see from the flask-login documentation that you 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. You don't need to use this function directly but it needs to be there for flask-login to work.
In my version of the app, i create a db method that does this, so the callback function looks like:
@login_manager.user_loader
def load_user(user_id):
result = db.get_user(user_id)
if result:
return User(user_id)
As you will also see from the flask-login documentation, you also need to provide a user class that implements some specific requirements. I have included user.py in the repo which will do this for you.
When using flask-login, instead of setting your own session on login, you use the user_id for the user to create a User instance, and pass that to the login_user method. In other words, like this
user_id = db.check_password(username,password)
if user_id:
user = User(user_id)
login_user(user)
Logging the user out simply means invoking logout_user().
Now that flask-login is working, access to any route / view can be restricted to logged in users simply by decorating the view with @login_required i.e.
@app.route('/add_twit', methods = ['GET', 'POST'])
@login_required
def add_twit():
# do stuff here
Task 4: hashing passwords
in the lab 15 repo this will be version _10 of the script
Switching to hashed passwords doesn't require any changes in our main app, as we will still be passing the username and password from the form to the db functions for checking.
The changes are in how the password is stored and checked i.e. in the database class and in the database itself.
We will use python's hashlib module: https://docs.python.org/3/library/hashlib.html
I have provided a new dummy.sql file for the database which includes a hash and a salt. You can import this in to your mytwits database in the same way as lab-14 i.e. in the mysql shell:
use mytwits
source dump.sql;
For backwards compatability this keeps the password field in plain text as well. A real user database would only store the hash and the salt.
The steps for your password checking function will be:
-
query the database for the user with the username given in the form
-
if that user exists, retrieve the user_id, salt and hashed password from the db
-
hash the combined string of the salt plus the password from the form, and see if it matches the stored hash i.e.
if hashlib.sha512((salt + password).encode('utf-8')).hexdigest() == hashed:
We need to encode the strings as utf-8 bytes before we can hash them (by default, strings in python 3 are unicode objects).
Be careful, as orer is important here; the hash of 'salt + password' is not the same as the hash of 'password + salt'.
exercise for the week
create a registration view for adding new users to the app