How to encrypt password using Python Flask-Security using bcrypt?

Question:

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()
Asked By: Johnny John Boy

||

Answers:

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.

Answered By: Attack68

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.

Answered By: Nick K9

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)


Answered By: mcsoini
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.