diff --git a/docs/index.rst b/docs/index.rst index 20ab865..fd25cf2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -114,7 +114,7 @@ using SQLAlchemy.:: 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(120)) + password = db.Column(db.String(255)) remember_token = db.Column(db.String(255)) active = db.Column(db.Boolean()) authentication_token = db.Column(db.String(255)) diff --git a/example/app.py b/example/app.py index 69e0b6e..e739c6d 100644 --- a/example/app.py +++ b/example/app.py @@ -136,7 +136,7 @@ def create_sqlalchemy_app(auth_config=None, register_blueprint=True): 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(120)) + password = db.Column(db.String(255)) remember_token = db.Column(db.String(255)) last_login_at = db.Column(db.DateTime()) current_login_at = db.Column(db.DateTime()) @@ -180,7 +180,7 @@ def create_mongoengine_app(auth_config=None): class User(db.Document, UserMixin): email = db.StringField(unique=True, max_length=255) - password = db.StringField(required=True, max_length=120) + password = db.StringField(required=True, max_length=255) remember_token = db.StringField(max_length=255) last_login_at = db.DateTimeField() current_login_at = db.DateTimeField() diff --git a/flask_security/core.py b/flask_security/core.py index 50a1b71..f8612b6 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -21,7 +21,7 @@ from werkzeug.local import LocalProxy from . import views, exceptions from .confirmable import requires_confirmation -from .utils import config_value as cv, get_config +from .utils import config_value as cv, get_config, verify_password # Convenient references _security = LocalProxy(lambda: current_app.extensions['security']) @@ -32,6 +32,8 @@ _default_config = { 'URL_PREFIX': None, 'FLASH_MESSAGES': True, 'PASSWORD_HASH': 'plaintext', + 'PASSWORD_SALT': None, + 'PASSWORD_HMAC': False, 'AUTH_URL': '/auth', 'LOGOUT_URL': '/logout', 'REGISTER_URL': '/register', @@ -308,7 +310,9 @@ class AuthenticationProvider(object): raise exceptions.BadCredentialsError('Account requires confirmation') # compare passwords - if _security.pwd_context.verify(password, user.password): + if verify_password(password, user.password, + salt=_security.password_salt, + use_hmac=_security.password_hmac): return user # bad match diff --git a/flask_security/datastore.py b/flask_security/datastore.py index 98d5213..e709cb0 100644 --- a/flask_security/datastore.py +++ b/flask_security/datastore.py @@ -113,7 +113,10 @@ class UserDatastore(object): pw = kwargs['password'] if not pwd_context.identify(pw): - kwargs['password'] = pwd_context.encrypt(pw) + pwd_hash = utils.encrypt_password(pw, + salt=_security.password_salt, + use_hmac=_security.password_hmac) + kwargs['password'] = pwd_hash kwargs['remember_token'] = utils.get_remember_token(kwargs['email'], kwargs['password']) diff --git a/flask_security/decorators.py b/flask_security/decorators.py index 93bfe8a..400cb0e 100644 --- a/flask_security/decorators.py +++ b/flask_security/decorators.py @@ -77,7 +77,9 @@ def _check_http_auth(): except UserNotFoundError: return False - rv = _security.pwd_context.verify(auth.password, user.password) + rv = utils.verify_password(auth.password, user.password, + salt=_security.password_salt, + use_hmac=_security.password_hmac) if rv: identity_changed.send(current_app._get_current_object(), diff --git a/flask_security/recoverable.py b/flask_security/recoverable.py index 48fd1b5..8098d9b 100644 --- a/flask_security/recoverable.py +++ b/flask_security/recoverable.py @@ -16,7 +16,7 @@ from werkzeug.local import LocalProxy from .exceptions import ResetPasswordError, UserNotFoundError from .signals import password_reset, password_reset_requested, \ reset_instructions_sent -from .utils import send_mail, get_max_age, md5, get_message +from .utils import send_mail, get_max_age, md5, get_message, encrypt_password # Convenient references @@ -85,7 +85,10 @@ def reset_by_token(token, password): if md5(user.password) != data[1]: raise UserNotFoundError() - user.password = _security.pwd_context.encrypt(password) + user.password = encrypt_password(password, + salt=_security.password_salt, + use_hmac=_security.password_hmac) + print user.password _datastore._save_model(user) send_password_reset_notice(user) diff --git a/flask_security/utils.py b/flask_security/utils.py index fdbcf39..a1a1d0c 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -11,6 +11,7 @@ import base64 import hashlib +import hmac import os from contextlib import contextmanager from datetime import datetime, timedelta @@ -25,6 +26,23 @@ from .signals import user_registered, password_reset_requested # Convenient references _security = LocalProxy(lambda: current_app.extensions['security']) +_pwd_context = LocalProxy(lambda: _security.pwd_context) + + +def get_hmac(msg, salt=None, digestmod=None): + digestmod = digestmod or hashlib.sha512 + return base64.b64encode(hmac.new(salt, msg, digestmod).digest()) + + +def verify_password(password, password_hash, salt=None, use_hmac=False): + hmac_value = get_hmac(password, salt) if use_hmac else password + return _pwd_context.verify(hmac_value, password_hash) + + +def encrypt_password(password, salt=None, use_hmac=False): + hmac_value = get_hmac(password, salt) if use_hmac else password + return _pwd_context.encrypt(hmac_value) + def md5(data): return hashlib.md5(data).hexdigest() @@ -202,4 +220,4 @@ def capture_reset_password_requests(reset_password_sent_at=None): try: yield reset_requests finally: - password_reset_requested.disconnect(_on) + password_reset_requested.disconnect(_on) \ No newline at end of file diff --git a/tests/functional_tests.py b/tests/functional_tests.py index 87a51be..f0c0b62 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -157,6 +157,9 @@ class DefaultSecurityTests(SecurityTest): class ConfiguredSecurityTests(SecurityTest): AUTH_CONFIG = { + 'SECURITY_PASSWORD_HASH': 'bcrypt', + 'SECURITY_PASSWORD_SALT': 'so-salty', + 'SECURITY_PASSWORD_HMAC': True, 'SECURITY_REGISTERABLE': True, 'SECURITY_AUTH_URL': '/custom_auth', 'SECURITY_LOGOUT_URL': '/custom_logout',