diff --git a/openedx/core/djangoapps/user_authn/urls_common.py b/openedx/core/djangoapps/user_authn/urls_common.py index 2edf28f24b..e8ca7dcc41 100644 --- a/openedx/core/djangoapps/user_authn/urls_common.py +++ b/openedx/core/djangoapps/user_authn/urls_common.py @@ -65,13 +65,13 @@ urlpatterns = [ url(r'^account/password$', password_reset.password_change_request_handler, name='password_change_request'), # logistration MFE flow - url(r'^user_api/v1/account/password_reset/token/validate/$', password_reset.password_reset_token_validate, + url(r'^user_api/v1/account/password_reset/token/validate/$', password_reset.PasswordResetTokenValidation.as_view(), name="user_api_password_reset_token_validate"), # logistration MFE reset flow url( r'^password/reset/(?P[0-9A-Za-z]+)-(?P.+)/$', - password_reset.password_reset_logistration, + password_reset.LogistrationPasswordResetView.as_view(), name='logistration_password_reset', ), ] diff --git a/openedx/core/djangoapps/user_authn/views/password_reset.py b/openedx/core/djangoapps/user_authn/views/password_reset.py index cf6f8548ed..268639adf0 100644 --- a/openedx/core/djangoapps/user_authn/views/password_reset.py +++ b/openedx/core/djangoapps/user_authn/views/password_reset.py @@ -25,6 +25,7 @@ from edx_ace import ace from edx_ace.recipient import Recipient from eventtracking import tracker from ratelimit.decorators import ratelimit +from rest_framework.response import Response from rest_framework.views import APIView from common.djangoapps.edxmako.shortcuts import render_to_string @@ -641,107 +642,106 @@ def password_change_request_handler(request): return HttpResponseBadRequest(_("No email address provided.")) -@require_POST -@ensure_csrf_cookie -def password_reset_token_validate(request): - """HTTP end-point to validate password reset token. """ - is_valid = False - token = request.POST.get('token') - try: - token = token.split('-', 1) - uid_int = base36_to_int(token[0]) - if request.user.is_authenticated and request.user.id != uid_int: - return JsonResponse({'is_valid': is_valid}) +class PasswordResetTokenValidation(APIView): - user = User.objects.get(id=uid_int) - if UserRetirementRequest.has_user_requested_retirement(user): - return JsonResponse({'is_valid': is_valid}) + def post(self, request): + """ HTTP end-point to validate password reset token. """ + is_valid = False + token = request.data.get('token') + try: + token = token.split('-', 1) + uid_int = base36_to_int(token[0]) + if request.user.is_authenticated and request.user.id != uid_int: + return Response({'is_valid': is_valid}) - is_valid = default_token_generator.check_token(user, token[1]) - if is_valid and not user.is_active: - user.is_active = True - user.save() - except Exception: # pylint: disable=broad-except - AUDIT_LOG.exception("Invalid password reset confirm token") + user = User.objects.get(id=uid_int) + if UserRetirementRequest.has_user_requested_retirement(user): + return Response({'is_valid': is_valid}) - return JsonResponse({'is_valid': is_valid}) + is_valid = default_token_generator.check_token(user, token[1]) + if is_valid and not user.is_active: + user.is_active = True + user.save() + except Exception: # pylint: disable=broad-except + AUDIT_LOG.exception("Invalid password reset confirm token") + + return Response({'is_valid': is_valid}) -def _check_token_has_required_values(uidb36, token): - """ - Helper function to test that token - string passed has the required kwargs needed - to process token validation. - """ +class LogistrationPasswordResetView(APIView): - if not uidb36 or not token: - return False, None - try: - uid_int = base36_to_int(uidb36) - except ValueError: - return False, None - return True, uid_int + def post(self, request, **kwargs): + """ Reset learner password using passed token and new credentials """ + reset_status = False + uidb36 = kwargs.get('uidb36') + token = kwargs.get('token') -@require_POST -@ensure_csrf_cookie -def password_reset_logistration(request, **kwargs): - """Reset learner password using passed token and new credentials""" + has_required_values, uid_int = self._check_token_has_required_values(uidb36, token) + if not has_required_values: + AUDIT_LOG.exception("Invalid password reset confirm token") + return Response({'reset_status': reset_status}) - reset_status = False - uidb36 = kwargs.get('uidb36') - token = kwargs.get('token') + request.data._mutable = True + request.data['new_password1'] = normalize_password(request.data['new_password1']) + request.data['new_password2'] = normalize_password(request.data['new_password2']) - has_required_values, uid_int = _check_token_has_required_values(uidb36, token) - if not has_required_values: - AUDIT_LOG.exception("Invalid password reset confirm token") - return JsonResponse({'reset_status': reset_status}) + password = request.data['new_password1'] + try: + user = User.objects.get(id=uid_int) + if not default_token_generator.check_token(user, token): + AUDIT_LOG.exception("Token validation failed") + return Response({'reset_status': reset_status}) - request.POST = request.POST.copy() - request.POST['new_password1'] = normalize_password(request.POST['new_password1']) - request.POST['new_password2'] = normalize_password(request.POST['new_password2']) + validate_password(password, user=user) + form = SetPasswordForm(user, request.data) + if form.is_valid(): + form.save() + reset_status = True - password = request.POST['new_password1'] - try: - user = User.objects.get(id=uid_int) - if not default_token_generator.check_token(user, token): - AUDIT_LOG.exception("Token validation failed") - return JsonResponse({'reset_status': reset_status}) + if 'is_account_recovery' in request.GET: + try: + old_primary_email = user.email + user.email = user.account_recovery.secondary_email + user.account_recovery.delete() + # emit an event that the user changed their secondary email to the primary email + tracker.emit( + SETTING_CHANGE_INITIATED, + { + "setting": "email", + "old": old_primary_email, + "new": user.email, + "user_id": user.id, + } + ) + user.save() + send_password_reset_success_email(user, request) + except ObjectDoesNotExist: + err = 'Account recovery process initiated without AccountRecovery instance for user {username}' + log.error(err.format(username=user.username)) + except ValidationError as err: + AUDIT_LOG.exception("Password validation failed") + error_status = { + 'reset_status': reset_status, + 'err_msg': ' '.join(err.messages) + } + return Response(error_status) + except Exception: # pylint: disable=broad-except + AUDIT_LOG.exception("Setting new password failed") - validate_password(password, user=user) - form = SetPasswordForm(user, request.POST) - if form.is_valid(): - form.save() - reset_status = True + return Response({'reset_status': reset_status}) - if 'is_account_recovery' in request.GET: - try: - old_primary_email = user.email - user.email = user.account_recovery.secondary_email - user.account_recovery.delete() - # emit an event that the user changed their secondary email to the primary email - tracker.emit( - SETTING_CHANGE_INITIATED, - { - "setting": "email", - "old": old_primary_email, - "new": user.email, - "user_id": user.id, - } - ) - user.save() - send_password_reset_success_email(user, request) - except ObjectDoesNotExist: - log.error('Account recovery process initiated without AccountRecovery instance for user {username}' - .format(username=user.username)) - except ValidationError as err: - AUDIT_LOG.exception("Password validation failed") - error_status = { - 'reset_status': reset_status, - 'err_msg': ' '.join(err.messages) - } - return JsonResponse(error_status) - except Exception: # pylint: disable=broad-except - AUDIT_LOG.exception("Setting new password failed") + def _check_token_has_required_values(self, uidb36, token): + """ + Helper function to test that token + string passed has the required kwargs needed + to process token validation. + """ - return JsonResponse({'reset_status': reset_status}) + if not uidb36 or not token: + return False, None + try: + uid_int = base36_to_int(uidb36) + except ValueError: + return False, None + return True, uid_int diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py b/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py index 947a98c3da..e882545cb8 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_reset_password.py @@ -34,7 +34,7 @@ from openedx.core.djangoapps.user_api.models import UserRetirementRequest from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH, EMAIL_MIN_LENGTH from openedx.core.djangoapps.user_authn.views.password_reset import ( - SETTING_CHANGE_INITIATED, password_reset, password_reset_logistration, + SETTING_CHANGE_INITIATED, password_reset, LogistrationPasswordResetView, PasswordResetConfirmWrapper) from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from common.djangoapps.student.tests.factories import TEST_PASSWORD, UserFactory @@ -754,7 +754,7 @@ class ResetPasswordAPITests(EventTestMixin, CacheIsolationTestCase): "logistration_password_reset", kwargs={"uidb36": uidb36, "token": token} ) + query_param, - request_param + request_param, format='json' ) return post_request @@ -771,7 +771,8 @@ class ResetPasswordAPITests(EventTestMixin, CacheIsolationTestCase): post_request = self.create_reset_request(uidb36, token, False) post_request.user = AnonymousUser() - json_response = password_reset_logistration(post_request, uidb36=uidb36, token=token) + reset_view = LogistrationPasswordResetView.as_view() + json_response = reset_view(post_request, uidb36=uidb36, token=token).render() json_response = json.loads(json_response.content.decode('utf-8')) self.assertEqual(json_response.get('reset_status'), status) @@ -784,7 +785,8 @@ class ResetPasswordAPITests(EventTestMixin, CacheIsolationTestCase): post_request = self.create_reset_request(self.uidb36, self.token, False) post_request.user = AnonymousUser() - self.assertRaises(Exception, password_reset_logistration(post_request, uidb36=uidb36, token=token)) + reset_view = LogistrationPasswordResetView.as_view() + self.assertRaises(Exception, reset_view(post_request, uidb36=uidb36, token=token)) def test_password_mismatch_in_reset_request(self): """ @@ -792,7 +794,8 @@ class ResetPasswordAPITests(EventTestMixin, CacheIsolationTestCase): """ post_request = self.create_reset_request(self.uidb36, self.token, False, 'new_password2') post_request.user = AnonymousUser() - json_response = password_reset_logistration(post_request, uidb36=self.uidb36, token=self.token) + reset_view = LogistrationPasswordResetView.as_view() + json_response = reset_view(post_request, uidb36=self.uidb36, token=self.token).render() json_response = json.loads(json_response.content.decode('utf-8')) self.assertFalse(json_response.get('reset_status')) @@ -803,7 +806,8 @@ class ResetPasswordAPITests(EventTestMixin, CacheIsolationTestCase): """ post_request = self.create_reset_request(self.uidb36, self.token, True) post_request.user = AnonymousUser() - password_reset_logistration(post_request, uidb36=self.uidb36, token=self.token) + reset_view = LogistrationPasswordResetView.as_view() + reset_view(post_request, uidb36=self.uidb36, token=self.token) updated_user = User.objects.get(id=self.user.id) self.assertEqual(updated_user.email, self.secondary_email) @@ -824,7 +828,8 @@ class ResetPasswordAPITests(EventTestMixin, CacheIsolationTestCase): post_request = self.create_reset_request(self.uidb36, self.token, True) post_request.user = AnonymousUser() post_request.site = Mock(domain='example.com') - password_reset_logistration(post_request, uidb36=self.uidb36, token=self.token) + reset_view = LogistrationPasswordResetView.as_view() + reset_view(post_request, uidb36=self.uidb36, token=self.token) updated_user = User.objects.get(id=self.user.id) from_email = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)