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
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 :)