1

Goal: updating a user password safely and securely using RESTful services.

I am a bit confused about what the workflow, using best practices, should be for:
   1.) updating a password for a user who knows there existing password
   2.) resetting a password if a user forgot.
   3.) Are the URIs (resources) RESTful? For the purposes of this web app, I just need GET and POST for changes.

I believe my code might be redundant. The password update method (shown below) is not updating the password correctly. It is changing it, but when I try to login with the new password set in new_password, the password does not match. I similarly followed this STACKs answer by Michael Merickel for updating.

user = DBSession.query(User).filter_by(email=email).first() if user: user.password = new_password

Thank you for any insights / input. I'm new and want to code properly.

All of this is through HTML and not JSON.
Software: Python 2.7.9, Pyramid 1.5.7, SQLAlchemy 1.0.9


Resources: __init__.py

config.add_route('users', '/users')
config.add_route('user', '/users/{id:\d+}/{login}') #/{login} added login

config.add_route('reset_password', '/users/{username}/reset_password')#reset
config.add_route('new_password', '/users/{username}/new_password')#new
config.add_route('save_password', '/save_password')#new

views.py

#http://0.0.0.0:6432/users/dorisday/reset_password <--like this
@view_config(route_name='reset_password', renderer='templates/password_recovery.jinja2')
def forgot_password(request):
    if request.method == 'GET':
        username = request.params['username']
        if username is not None:
            user = api.retrieve_user(username)

        return {}

#http://0.0.0.0:6432/users/macycat/new_password <--like this
@view_config(route_name='new_password', request_method='GET', renderer='templates/new_password.jinja2')
def new_password(request):
    logged_in_userid  = authenticated_userid(request)
    if logged_in_userid is None:
        raise HTTPForbidden()
    user = api.retrieve_user(logged_in_userid)
    return {'user': user.username}

@view_config(route_name='save_password', request_method='POST', renderer='templates/new_password.jinja2')
def save_password(request):
    with transaction.manager:
        logged_in_userid  = authenticated_userid(request)
        if logged_in_userid is None:
            raise HTTPForbidden()
        user = api.retrieve_user(logged_in_userid)
        if 'form.submitted' in request.params:
            password = request.params['old_password']
            new_password = request.params['new_password']
            confirm_password = request.params['confirm_password']
            if new_password == confirm_password:
                continue
                if user is not None and user.validate_password(password): #encrypted way of checking password from DB
                    user.password = new_password 

                message = 'Whoops! Passwords do not match.'

        transaction.commit()
        raise HTTPSeeOther(location=request.route_url('login'))
        return {'message': message, 'new_password': new_password}

Hash Password in SQLALCHEMY DB scheme as shown here:

Storing and validating encrypted password for login in Pyramid

Community
  • 1
  • 1
thesayhey
  • 938
  • 3
  • 17
  • 38
  • you should use some authentication (oauth can authenticate and its still restful) ... you should also not actually change the password but email the associated email account the link to a "change password" type thing – Joran Beasley Dec 08 '15 at 20:41
  • @JoranBeasley I am using Pyramid's authentication code to login. As for the reset password, I have no idea how to do that. Can you point me to something that is not Django as I use SQLAlchemy and Pyramid. Maybe example pseudocode? That would be very helpful as a blueprint. Or links? thanks!!! – thesayhey Dec 08 '15 at 20:44

1 Answers1

6

1. Updating the password.

Normally, passwords are stored in the database in hashed form, so if somebody steals your database they can't easily get the passwords. From your code it's not clear where the hashing actually occurs, so you need to check whether user.password = new_password does the required magic or if you need to do that manually. It should be similar to the code which is used to originally create a user with a password.

The actual problem of you getting the "Whoops! Passwords do not match.'" message if that you forgot the else clause in the line preceding it:

            if user is not None and user.validate_password(password): #encrypted way of checking password from DB
                user.password = new_password 
            **else:**
                message = 'Whoops! Passwords do not match.'

2. Password reset for users who don't know their current password

A simple way to do that is to add a new field, say, password_reset_secret, to your User model. When a forgetful person types in their email, you find a User by email, populate the password_reset_secret with random un-guessable gibberish and send the user a email with a link to a special page, say https://yoursite.com/password_reset/jhg876jhgd87676

Upon receiving the email, user clicks on the link and visits the "secret" page - a this point you know they have access to the email address they typed in, since the URL is otherwise un-guessable and not linked to from anywhere. On that page is a form with a field for new password. When they type in a new password, you query the User object from the DB by the password_reset_secret from the URL and update its password. Done.

To make the password reset URL expire after a certain amount of time you can add another field field to your User model - say, 'password_reset_last_valid', set it to "now + 3 days" when creating the password reset hash and checking the field when the user tries to visit the link. If the field's value is in the past then you just pretend nothing is found.

To prevent the user to use the link multiple times you just clear the password_reset_secret and password_reset_last_valid fields once the user successfully changed their password.

3. Are the URIs restful?

No, they're not, but you probably should not worry about this at this stage :)

Sergey
  • 11,892
  • 2
  • 41
  • 52
  • When a user is created, the password is automatically hashed at creation. I do have a `validate_password()` function inside the db that then validates. See the link I added above. Does that work? Or should I add a the manual code inside the `new_password` code? Your explanation of 2. password reset is awesome! I still am unsure of what `password_reset_secret` would look like in the User model since it requires sending an email (text) to the user. I imagine the view code would simply ask for the user email and if valid send, elif error message. PS,Your explanation is so clear and enlightening. – thesayhey Dec 09 '15 at 15:50
  • @thesayhey: "When a user is created, the password is automatically hashed at creation" - so you need to ensure the password is also hashed when you set a new password. The `validate_password()`, I suppose, works by hashing a copy of the new password and comparing it with the old stored hashed password, but it doesn't modify the value which you assign to user.password, unless some magic is taking place. Just check in the database the contents of the user's `password` field after updating the password – Sergey Dec 09 '15 at 22:59
  • no only the validate password as shown in my link and the automatic hashing at creation. Should a new function exist within the table that hashes just like the creation method? As for the second part, I created a `retrieve_user_byemail(email)` method in the db that queries the user by email and returns the user object with all the user info. I assume this is what would be the email send link piece in views.py? If so, I'm uncertain about the automatic send and the jibberish portion you mentioned. – thesayhey Dec 09 '15 at 23:10
  • Yes, you need to hash the password every time you assign a value to the `password` field, something like `user.password = hashlib.sha224(new_password).hexdigest()` (although I'm afraid it's not the most bullet-proof way of hashing passwords, let's not complicate things right now) – Sergey Dec 10 '15 at 00:18
  • Gotcha. How about the second part? I'll rework the code in the meantime. – thesayhey Dec 10 '15 at 01:21
  • 1
    Second part: generate a unique ID using Python's uuid module (namely, str(uuid.uuid4()): https://docs.python.org/2/library/uuid.html, then send the email using pyramid_mailer: http://docs.pylonsproject.org/projects/pyramid-mailer/en/latest/ – Sergey Dec 10 '15 at 01:25
  • Is it possible to add an expiration to the email link or prevent a user from using the same link multiple times? OR disable to previous password once a `uuid` is created? – thesayhey Dec 22 '15 at 21:47
  • just a quick clarification: when you state "clear the ... fields once a user has successfully changed their password" do you mean reassigning `password_reset_secret` to `None` in the view code? – thesayhey Dec 22 '15 at 23:43