Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions flask_user/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def hash_password(self, password):
return passwords.hash_password(self, password)

def get_password(self, user):
use_auth_class = True if self.db_adapter.UserAuthClass and hasattr(user, 'user_auth') else False
use_auth_class = self.db_adapter.UserAuthClass and hasattr(user, 'user_auth')
# Handle v0.5 backward compatibility
if self.db_adapter.UserProfileClass:
hashed_password = user.password
Expand All @@ -245,7 +245,7 @@ def get_password(self, user):
return hashed_password

def update_password(self, user, hashed_password):
use_auth_class = True if self.db_adapter.UserAuthClass and hasattr(user, 'user_auth') else False
use_auth_class = self.db_adapter.UserAuthClass and hasattr(user, 'user_auth')

if use_auth_class:
user.user_auth.password = hashed_password
Expand All @@ -261,6 +261,9 @@ def verify_password(self, password, user):
verified = False
hashed_password = self.get_password(user)

if not hashed_password:
return False

try:
verified = passwords.verify_password(self, password, hashed_password)
except ValueError:
Expand Down
24 changes: 23 additions & 1 deletion flask_user/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ class ChangePasswordForm(Form):
next = HiddenField()
submit = SubmitField(_('Change password'))

def __init__(self, formdata=None, obj=None, prefix='', data=None, meta=None, user=None, **kw):
self.user = user
return super(ChangePasswordForm, self).__init__(
formdata=formdata,
obj=obj,
prefix=prefix,
data=data,
meta=meta,
**kw
)

def validate(self):
# Use feature config to remove unused form fields
user_manager = current_app.user_manager
Expand Down Expand Up @@ -210,7 +221,7 @@ def validate(self):
user, user_email = user_manager.find_user_by_email(self.email.data)

# Handle successful authentication
if user and user.password and user_manager.verify_password(self.password.data, user):
if user and user_manager.verify_password(self.password.data, user):
Copy link
Author

@3noch 3noch Apr 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change minimizes the assumption that password is a field on the user entity. In reality, verify_password should never be returning True if the user has no password at all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For heterogeneous authorization systems, some users do not have a local password (such as those users relying on an external OAuth server). Without a defined password, an attribute error is raised when trying to pass self.password.data into verify_password - thus mandating the extra check.

Copy link
Author

@3noch 3noch Apr 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.password.data is on the form itself. I don't know how that could fail unless you're overriding the password input on the form, in which case you can override the validate method as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pbugni I see now that you added that check. I'm confused why you needed it. With my change (and the way it was before), you can always check user.password within the body of user_manager.verify_password if you override that method on the UserManager.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If self.password does not exist, then you have clearly changed the original form class and it would make sense that you need to also override the form's validate method to accomodate your changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@3noch - Your assertions are correct about form data - the problem is that when a user doesn't have a password and then they attempt to log in - an error is raised in the call to user_manager.verify_password. I'd forgotten the details when replying earlier today, but it's easy to reproduce - clear the db password for a user and attempt to log in - at the bottom of the error stack:

  File ".../env/local/lib/python2.7/site-packages/passlib/context.py", line 1447, in identify_record
    raise ExpectedStringError(hash, "hash")
TypeError: hash must be unicode or bytes, not None

The point of this check is clear - if the user doesn't have a password, don't bother trying to verify. Yes, you could alternatively patch the lower level checks, but this seemed like the clean work around to me.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can re-add that check to the default implementation of verify_password. Do you think that would solve your case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - that'd be great. I can test your branch when it's ready. Thanks.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pbugni Ok, I added that check to verify_password.

return True # Successful authentication

# Handle unsuccessful authentication
Expand Down Expand Up @@ -308,6 +319,17 @@ class ResetPasswordForm(Form):
next = HiddenField()
submit = SubmitField(_('Change password'))

def __init__(self, formdata=None, obj=None, prefix='', data=None, meta=None, user=None, **kw):
self.user = user
return super(ResetPasswordForm, self).__init__(
formdata=formdata,
obj=obj,
prefix=prefix,
data=data,
meta=meta,
**kw
)

def validate(self):
# Use feature config to remove unused form fields
user_manager = current_app.user_manager
Expand Down
11 changes: 4 additions & 7 deletions flask_user/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ def change_password():
db_adapter = user_manager.db_adapter

# Initialize form
form = user_manager.change_password_form(request.form)
form = user_manager.change_password_form(request.form, user=current_user)

form.next.data = request.args.get('next', _endpoint_url(user_manager.after_change_password_endpoint)) # Place ?next query param in next form field

# Process valid POST
Expand Down Expand Up @@ -580,7 +581,7 @@ def reset_password(token):
user_email.confirmed_at = datetime.utcnow()

# Initialize form
form = user_manager.reset_password_form(request.form)
form = user_manager.reset_password_form(request.form, user=user)

# Process valid POST
if request.method=='POST' and form.validate():
Expand All @@ -590,9 +591,7 @@ def reset_password(token):

# Change password
hashed_password = user_manager.hash_password(form.new_password.data)
user_auth = user.user_auth if db_adapter.UserAuthClass and hasattr(user, 'user_auth') else user
db_adapter.update_object(user_auth, password=hashed_password)
db_adapter.commit()
user_manager.update_password(user, hashed_password)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change also minimizes the assumption that password is a field on the user entity. It also makes the code more consistent I think because update_password is already a method on UserManager so I don't know why it was not being used here (even though it has the same logic).


# Send 'password_changed' email
if user_manager.enable_email and user_manager.send_password_changed_email:
Expand Down Expand Up @@ -733,5 +732,3 @@ def _endpoint_url(endpoint):
if endpoint:
url = url_for(endpoint)
return url