def post(self): """ Authenticate the user, logout the current user, login the new user :return: dict containing success message """ try: data = get_post_data(request) email = data['username'] password = data['password'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 u = user_manipulator.first(email=email) if u is None or not u.validate_password(password): abort(401) if u.confirmed_at is None: return {"error": "account has not been verified"}, 403 # Logout of previous user (may have been bumblebee) if current_user.is_authenticated(): logout_user() login_user(u) # Login to real user user_manipulator.update( u, last_login_at=datetime.datetime.now(), login_count=u.login_count + 1 if u.login_count else 1, ) return {"message": "success"}, 200
def login_oauth2_user(valid, oauth): """ Login a user after having been verified """ if valid: login_user(user_manipulator.first(id=oauth.user.id)) return valid, oauth
def post(self, token): """ Send the password reset email to the specified email address (recaptcha protected) Note that param "token" represents the raw email address of the recipient, and it is not expected to be encoded. :param token: email address of the recipient :type token: basestring """ if token == current_app.config['BOOTSTRAP_USER_EMAIL']: abort(403) try: data = get_post_data(request) reset_url = data['reset_url'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 if not verify_recaptcha(request): return {'error': 'captcha was not verified'}, 403 u = user_manipulator.first(email=token) if u is None: return {'error': 'no such user exists'}, 404 if not u.confirmed_at: return {'error': 'This email was never verified. It will be ' 'deleted from out database within a day'}, 403 send_email( email_addr=token, email_template=PasswordResetEmail, base_url=reset_url, payload=token ) return {"message": "success"}, 200
def get(self, token): """ Attempts to decode a verification token into an email. Responds with the resulting decoded email :param token: HMAC encoded string :type token: basestring """ try: email = current_app.ts.loads(token, max_age=86400, salt=PasswordResetEmail.salt) except: current_app.logger.warning( "Invalid Token {0} in ForgotPasswordView".format(token) ) return {"error": "unknown verification token"}, 404 # Check that the user still exists u = user_manipulator.first(email=email) if u is None: current_app.logger.error( "[GET] Reset password validated link," " but no user exists for {email}".format(email=email)) abort(400) return {"email": email}, 200
def post(self, token): """ Send the password reset email to the specified email address (recaptcha protected) Note that param "token" represents the raw email address of the recipient, and it is not expected to be encoded. :param token: email address of the recipient :type token: basestring """ if token == current_app.config['BOOTSTRAP_USER_EMAIL']: abort(403) try: data = get_post_data(request) reset_url = data['reset_url'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 if not verify_recaptcha(request): return {'error': 'captcha was not verified'}, 403 u = user_manipulator.first(email=token) if u is None: return {'error': 'no such user exists'}, 404 if not u.confirmed_at: return { 'error': 'This email was never verified. It will be ' 'deleted from out database within a day' }, 403 send_email(email_addr=token, email_template=PasswordResetEmail, base_url=reset_url, payload=token) return {"message": "success"}, 200
def post(self): """ Authenticate the user, logout the current user, login the new user :return: dict containing success message """ try: data = get_post_data(request) email = data['username'] password = data['password'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 u = user_manipulator.first(email=email) if u is None or not u.validate_password(password): abort(401) if u.confirmed_at is None: return {"error": "account has not been verified"}, 403 # Logout of previous user (may have been bumblebee) if current_user.is_authenticated(): logout_user() login_user(u) # Login to real user user_manipulator.update( u, last_login_at=datetime.datetime.now(), login_count=u.login_count+1 if u.login_count else 1, ) return {"message": "success"}, 200
def get(self, token): """ Attempts to decode a verification token into an email. Responds with the resulting decoded email :param token: HMAC encoded string :type token: basestring """ try: email = current_app.ts.loads(token, max_age=86400, salt=PasswordResetEmail.salt) except: current_app.logger.warning( "Invalid Token {0} in ForgotPasswordView".format(token)) return {"error": "unknown verification token"}, 404 # Check that the user still exists u = user_manipulator.first(email=email) if u is None: current_app.logger.error( "[GET] Reset password validated link," " but no user exists for {email}".format(email=email)) abort(400) return {"email": email}, 200
class ChangePasswordView(Resource): """ Implements change password functionality """ decorators = [login_required] def post(self): """ Verify that the current user's password is correct, that the desired new password is valid, and finally update the password in the User object """ try: data = get_post_data(request) old_password = data['old_password'] new_password1 = data['new_password1'] new_password2 = data['new_password2'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 if not current_user.validate_password(old_password): return {'error': 'please verify your current password'}, 401 if new_password1 != new_password2: return {'error': 'passwords do not match'}, 400 try: validate_password(new_password1) except ValidationError, e: return {'error': 'validation error'}, 400 u = user_manipulator.first(email=current_user.email) user_manipulator.update(u, password=new_password1) return {'message': 'success'}, 200
def get(self): """ If the current user is unauthenticated, or the current user is the "bootstrap" (anon) user, return/create a "BB Client" OAuthClient and token depending if "oauth_client" is encoded into their session cookie If the user is a authenticated as a real user, return/create a "BB Client" OAuthClient and token depending if that user already has one in the database """ # rca: I'd like to register here my distaste for Flask-Restful and # how it divorces parameters; it was a big mistake to go with that framework # and the decision shouldn't have been left to inexperienced developers # this is not recommended solution, but even the recommended solution # is just awful: http://stackoverflow.com/questions/30779584/flask-restful-passing-parameters-to-get-request parser = reqparse.RequestParser() parser.add_argument('redirect_uri', type=str) parser.add_argument('scope', type=str) parser.add_argument('client_name', type=str) parser.add_argument('ratelimit', type=float) parser.add_argument('create_new', type=inputs.boolean) parser.add_argument('expires', type=str) kwargs = parser.parse_args() client_name = kwargs.get('client_name', None) redirect_uri = kwargs.get('redirect_uri', None) ratelimit = kwargs.get('ratelimit', 1.0) create_new = kwargs.get('create_new', False) expires = kwargs.get('expires', None) if ratelimit is None: ratelimit = 1.0 assert ratelimit >= 0.0 if expires is not None: expires = get_date(expires) # throwing error on parse OK else: expires = datetime.datetime(2500, 1, 1) # If we visit this endpoint and are unauthenticated, then login as # our anonymous user if not current_user.is_authenticated(): login_user(user_manipulator.first( email=current_app.config['BOOTSTRAP_USER_EMAIL'] )) if current_user.email == current_app.config['BOOTSTRAP_USER_EMAIL']: if 'scopes' in kwargs or client_name or redirect_uri: abort(401, "Sorry, you cant change scopes/name/redirect_uri when creating temporary OAuth application") try: scopes = self._sanitize_scopes(kwargs.get('scope', None)) except ValidationError, e: return {'error': e.value}, 400
def post(self): """ POST desired email and password to change the current user's email. Checks that the desired new email isn't already registerd This will create the new user. The encoded email payload will have a second argument `id` which is the id of the user making this request. We assume that the view responsible for verifying emails knows what to do with this extra argument. This should be deprecated by using signals in the future. """ try: data = get_post_data(request) email = data['email'] password = data['password'] verify_url = data['verify_url'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 u = user_manipulator.first(email=current_user.email) if not u.validate_password(password): abort(401) if user_manipulator.first(email=email) is not None: return { "error": "{0} has already been registered".format(email) }, 403 send_email( email_addr=email, base_url=verify_url, email_template=VerificationEmail, payload=[email, u.id] ) send_email( email_addr=current_user.email, email_template=EmailChangedNotification ) user_manipulator.create( email=email, password=password, active=True, registered_at=datetime.datetime.now(), login_count=0, ) return {"message": "success"}, 200
def post(self): """ Delete the current user's account use POST instead of GET to enable csrf validation """ u = user_manipulator.first(email=current_user.email) logout_user() user_manipulator.delete(u) return {"message": "success"}, 200
def test_register_user(self): """ test user registration """ url = url_for('userregistrationview') with self.client as c: csrf = self.get_csrf() self.setup_google_recaptcha_response() # posting without a csrf token should return 400 r = c.post(url) self.assertStatus(r, 400) # Giving incorrect input should return 400 payload = {'email': 'me@email'} r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf} ) self.assertStatus(r, 400) # Test that registering an already registered email returns 409 payload = { 'email': self.real_user.email, 'password1': 'Password1', 'password2': 'Password1', 'g-recaptcha-response': 'correct_response', 'verify_url': 'http://not_relevant.com' } r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf} ) self.assertStatus(r, 409) # Test a valid new user registration payload = { 'email': 'me@email', 'password1': 'Password1', 'password2': 'Password1', 'g-recaptcha-response': 'correct_response', 'verify_url': 'http://not_relevant.com' } r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf} ) self.assertStatus(r, 200) u = user_manipulator.first(email="me@email") self.assertIsNotNone(u) self.assertIsNone(u.confirmed_at)
def get(self, token): try: email = current_app.ts.loads(token, max_age=86400, salt=VerificationEmail.salt) except: current_app.logger.warning( "{0} verification token not validated".format(token)) return {"error": "unknown verification token"}, 404 # This logic is necessary to de-activate accounts via the change-email # workflow. This strong coupling should be deprecated by using signals. previous_uid = None if " " in email: email, previous_uid = email.split() u = user_manipulator.first(email=email) if u is None: return { "error": "no user associated " "with that verification token" }, 404 if u.confirmed_at is not None: return { "error": "this user and email " "has already been validated" }, 400 if previous_uid: # De-activate previous accounts by deleting the account associated # with the new email address, then update the old account with the # new email address. Again, this should be deprecated with signals. user_manipulator.delete(u) u = user_manipulator.first(id=previous_uid) user_manipulator.update(u, email=email, confirmed_at=datetime.datetime.now()) else: user_manipulator.update(u, confirmed_at=datetime.datetime.now()) login_user(u) return {"message": "success", "email": email}
class UserRegistrationView(Resource): """ Implements new user registration """ decorators = [ ratelimit.shared_limit_and_check("50/600 second", scope=scope_func) ] def post(self): """ Standard user registration workflow; verifies that the email is available, creates a de-activated accounts, and sends verification email that serves to activate said account """ try: data = get_post_data(request) email = data['email'] password = data['password1'] repeated = data['password2'] verify_url = data['verify_url'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 if not verify_recaptcha(request): return {'error': 'captcha was not verified'}, 403 if password != repeated: return {'error': 'passwords do not match'}, 400 try: validate_email(email) validate_password(password) except ValidationError, e: return {'error': 'validation error'}, 400 if user_manipulator.first(email=email) is not None: return { 'error': 'an account is already' ' registered for {0}'.format(email) }, 409 send_email(email_addr=email, base_url=verify_url, email_template=WelcomeVerificationEmail, payload=email) u = user_manipulator.create( email=email, password=password, active=True, registered_at=datetime.datetime.now(), login_count=0, ) return {"message": "success"}, 200
def load_user(username, password, *args, **kwargs): """ Loads the user (resource owner) User getter is optional. It is only required if you need password credential authorization: Needed for grant type 'password'. Note, grant type password is by default disabled. """ user = user_manipulator.first(email=username) if user.validate_password(password): return user
def get(self, token): try: email = current_app.ts.loads(token, max_age=86400, salt=VerificationEmail.salt) except: current_app.logger.warning( "{0} verification token not validated".format(token) ) return {"error": "unknown verification token"}, 404 # This logic is necessary to de-activate accounts via the change-email # workflow. This strong coupling should be deprecated by using signals. previous_uid = None if " " in email: email, previous_uid = email.split() u = user_manipulator.first(email=email) if u is None: return {"error": "no user associated " "with that verification token"}, 404 if u.confirmed_at is not None: return {"error": "this user and email " "has already been validated"}, 400 if previous_uid: # De-activate previous accounts by deleting the account associated # with the new email address, then update the old account with the # new email address. Again, this should be deprecated with signals. user_manipulator.delete(u) u = user_manipulator.first(id=previous_uid) user_manipulator.update( u, email=email, confirmed_at=datetime.datetime.now() ) else: user_manipulator.update(u, confirmed_at=datetime.datetime.now()) login_user(u) return {"message": "success", "email": email}
def test_users_crud_operations(self): """ perform and test create, read, update, and delete patterns on user models using the `user_manipulator` service """ # .new() should not save the User to the database joe = user_manipulator.new(email='joe') self.assertIsNone(user_manipulator.first(email='joe')) # .save() should save the User to the database user_manipulator.save(joe) u = user_manipulator.first(email='joe') self.assertIsNotNone(u) self.assertEqual(u.email, 'joe') # .create() should create immediately elias = user_manipulator.create(email='elias') u = user_manipulator.first(email='elias') self.assertIsNotNone(u) self.assertEqual(elias, u) # .update() should update immediately user_manipulator.update(elias, confirmed_at=datetime(2000, 1, 1)) u = user_manipulator.first(email='elias') self.assertEqual(u.confirmed_at, datetime(2000, 1, 1)) self.assertEqual(elias, u) # .delete() should delete immediately user_manipulator.delete(elias) u = user_manipulator.first(email='elias') self.assertIsNone(u) # even though this object was deleted in the db, we still should # have a reference to the python object self.assertIsNotNone(elias) self.assertEqual(elias.confirmed_at, datetime(2000, 1, 1))
def test_register_user(self): """ test user registration """ url = url_for('userregistrationview') with self.client as c: csrf = self.get_csrf() self.setup_google_recaptcha_response() # posting without a csrf token should return 400 r = c.post(url) self.assertStatus(r, 400) # Giving incorrect input should return 400 payload = {'email': 'me@email'} r = c.post(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 400) # Test that registering an already registered email returns 409 payload = { 'email': self.real_user.email, 'password1': 'Password1', 'password2': 'Password1', 'g-recaptcha-response': 'correct_response', 'verify_url': 'http://not_relevant.com' } r = c.post(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 409) # Test a valid new user registration payload = { 'email': 'me@email', 'password1': 'Password1', 'password2': 'Password1', 'g-recaptcha-response': 'correct_response', 'verify_url': 'http://not_relevant.com' } r = c.post(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 200) u = user_manipulator.first(email="me@email") self.assertIsNotNone(u) self.assertIsNone(u.confirmed_at)
def get(self): """ If the current user is unauthenticated, or the current user is the "bootstrap" (anon) user, return/create a "BB Client" OAuthClient and token depending if "oauth_client" is encoded into their session cookie If the user is a authenticated as a real user, return/create a "BB Client" OAuthClient and token depending if that user already has one in the database """ # If we visit this endpoint and are unauthenticated, then login as # our anonymous user if not current_user.is_authenticated(): login_user(user_manipulator.first( email=current_app.config['BOOTSTRAP_USER_EMAIL'] )) if current_user.email == current_app.config['BOOTSTRAP_USER_EMAIL']: try: if 'oauth_client' in session: client, token = Bootstrap.load_client( session['oauth_client'] ) else: raise NoClientError('client/user mismatch') if client.user_id != int(current_user.get_id()): raise NoClientError("client/user mistmatch") except (NoTokenError, NoClientError): client, token = Bootstrap.bootstrap_bumblebee() session['oauth_client'] = client.client_id else: client, token = Bootstrap.bootstrap_user() client.last_activity = datetime.datetime.now() db.session.commit() return print_token(token)
def test_delete_account(self): """ Test the delete account workflow """ url = url_for('deleteaccountview') with self.client as c: csrf = self.get_csrf() # CSRF not passed; should return 400 r = c.post(url) self.assertStatus(r, 400) # Not authenticated; should 401 r = c.post(url, headers={'X-CSRFToken': csrf}) self.assertStatus(r, 401) # Login and passing the correct csrf should delete the acct self.login(self.real_user, c, csrf) r = c.post(url, headers={'X-CSRFToken': csrf}) self.assertStatus(r, 200) u = user_manipulator.first(email=self.real_user.email) self.assertIsNone(u)
def test_delete_account(self): """ Test the delete account workflow """ url = url_for('deleteaccountview') with self.client as c: csrf = self.get_csrf() # CSRF not passed; should return 400 r = c.post(url) self.assertStatus(r, 400) # Not authenticated; should 401 r = c.post(url,headers={'X-CSRFToken':csrf}) self.assertStatus(r,401) # Login and passing the correct csrf should delete the acct self.login(self.real_user, c, csrf) r = c.post(url,headers={'X-CSRFToken':csrf}) self.assertStatus(r, 200) u = user_manipulator.first(email=self.real_user.email) self.assertIsNone(u)
def test_change_email(self): """ Test the change email workflow. The workflow is tightly coupled with the verify-email workflow, which should be de-coupled by using signals in the future """ url = url_for('changeemailview') with self.client as c: csrf = self.get_csrf() self.setup_google_recaptcha_response() # Unauthenticated should return 401 r = c.post(url, headers={'X-CSRFToken': csrf}) self.assertStatus(r, 401) self.login(self.real_user, c, csrf) # incorrect password, even though we're logged in should return 401 payload = { 'email': self.real_user.email, 'password': '******', 'verify_url': 'http://not_relevant.com' } r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}, ) self.assertStatus(r, 401) # correct password, but that email is already registered payload = { 'email': self.bootstrap_user.email, 'password': self.passwords[self.real_user], 'verify_url': 'http://not_relevant.com' } r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}, ) self.assertStatus(r, 403) # valid end-to-end workflow previous_email = self.real_user.email payload = { 'email': 'changed@email', 'password': self.passwords[self.real_user], 'verify_url': 'http://not_relevant.com' } r = c.post(url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}) self.assertStatus(r, 200) u = user_manipulator.first(email='changed@email') self.assertIsNotNone(u) self.assertIsNone(u.confirmed_at) self.assertIsNotNone(user_manipulator.first(email=previous_email)) self.assertIsNotNone( user_manipulator.first(email=previous_email).confirmed_at) self.assertNotEqual(self.real_user.email, "changed@email") # Get the token that this view will send, send it to the # verfication email endpoint, and check that the user's email # was correctly updated msg, token = utils.send_email( email_addr="changed@email", email_template=VerificationEmail, payload=["changed@email", self.real_user.id], ) url = url_for('verifyemailview', token=token) r = self.client.get(url) self.assertStatus(r, 200) self.assertIsNone(user_manipulator.first(email=previous_email)) self.assertEqual(self.real_user.email, "changed@email")
def get(self): """ If the current user is unauthenticated, or the current user is the "bootstrap" (anon) user, return/create a "BB Client" OAuthClient and token depending if "oauth_client" is encoded into their session cookie If the user is a authenticated as a real user, return/create a "BB Client" OAuthClient and token depending if that user already has one in the database """ # rca: I'd like to register here my distaste for Flask-Restful and # how it divorces parameters; it was a big mistake to go with that framework # and the decision shouldn't have been left to inexperienced developers # this is not recommended solution, but even the recommended solution # is just awful: http://stackoverflow.com/questions/30779584/flask-restful-passing-parameters-to-get-request parser = reqparse.RequestParser() parser.add_argument('redirect_uri', type=str) parser.add_argument('scope', type=str) parser.add_argument('client_name', type=str) kwargs = parser.parse_args() scopes = kwargs.get('scope', None) client_name = kwargs.get('client_name', None) redirect_uri = kwargs.get('redirect_uri', None) # If we visit this endpoint and are unauthenticated, then login as # our anonymous user if not current_user.is_authenticated(): login_user(user_manipulator.first( email=current_app.config['BOOTSTRAP_USER_EMAIL'] )) if scopes or client_name: abort(401, "Sorry, you cant change scopes/name/redirect_uri of this user") if current_user.email == current_app.config['BOOTSTRAP_USER_EMAIL']: try: if 'oauth_client' in session: client, token = Bootstrap.load_client( session['oauth_client'] ) else: raise NoClientError('client/user mismatch') if client.user_id != int(current_user.get_id()): raise NoClientError("client/user mistmatch") except (NoTokenError, NoClientError): client, token = Bootstrap.bootstrap_bumblebee() session['oauth_client'] = client.client_id else: client, token = Bootstrap.bootstrap_user() if scopes: client._default_scopes = scopes if redirect_uri: client._redirect_uris = redirect_uri if client_name: client.client_name = client_name client.last_activity = datetime.datetime.now() db.session.commit() output = print_token(token) output['client_id'] = client.client_id output['client_secret'] = client.client_secret return output
def get(self): """ If the current user is unauthenticated, or the current user is the "bootstrap" (anon) user, return/create a "BB Client" OAuthClient and token depending if "oauth_client" is encoded into their session cookie If the user is a authenticated as a real user, return/create a "BB Client" OAuthClient and token depending if that user already has one in the database """ # rca: I'd like to register here my distaste for Flask-Restful and # how it divorces parameters; it was a big mistake to go with that framework # and the decision shouldn't have been left to inexperienced developers # this is not recommended solution, but even the recommended solution # is just awful: http://stackoverflow.com/questions/30779584/flask-restful-passing-parameters-to-get-request parser = reqparse.RequestParser() parser.add_argument('redirect_uri', type=str) parser.add_argument('scope', type=str) parser.add_argument('client_name', type=str) kwargs = parser.parse_args() scopes = kwargs.get('scope', None) client_name = kwargs.get('client_name', None) redirect_uri = kwargs.get('redirect_uri', None) # If we visit this endpoint and are unauthenticated, then login as # our anonymous user if not current_user.is_authenticated(): login_user( user_manipulator.first( email=current_app.config['BOOTSTRAP_USER_EMAIL'])) if scopes or client_name: abort( 401, "Sorry, you cant change scopes/name/redirect_uri of this user" ) if current_user.email == current_app.config['BOOTSTRAP_USER_EMAIL']: try: if 'oauth_client' in session: client, token = Bootstrap.load_client( session['oauth_client']) else: raise NoClientError('client/user mismatch') if client.user_id != int(current_user.get_id()): raise NoClientError("client/user mistmatch") except (NoTokenError, NoClientError): client, token = Bootstrap.bootstrap_bumblebee() session['oauth_client'] = client.client_id else: client, token = Bootstrap.bootstrap_user() if scopes: client._default_scopes = scopes if redirect_uri: client._redirect_uris = redirect_uri if client_name: client.client_name = client_name client.last_activity = datetime.datetime.now() db.session.commit() output = print_token(token) output['client_id'] = client.client_id output['client_secret'] = client.client_secret return output
class ForgotPasswordView(Resource): """ Implements "reset password" functionality """ def get(self, token): """ Attempts to decode a verification token into an email. Responds with the resulting decoded email :param token: HMAC encoded string :type token: basestring """ try: email = current_app.ts.loads(token, max_age=86400, salt=PasswordResetEmail.salt) except: current_app.logger.warning( "Invalid Token {0} in ForgotPasswordView".format(token)) return {"error": "unknown verification token"}, 404 # Check that the user still exists u = user_manipulator.first(email=email) if u is None: current_app.logger.error( "[GET] Reset password validated link," " but no user exists for {email}".format(email=email)) abort(400) return {"email": email}, 200 def post(self, token): """ Send the password reset email to the specified email address (recaptcha protected) Note that param "token" represents the raw email address of the recipient, and it is not expected to be encoded. :param token: email address of the recipient :type token: basestring """ if token == current_app.config['BOOTSTRAP_USER_EMAIL']: abort(403) try: data = get_post_data(request) reset_url = data['reset_url'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 if not verify_recaptcha(request): return {'error': 'captcha was not verified'}, 403 u = user_manipulator.first(email=token) if u is None: return {'error': 'no such user exists'}, 404 if not u.confirmed_at: return { 'error': 'This email was never verified. It will be ' 'deleted from out database within a day' }, 403 send_email(email_addr=token, email_template=PasswordResetEmail, base_url=reset_url, payload=token) return {"message": "success"}, 200 def put(self, token): """ Check if the current user has the same email as the decoded token, and, if so, change that user's password to that specified in the PUT body :param token: HMAC encoded email string :type token: basestring """ try: email = current_app.ts.loads(token, max_age=86400, salt=PasswordResetEmail.salt) except: current_app.logger.critical( "PUT on reset-password with invalid token. " "This may indicate a brute-force attack!") return {"error": "unknown verification token"}, 404 try: data = get_post_data(request) new_password1 = data['password1'] new_password2 = data['password2'] except (AttributeError, KeyError): return {'error': 'malformed request'}, 400 if new_password1 != new_password2: return {'error': 'passwords do not match'}, 400 try: validate_password(new_password1) except ValidationError, e: return {'error': 'validation error'}, 400 u = user_manipulator.first(email=email) if u is None: current_app.logger.error( "[PUT] Reset password validated link," " but no user exists for {email}".format(email=email)) abort(500) user_manipulator.update(u, password=new_password1) logout_user() login_user(u) return {"message": "success"}, 200
def test_change_email(self): """ Test the change email workflow. The workflow is tightly coupled with the verify-email workflow, which should be de-coupled by using signals in the future """ url = url_for('changeemailview') with self.client as c: csrf = self.get_csrf() self.setup_google_recaptcha_response() # Unauthenticated should return 401 r = c.post(url, headers={'X-CSRFToken': csrf}) self.assertStatus(r, 401) self.login(self.real_user, c, csrf) # incorrect password, even though we're logged in should return 401 payload = { 'email': self.real_user.email, 'password': '******', 'verify_url': 'http://not_relevant.com' } r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}, ) self.assertStatus(r, 401) # correct password, but that email is already registered payload = { 'email': self.bootstrap_user.email, 'password': self.passwords[self.real_user], 'verify_url': 'http://not_relevant.com' } r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf}, ) self.assertStatus(r, 403) # valid end-to-end workflow previous_email = self.real_user.email payload = { 'email': 'changed@email', 'password': self.passwords[self.real_user], 'verify_url': 'http://not_relevant.com' } r = c.post( url, data=json.dumps(payload), headers={'X-CSRFToken': csrf} ) self.assertStatus(r, 200) u = user_manipulator.first(email='changed@email') self.assertIsNotNone(u) self.assertIsNone(u.confirmed_at) self.assertIsNotNone( user_manipulator.first(email=previous_email) ) self.assertIsNotNone( user_manipulator.first(email=previous_email).confirmed_at ) self.assertNotEqual(self.real_user.email, "changed@email") # Get the token that this view will send, send it to the # verfication email endpoint, and check that the user's email # was correctly updated msg, token = utils.send_email( email_addr="changed@email", email_template=VerificationEmail, payload = ["changed@email", self.real_user.id], ) url = url_for('verifyemailview', token=token) r = self.client.get(url) self.assertStatus(r, 200) self.assertIsNone( user_manipulator.first(email=previous_email) ) self.assertEqual(self.real_user.email, "changed@email")