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
BCRYPT_LOG_ROUNDS = 12
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))
@hybrid_property
def password(self):
return self._password
@password.setter
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)
db.session.add(user)
db.session.commit()
return redirect(url_for('index'))
return render_template('signup.html', form=form)