Skip to content

Commit

Permalink
Make SECURITY_PASSWORD_SINGLE_HASH a list of scheme ignoring double …
Browse files Browse the repository at this point in the history
…hash (opr #714) (#60)

* Make SECURITY_PASSWORD_SINGLE_HASH a list of scheme ignoring double hash

* Make SECURITY_PASSWORD_SINGLE_HASH a list of scheme ignoring double hash
  • Loading branch information
jwag956 authored May 6, 2019
1 parent f982c5a commit 5123f8e
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 26 deletions.
17 changes: 9 additions & 8 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Core
``SECURITY_URL_PREFIX`` Specifies the URL prefix for the
Flask-Security blueprint. Defaults to
``None``.
``SECURITY_SUBDOMAIN`` Specifies the subdomain for the
``SECURITY_SUBDOMAIN`` Specifies the subdomain for the
Flask-Security blueprint. Defaults to
``None``.
``SECURITY_FLASH_MESSAGES`` Specifies whether or not to flash
Expand All @@ -42,13 +42,14 @@ Core
``bcrypt``.
``SECURITY_PASSWORD_SALT`` Specifies the HMAC salt. Defaults to
``None``.
``SECURITY_PASSWORD_SINGLE_HASH`` Specifies that passwords should only be
hashed once. By default, passwords are
``SECURITY_PASSWORD_SINGLE_HASH`` A list of schemes that should not be hashed
twice. By default, passwords are
hashed twice, first with
``SECURITY_PASSWORD_SALT``, and then
with a random salt. May be useful for
integrating with other applications.
Defaults to ``False``.
with a random salt.
Defaults to a list of known schemes
not working with double hashing
(`django_{digest}`, `plaintext`).
``SECURITY_HASHING_SCHEMES`` List of algorithms used for
creating and validating tokens.
Defaults to ``sha256_crypt``.
Expand Down Expand Up @@ -195,8 +196,8 @@ Feature Flags
``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user
login statistics. If set to ``True``, ensure your
models have the required fields/attributes
and make sure to commit changes after calling
``login_user``. Be sure to use `ProxyFix <http://flask.pocoo.org/docs/0.10/deploying/wsgi-standalone/#proxy-setups>`_ if you are using a proxy.
and make sure to commit changes after calling
``login_user``. Be sure to use `ProxyFix <http://flask.pocoo.org/docs/0.10/deploying/wsgi-standalone/#proxy-setups>`_ if you are using a proxy.
Defaults to ``False``
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the
passwordless login feature. If set to ``True``, users
Expand Down
12 changes: 11 additions & 1 deletion flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,17 @@
'flask_security', 'translations'),
'PASSWORD_HASH': 'bcrypt',
'PASSWORD_SALT': None,
'PASSWORD_SINGLE_HASH': False,
'PASSWORD_SINGLE_HASH': {
'django_argon2',
'django_bcrypt_sha256',
'django_pbkdf2_sha256',
'django_pbkdf2_sha1',
'django_bcrypt',
'django_salted_md5',
'django_salted_sha1',
'django_des_crypt',
'plaintext',
},
'LOGIN_URL': '/login',
'LOGOUT_URL': '/logout',
'REGISTER_URL': '/register',
Expand Down
13 changes: 6 additions & 7 deletions flask_security/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,17 +450,16 @@ def get_identity_attributes(app=None):

def use_double_hash(password_hash=None):
"""Return a bool indicating whether a password should be hashed twice."""
single_hash = config_value('PASSWORD_SINGLE_HASH')
if single_hash and _security.password_salt:
raise RuntimeError('You may not specify a salt with '
'SECURITY_PASSWORD_SINGLE_HASH')
# Default to plaintext for backward compatibility with
# SECURITY_PASSWORD_SINGLE_HASH = False
single_hash = config_value('PASSWORD_SINGLE_HASH') or {'plaintext'}

if password_hash is None:
is_plaintext = _security.password_hash == 'plaintext'
scheme = _security.password_hash
else:
is_plaintext = _pwd_context.identify(password_hash) == 'plaintext'
scheme = _pwd_context.identify(password_hash)

return not (is_plaintext or single_hash)
return not (single_hash is True or scheme in single_hash)


@contextmanager
Expand Down
43 changes: 33 additions & 10 deletions tests/test_hashing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

from pytest import raises
from utils import authenticate, init_app_with_options
from passlib.hash import pbkdf2_sha256, django_pbkdf2_sha256, plaintext

from flask_security.utils import encrypt_password, verify_password
from flask_security.utils import encrypt_password, verify_password, get_hmac


def test_verify_password_bcrypt_double_hash(app, sqlalchemy_datastore):
Expand All @@ -32,6 +33,37 @@ def test_verify_password_bcrypt_single_hash(app, sqlalchemy_datastore):
assert verify_password('pass', encrypt_password('pass'))


def test_verify_password_single_hash_list(app, sqlalchemy_datastore):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SALT': 'salty',
'SECURITY_PASSWORD_SINGLE_HASH': ['django_pbkdf2_sha256', 'plaintext'],
'SECURITY_PASSWORD_SCHEMES': [
'bcrypt', 'pbkdf2_sha256', 'django_pbkdf2_sha256', 'plaintext'
]
})
with app.app_context():
# double hash
assert verify_password('pass', encrypt_password('pass'))
assert verify_password('pass', pbkdf2_sha256.hash(get_hmac('pass')))
# single hash
assert verify_password('pass', django_pbkdf2_sha256.hash('pass'))
assert verify_password('pass', plaintext.hash('pass'))


def test_verify_password_backward_compatibility(app, sqlalchemy_datastore):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SINGLE_HASH': False,
'SECURITY_PASSWORD_SCHEMES': ['bcrypt', 'plaintext']
})
with app.app_context():
# double hash
assert verify_password('pass', encrypt_password('pass'))
# single hash
assert verify_password('pass', plaintext.hash('pass'))


def test_verify_password_bcrypt_rounds_too_low(app, sqlalchemy_datastore):
with raises(ValueError) as exc_msg:
init_app_with_options(app, sqlalchemy_datastore, **{
Expand Down Expand Up @@ -59,12 +91,3 @@ def test_missing_hash_salt_option(app, sqlalchemy_datastore):
'SECURITY_PASSWORD_SALT': None,
'SECURITY_PASSWORD_SINGLE_HASH': False,
})


def test_single_hash_should_have_no_salt(app, sqlalchemy_datastore):
with raises(RuntimeError):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SALT': 'salty',
'SECURITY_PASSWORD_SINGLE_HASH': True,
})

0 comments on commit 5123f8e

Please sign in to comment.