How to encrypt password using Python Flask-Security using bcrypt?
Asked Answered
K

3

7

I'm trying to utlise the standard basic example in the docs for Flask-Security and have made it work except for the password being stored in plaintext.

I know this line:

user_datastore.create_user(email='[email protected]', password='password')

I could change to:

user_datastore.create_user(email='[email protected]', password=bcrypt.hashpw('password', bcrypt.gensalt()))

But I thought Flask-Security took care of the (double?) salted encryption and if I add the app.config['SECURITY_REGISTERABLE'] = True and go to /register the database this time IS encrypted correctly.

I know I am missing something simple but don't quite understand where..

from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin, login_required
import bcrypt

# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'super-secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///login.db'
app.config['SECURITY_PASSWORD_HASH'] = 'bcrypt'
app.config['SECURITY_PASSWORD_SALT'] = b'$2b$12$wqKlYjmOfXPghx3FuC3Pu.'

# Create database connection object
db = SQLAlchemy(app)

# Define models
roles_users = db.Table('roles_users',
        db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
        db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))

class Role(db.Model, RoleMixin):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(80), unique=True)
    description = db.Column(db.String(255))

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    active = db.Column(db.Boolean())
    confirmed_at = db.Column(db.DateTime())
    roles = db.relationship('Role', secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))

# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)

# Create a user to test with
@app.before_first_request
def create_user():
    try:
        db.create_all()
        user_datastore.create_user(email='[email protected]', password='password')
        db.session.commit()
    except:
        db.session.rollback()
        print("User created already...")

# Views
@app.route('/')
@login_required
def home():
    return render_template('index.html')

if __name__ == '__main__':
    app.run()
Kneepad answered 5/9, 2018 at 17:59 Comment(2)
I'm not sure what your problem is, but bcrypt doesn't encrypt passwords. Passwords are properly stored hashed, which is an important distinction. Encrypted data can be decrypted; cryptographic hashes are designed to be irreversible. Also, the size of the output from these hashes is predictable and the same for the empty string and the entire text of War and Peace, meaning it's easy to store hashes of arbitrarily large passwords in a database column with a fixed size.Careful
Ok I understand the terminology now but it doesn’t answer why the password is stored in plaintext in the database.Kneepad
H
11

Instead of storing the password you can use python's native decorators to store a hashed version of the password instead and make the password unreadable for security purposes, like this:

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True)
    password_hash = db.Column(db.String(128))

    @property
    def password(self):
        raise AttributeError('password not readable')
    @password.setter
    def password(self, password):
        self.password_hash = bcrypt.hashpw('password', bcrypt.gensalt()))
        # or whatever other hashing function you like.

You should add a verify password function inline with the bcrypt technolgy you implement:

    def verify_password(self, password)
        return some_check_hash_func(self.password_hash, password)

Then you can create a user with the usual:

User(email='[email protected]', password='abc')

and your Database should be populated with a hashed password_hash instead of a password attribute.

Helterskelter answered 5/9, 2018 at 18:47 Comment(0)
L
4

You're right, create_user() doesn't hash the password. It is a lower-level method. If you are able to use registerable.register_user() instead, then it will hash the password for you. But if you would like to use create_user() directly, then just encrypt the password before calling it:

from flask import request
from flask_security.utils import encrypt_password

@bp.route('/register/', methods=['GET', 'POST'])
@anonymous_user_required
def register():
    form = ExtendedRegistrationForm(request.form)

    if form.validate_on_submit():
        form_data = form.to_dict()
        form_data['password'] = encrypt_password(form_data['password'])
        user = security.datastore.create_user(**form_data)
        security.datastore.commit()

    # etc.

I wouldn't recommend overriding the password hashing on the User object, since Flask-Security uses the SECURITY_PASSWORD_HASH setting to store the password hashing algorithm. (It defaults to bcrypt, so you don't need to set this explicitly if you don't want to.) Flask-Security uses HMAC to salt the password, in addition to the SECURITY_PASSWORD_SALT which you provide, so just hashing the password using e.g. passlib with bcrypt won't result in a hash that Flask-Security will correctly match. You might be able to side-step this by cutting Flask-Security out of the loop and doing all password creation and comparison tasks yourself… but what's the point? You're using a security library, let it do security for you. They've already fixed the bugs you're bound to run into.

Lanta answered 28/6, 2019 at 15:21 Comment(0)
T
0

Not sure if things have changed since this was asked, but the docs now state explicitly that

"Be aware that whatever password is passed in will be stored directly in the DB. Do NOT pass in a plaintext password! Best practice is to pass in hash_password(plaintext_password)." (emphasis mine)

i.e.:


from flask_security import hash_password

...

user_datastore = SQLAlchemyUserDatastore(db, User, Role)
app.security = Security(app, user_datastore)

...

app.security.datastore.create_user(email=email, password=hash_password(password), roles=roles)


Trisa answered 15/2, 2023 at 8:0 Comment(2)
This is for Flask-Security-Too, which is a maintained fork of the original (and now abandoned) Flask-Security. And it appears that encrypt_password() has been renamed to hash_password().Lanta
Worth keeping as an answer, I suppose.Trisa

© 2022 - 2024 — McMap. All rights reserved.