×
By the end of this chapter, you should be able to:
salt
and a work factor
areSo far our applications have handled multiple resources, but we have not discussed an essential part of building web applications: authentication and authorization! Let's first define what these terms mean.
Authentication - making sure a user is who they say they are. The process of logging in is a prime example of authentication. In order to successfully log in, we need to make sure a user is who they say they are (by providing a correct username and password).
Authorization - making sure a user is allowed to access a route / resource. On Facebook, you are not "authorized" to delete other people's posts. On Github, you are not authorized to push to other people's repositories unless they "authorize" you.
Let's start with authentication and analyze the key parts of the process: signing up and logging in.
Signing up - make sure a user provides a unique identifier (username / email) and a password. Store that information in the database for when a user logs in
Logging In - first make sure that the user has provided a unique identifier (username / email) that exists. If that identifier exists, check to see if their password provided is the same one as in the database. If it is, log them in!
In the sign up process, we mention that we store a username and password that a user provides when saving them. This seems simple, but there is a highly dangerous security risk that we must always consider. When we accept the username and password, they come to the server as plain text, which means that if we were to store that information directly we would be storing our password in plain text.
So what's the problem here? Imagine if someone (a hacker, disgruntled employee or even another developer) got access to your database and every user's password was clearly visible. Not only would this be a terrible security breach for your application, but very commonly we use the same password for many different applications. So when storing passwords, we must always remember: NEVER STORE PASSWORDS IN PLAIN TEXT.
What we need to do is hash a password before saving it to the database. You'll sometimes see hashing referred to as one-way encryption. This means that our goal isn't to ever decrypt the password: instead, we just want to make it difficult for someone to obtain a user's password even if they gain access to the database.
But if we don't know the user's password, how can we log them in? The trick is that when someone attempts to log in, we'll hash the password they type, and compare that hashed password to the value in the database. But we never decrypt anything. (You'll see more details of this in just a moment.)
Another type of encryption, which we won't be using, is two-way encryption. With two-way encryption, both parties know a secret key that they can use to decipher messages.
hen you are doing any kind of password hashing, you should use an industry established hashing algorithm. We will be using the bcrypt
algorithm which is based off of the Blowfish cipher.
bcrypt
To get started using bcrypt
, let's first create a virtual environment and install Flask. While bcrypt
doesn't come natively in Flask, there is a bcrypt
module designed for integration with Flask, called (unsurprisingly) flask-bcrypt
:
mkvirtualenv learn-auth workon learn-auth pip install flask flask-bcrypt ipython createdb learn-auth
Before building our app, let's explore how bcrypt
works. Hop into ipython
and run the following:
from flask_bcrypt import Bcrypt bcrypt = Bcrypt() pw_hash = bcrypt.generate_password_hash('secret') bcrypt.check_password_hash(pw_hash, 'secret') # True bcrypt.check_password_hash(pw_hash, 'secret2') # False pw_hash # should look like a long, incomprehensible byte literal!
This is exactly how we will be storing our users' passwords and checking to see if they are correct!
If you look at a password that's been hashed using bcrypt
, it should look like a bunch of characters jumbled together. However, there is a structure in this hashed password that it's helpful to know about. Let's look at an example:
b'$2b$12$3cy0jD1AfgcT0ipGL1UhquBZXvAxUwRrdG90Gi951AcxIXm2F2gMK' # prefix = 2b # work factor = 12 # salt = 3cy0jD1AfgcT0ipGL1Uhqu # hash = BZXvAxUwRrdG90Gi951AcxIXm2F2gMK
We won't go into too much detail about these components of the byte literal, but it is worth knowing a bit about these different pieces. The hashed password is actually just the last 31 characters of the byte literal (BZXvAxUwRrdG90Gi951AcxIXm2F2gMK
). The rest of the components give you information about how the original password was hashed.
The prefix is not terribly important: it simply indicates that bcrypt
was used to encrypt the password, as opposed to some other encryption algorithm. In this case the prefix is 2b
, but for bcrypt
you might also see 2a
or 2y
as the prefix.
Next comes the work factor. Roughly speaking, this measures how long it takes to perform the encryption. One benefit to using a good hashing algorithm is that it can prevent brute force attacks, whereby an attacker simply tries thousands or even millions of passwords in quick succession. The more time it takes to hash the passwords and perform the check, the less effective a brute force attack becomes. However, there's also a tradeoff here: the higher the work factor, the better the hashing, but the worse the user experience. Imagine if it took several minutes to log in to a website because of the time spent hashing the password that the user typed in!
Finally, you can think of the salt
as a randomly generated string that's used to provide a degree of randomness into the hashing process. The salt is combined with the original password to generate the hash. The salt is stored along with the hash because if you want to check that the user has provided the right password when they attempt to log in, you need to know what salt was originally used to hash the encrypted password that's stored in the database.
This is also why when you hash the same string twice, you'll get different output values from bcrypt
! Because of the salt, even people who have the same passwords will have different hashes in the database.
from flask_bcrypt import Bcrypt bcrypt = Bcrypt() hash1 = bcrypt.generate_password_hash('secret') hash2 = bcrypt.generate_password_hash('secret') hash1 == hash2 # False - check out the hashes, they'll have different values! hash3 = bcrypt.generate_password_hash('secret', 17) # the second argument lets us increase/decrease the work factor. Default value is 12.
For more details on Bcrypt, you can check out this article. If you're curious about the work factor specifically, this blog post digs into performance a bit.
Now that we have an idea of how to secure our passwords, let's build a Flask application that contains two forms: signup and login. We will securely store the user's information and authenticate them when they submit the signup form. If they successfully log in, we will redirect them to a simple page that says "You are logged in!"
To begin, let's set up our application so it follows our new structure for Flask applications. From the learn-auth
directory, we need to create the following files and folders:
touch app.py mkdir project mkdir project/{users,templates} mkdir project/users/templates touch project/users/templates/{signup,login,welcome}.html touch project/users/{forms,models,views}.py touch project/templates/base.html touch project/__init__.py pip install psycopg2 flask-sqlalchemy flask-wtf flask-migrate flask-bcrypt
That's a fair amount of setup, but with practice the process should become more familiar. Let's put some initial setup into our project/__init__.py
:
from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from flask_migrate import Migrate app = Flask(__name__) bcrypt = Bcrypt(app) app.config['SQLALCHEMY_DATABASE_URI'] = 'postgres://localhost/learn-auth' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SECRET_KEY'] = 'super secret' # bad practice in general, but we'll live with it for now db = SQLAlchemy(app) migrate = Migrate(app, db) from project.users.views import users_blueprint app.register_blueprint(users_blueprint, url_prefix='/users')
Of course, we don't have a users blueprint yet, so let's work on that next. With db
and bcrypt
defined, let's create our User
model in project/users/models.py
:
from project import db, bcrypt class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.Text, unique=True) password = db.Column(db.Text) def __init__(self, username, password): self.username = username self.password = bcrypt.generate_password_hash(password).decode('UTF-8')
(The decoding on the password just ensures that our passwords are stored in the database with the proper character encoding.)
We'll also need a form for signing up and logging in. Here's our project/users/forms.py
:
from flask_wtf import FlaskForm from wtforms import StringField, PasswordField from wtforms.validators import DataRequired class UserForm(FlaskForm): username = StringField('username', validators=[DataRequired()]) password = PasswordField('password', validators=[DataRequired()])
Next we need to work on project/users/views.py
. Here's some starter code (we'll unpack it below):
from flask import redirect, render_template, request, url_for, Blueprint from project.users.forms import UserForm from project.users.models import User from project import db,bcrypt from sqlalchemy.exc import IntegrityError users_blueprint = Blueprint( 'users', __name__, template_folder='templates' ) @users_blueprint.route('/signup', methods =["GET", "POST"]) def signup(): form = UserForm(request.form) if request.method == "POST" and form.validate(): try: new_user = User(form.data['username'], form.data['password']) db.session.add(new_user) db.session.commit() except IntegrityError as e: return render_template('signup.html', form=form) return redirect(url_for('users.login')) return render_template('signup.html', form=form) @users_blueprint.route('/login', methods = ["GET", "POST"]) def login(): form = UserForm(request.form) if request.method == "POST" and form.validate(): found_user = User.query.filter_by(username = form.data['username']).first() if found_user: authenticated_user = bcrypt.check_password_hash(found_user.password, form.data['password']) if authenticated_user: return redirect(url_for('users.welcome')) return render_template('login.html', form=form) @users_blueprint.route('/welcome') def welcome(): return render_template('welcome.html')
The bulk of the logic comes inside of the signup
and login
methods. Let's take a look at signup
first. Here's what happens:
signup
page.User
model, so we don't need to explicitly refer to bcrypt
here).Similarly, here's what happens inside of login
:
login
page.login
page.login
page.If you look at the following code:
found_user = User.query.filter_by(username = form.data['username']).first() if found_user: authenticated_user = bcrypt.check_password_hash(found_user.password, form.data['password']) if authenticated_user:
There is a nice option to refactor here, where we can move some of this logic into a method in our model. We could make a class method called authenticate which accepts a username and a password and implements the following logic above. By moving that logic to a model, we can delegate the responsibility of our "data" related logic to the model and reduce code in our controller.
Here's what that might look like:
from project import db, bcrypt class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.Text, unique=True) password = db.Column(db.Text) def __init__(self, username, password): self.username = username self.password = bcrypt.generate_password_hash(password).decode('UTF-8') # notice we are making a class method here since we will be invoking this using User.authenticate() @classmethod # let's pass some username and some password def authenticate(cls, username, password): found_user = cls.query.filter_by(username = username).first() if found_user: authenticated_user = bcrypt.check_password_hash(found_user.password, password) if authenticated_user: return found_user # make sure to return the user so we can log them in by storing information in the session return False
After moving authentication to the model, we can simplify our login
function inside of project/users/views
to use our new class method:
@users_blueprint.route('/login', methods = ["GET", "POST"]) def login(): form = UserForm(request.form) if request.method == "POST" and form.validate(): if User.authenticate(form.data['username'], form.data['password']): return redirect(url_for('users.welcome')) return render_template('login.html', form=form)
We can also stop importing bcrypt
inside of our views file - only the model needs it now!
With this, all of our server-side code is complete. Let's run a quick migration:
flask db init flask db migrate flask db upgrade
If you have any errors, try your best to debug them!
Let's now add our views. Let's keep things as simple as possible:
project/templates/base.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Authentication App</title> </head> <body> {% block content %} {% endblock %} </body> </html>
project/users/templates/signup.html
{% extends 'base.html' %} {% block content %} <form method="POST" action="{{url_for('users.signup')}}"> {{ form.csrf_token }} <p>{{ form.username(placeholder="username") }} <span> {% if form.username.errors %} {% for error in form.username.errors %} {{ error }} {% endfor %} {% endif %} </span> </p> <p> {{ form.password(placeholder="password") }} <span> {% if form.password.errors %} {% for error in form.password.errors %} {{ error }} {% endfor %} {% endif %} </span> </p> <button type="submit">Sign Up!</button> </form> {% endblock %}
project/users/templates/login.html
{% extends 'base.html' %} {% block content %} <form method="POST" action="{{url_for('users.login')}}"> {{ form.csrf_token }} <p>{{ form.username(placeholder="username") }} <span> {% if form.username.errors %} {% for error in form.username.errors %} {{ error }} {% endfor %} {% endif %} </span> </p> <p> {{ form.password(placeholder="password") }} <span> {% if form.password.errors %} {% for error in form.password.errors %} {{ error }} {% endfor %} {% endif %} </span> </p> <button type="submit">Log In!</button> </form> {% endblock %}
project/users/templates/welcome.html
{% extends 'base.html' %} {% block content %} <h1>You are logged in!</h1> {% endblock %}
Finally, let's complete our app.py
:
from project import app if __name__ == '__main__': app.run(debug=True)
Now you can run python app.py
and confirm that the app works as expected. Things you should verify:
When you're ready, move on to Authentication with Cookies and Sessions in Flask