Explore Flask

12.2. Storing passwords

Rule number one of handling users is to hash passwords with the Bcrypt (or scrypt, but we'll use Bcrypt here) algorithm before storing them. We never store passwords in plain text. It's a massive security issue and it's unfair to our users. All of the hard work has already been done and abstracted away for us, so there's no excuse for not following the best practices here.

Note OWASP is one of the industry's most trusted source for information regarding web application security. Take a look at some of their recommendations for secure coding.

We'll go ahead and use the Flask-Bcrypt extension to implement the bcrypt package in our application. This extension is basically just a wrapper around the py-bcrypt package, but it does handle a few things that would be annoying to do ourselves (like checking string encodings before comparing hashes).

# ourapp/__init__.py

from flask.ext.bcrypt import Bcrypt

bcrypt = Bcrypt(app)

One of the reasons that the Bcrypt algorithm is so highly recommended is that it is "future adaptable." This means that over time, as computing power becomes cheaper, we can make it more and more difficult to brute force the hash by guessing millions of possible passwords. The more "rounds" we use to hash the password, the longer it will take to make one guess. If we hash our passwords 20 times with the algorithm before storing them the attacker has to hash each of their guesses 20 times.

Keep in mind that if we're hashing our passwords 20 times then our application is going to take a long time to return a response that depends on that process completing. This means that when choosing the number of rounds to use, we have to balance security and usability. The number of rounds we can complete in a given amount of time will depend on the computational resources available to our application. It's a good idea to test out some different numbers and shoot for between 0.25 and 0.5 seconds to hash a password. We should try to use at least 12 rounds though.

To test the time it takes to hash a password, we can time a quick Python script that, well, hashes a password.

# benchmark.py

from flask.ext.bcrypt import generate_password_hash

# Change the number of rounds (second argument) until it takes between
# 0.25 and 0.5 seconds to run.
generate_password_hash('password1', 12)

Now we can keep timing our changes to the number of rounds with the UNIX time utility.

$ time python test.py

real    0m0.496s
user    0m0.464s
sys     0m0.024s

I did a quick benchmark on a small server that I have handy and 12 rounds seemed to take the right amount of time, so I'll configure our example to use that.

# config.py


Now that Flask-Bcrypt is configured, it's time to start hashing passwords. We could do this manually in the view that receives the request from the sign-up form, but we'd have to do it again in the password reset and password change views. Instead, what we'll do is abstract away the hashing so that our app does it without us even thinking about it. We'll use a setter so that when we set user.password = 'password1', it's automatically hashed with Bcrypt before being stored.

# ourapp/models.py

from sqlalchemy.ext.hybrid import hybrid_property

from . import bcrypt, db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(64), unique=True)
    _password = db.Column(db.String(128))

    def password(self):
        return self._password

    def _set_password(self, plaintext):
        self._password = bcrypt.generate_password_hash(plaintext)

We're using SQLAlchemy's hybrid extension to define a property with several different functions called from the same interface. Our setter is called when we assign a value to the user.password property. In it, we hash the plaintext password and store it in the _password column of the user table. Since we're using a hybrid property we can then access the hashed password via the same user.password property.

Now we can implement a sign-up view for an app using this model.

# ourapp/views.py

from . import app, db
from .forms import EmailPasswordForm
from .models import User

@app.route('/signup', methods=["GET", "POST"])
def signup():
    form = EmailPasswordForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, password=form.password.data)
        return redirect(url_for('index'))

    return render_template('signup.html', form=form)