Python- creating a student records system in Python, Flask and SQLite – Part 2

Adding authentication and login

At the moment our app has no authentication at all, anyone can access it, instead, we’ll make it so that only people that are logged in can access the private areas.

We’ll do this using another database that handles all the login stuff and the use of a few flask libraries. So, let’s get the installation and setup out the way.

 

Run in the terminal:

Pip install flask_bcrypt

Pip intall flask_login

 

Now, in order to test our ability to login we need to do some setup to create some credentials and a database to hold them. First, we create a new script to setup the database.

Create a new script (I called mine setupLoginDB), add to it the following;

import sqlite3

conn = sqlite3.connect('login.db')

conn.execute('create table login(user_id INTEGER PRIMARY KEY AUTOINCREMENT,email text not null,password text not null);')

conn.execute('insert into login (user_id,email,password) VALUES (120,"xyz@mail.com","123xyz")')

conn.execute('insert into login (email,password) VALUES ("abc@mail.com","123abc")')

conn.commit()

 

This creates a new database “login.db” and in that database creates a table with three fields- the primary key, the unique identifier for that record in the database. We set this as autoincrement so it adds 1 to the previous value of key- this means we also have a unique identifier. Then we have a email and password field. Both are text, both are not null- meaning they can’t be empty.

 

Finally, we insert into the login tables some sample data, here we’re not protecting the data, we’re just using for testing.

YOU SHOULD NOT DO THIS FOR ANYTHING OTHER THAN TESTING.

It’s a very insecure process- we’ll go over protecting the data later.

Finally, we commit the changes to the database.

Run this script and it should create and populate your database meaning we now have two sample database users to test our login page with.

 

Our next step is to write the login.html page we’ll use to login, in keeping with prior pages, we’ll keep it plain, no css and no base template. If you’ve completed the previous, you can add them on.

Register

{% with messages = get_flashed_messages() %} {% if messages %}
    {% for message in messages %} {{ message }} {% endfor %}
{% endif %} {% endwith %}
 
 
 
 

This is simply a form that has boxes for email, password and rememberme, the email and password types are set and placeholders are set. On pressing the login button it’ll send it via a POST request to the /login endpoint- which we’ll write shortly         .

Back to your app.py and, at the top of your app.py file, add the following (some of this may already be there from the previous tutorial.

from flask import Flask, render_template, request, redirect, url_for, flash

from flask_login import LoginManager, UserMixin, login_required, login_user, logout_user, current_user

import sqlite3

from flask_bcrypt import Bcrypt

Then, at the top of your application (below the imports) add the below

app= Flask(__name__) #this should already be here

app.config['SECRET_KEY'] = 'thisIsSecret'

login_manager = LoginManager(app)

login_manager.login_view="login"

Secret keys are used to manage sessions- keeping the data for a session- data across multiple request about the user and previous requests.

We then setup a flask login manager- giving it the app name and telling it what the view will be- the endpoint we’re going to setup below the base login just returns the html page- we’ll later use a post request too but for now:

@app.route('/login')

def login():

    return render_template('login.html')

Following that, we need to implement something that represents the user, flask-login uses the userMixin class to do this. We create a class that inherits from this, declaring the construction with the fields we want- in this case, ID, email and password. This can be done within your app.py main script.

#creates a user model represent ing the user
class User(UserMixin):

    def __init__(self, id, email, password):

         self.id = id

         self.email = email

         self.password = password

         self.authenticated = False

         def is_active(self):

            return self.is_active()

         def is_anonymous(self):

            return False

         def is_authenticated(self):

            return self.authenticated

         def is_active(self):

            return True

         def get_id(self):

            return self.id

Next, we need to add in the post method- I’ve separated it out, but you could just include it in login, adding an if post check like we do prior.

@app.route('/login', methods=['POST'])

def login_post():

#check if already logged in- if so send home

    if current_user.is_authenticated:

        return redirect(url_for('home'))

           # do the standard database stuff and find the user with email

    con = sqlite3.connect("login.db")

    curs = con.cursor()

    email= request.form['email']

    curs.execute("SELECT * FROM login where email = (?)", [email])

#return the first matching user then pass the details to

#create a User object – unless there is nothing returned then flash a message

    row=curs.fetchone()

    if row==None:

        flash('Please try logging in again')

        return render_template('login.html')

    user = list(row);     

    liUser = User(int(user[0]),user[1],user[2])

    password = request.form['password']

    match = liUser.password==password

#if our password matches- run the login_user method

    if match and email==liUser.email:

        login_user(liUser,remember=request.form.get('remember'))

        redirect(url_for('home'))

    else:

        flash('Please try logging in again')

        return render_template('login.html')

    return render_template('home.html')

Eagle eyed amongst you will notice that there is a couple things here. First, login_user hasn’t been declared, it’s brought in from Flask and is the bit that tells Flask that a user is logged in. Second, the user of flash. Flash allows a message to quickly be added to the HTML providing the template already has a flash section defined. Looking back at the login.html template we do this using the following code which should already be in there.

 {% with messages = get_flashed_messages() %}

  {% if messages %}

    <ul class=flashes>

    {% for message in messages %}

      {{ message }}

    {% endfor %}

    </ul>

  {% endif %}

{% endwith %}

In order for login_user to work we also need Flask to be able to load the user in a way in which it expects. We create a load_user method in the format below and tell flask login_manager to use this.

@login_manager.user_loader

def load_user(user_id):
   conn = sqlite3.connect('login.db')
   curs = conn.cursor()
   curs.execute("SELECT * from login where user_id = (?)",[user_id])
   liUser = curs.fetchone()
   if liUser is None:
      return None
   else:
      return User(int(liUser[0]), liUser[1], liUser[2])

And that’s it, a very basic login system, storing passwords in plaintext, in order to actually function and protect pages though we need to tell flask-login which bits to protect.

We do this using @login_required between the app route and the function name. E.g. in the below listStudents and new_student require login but login does not.

@app.route('/liststudents')

@login_required

def listStudents():


@app.route('/enternew')

@login_required

def new_student():


@app.route('/login', methods=['POST'])

def login_post():

 

We can also provide a method for people to logout too! We could (and should) link login and logout buttons and links to the navbar.

@app.route('/logout')

@login_required

def logout():

    logout_user()

    return redirect(url_for('home'))

Protecting passwords

Plain text passwords in a database are vulnerable to interception and theft, instead of using plain text we’re going to use a hashing and salting method.

Hashing is a process that ciphers text into something different using a specific algorithm, once done it’s impossible to change back. Salting adds random characters into the mix before or after the password before it’s hashed so the end result is even harder to decipher or use.

So, let’s go through and add it in, we’ll be using the popular bcyrpt library and it’s flask companion. Add the import and then setup your app at the top.

from flask_bcrypt import Bcrypt

app= Flask(__name__) # should already be here

bcrypt = Bcrypt(app) #creates new bcrypt instance

 

We then need to make sure our login methods use this new encryption- any of our old passwords that are plain text will then stop working, we’ll need users to create new ones with the hashing algorithm. We can either do that manually or instead implement a register page later to try it out. See the change below in login_post- I’ve ensured it’s a different colour. Everything else stays the same.

@app.route('/login', methods=['POST'])

def login_post():

#previous stuff above

    liUser = User(int(user[0]),user[1],user[2])
    password = request.form['password']

    match = bcrypt.check_password_hash(liUser.password, password)
    if match and email==liUser.email:

#previous stuff below      

Instead of just checking to see if the hashes match or the password matches, we now need to use the check_password_hash method, this checks to make sure that- when hashed in the same way, the password matches.

To our existing implementation that’s all we need to change, but, we need a way to input and setup users.

 

Registering new users

Once again I’ve separated out the GET and POST requests, with the standard register just displaying the page, the POST

@app.route('/register')

def register():

    return render_template('register.html')




@app.route('/register', methods=['POST'])  

def register_post():

#check is authenticated/ logged in already

    if current_user.is_authenticated:

        return redirect(url_for('home'))

           #standard DB stuff

    con = sqlite3.connect("login.db")

    curs = con.cursor()

    email= request.form['email']

    password = request.form['password']

# we use bycrpt to hash the password we’ve put in with salt

    hashedPassword=bcrypt.generate_password_hash(password)
#some users have complained of getting an error- if you do change this to hashedPassword=bcrypt.generate_password_hash(password).decode('utf-8')

# then add it into the database- now hashed

    con.execute('insert into login (email,password) VALUES (?,?)',[email,hashedPassword])

    con.commit()

    return render_template('home.html')

Finally, we need to add in the register.html page so that people can actually register. It’s very similar in structure to the login page.



    

Register

{% with messages = get_flashed_messages() %} {% if messages %}
    {% for message in messages %} {{ message }} {% endfor %}
{% endif %} {% endwith %}
 
 
 
 



Comments are closed.