Skip to content
Snippets Groups Projects
Commit 30bf95b0 authored by Shafqat Farhan's avatar Shafqat Farhan
Browse files

VAN-437 - Unlocking the learners upon successful password reset

parent fe37651d
No related branches found
No related tags found
No related merge requests found
......@@ -43,7 +43,7 @@ from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from openedx.core.djangoapps.user_authn.message_types import PasswordReset, PasswordResetSuccess
from openedx.core.djangolib.markup import HTML
from common.djangoapps.student.forms import send_account_recovery_email_for_user
from common.djangoapps.student.models import AccountRecovery
from common.djangoapps.student.models import AccountRecovery, LoginFailures
from common.djangoapps.util.json_request import JsonResponse
from common.djangoapps.util.password_policy_validators import normalize_password, validate_password
......@@ -505,6 +505,10 @@ class PasswordResetConfirmWrapper(PasswordResetConfirmView):
if password_reset_successful and is_account_recovery:
self._handle_password_creation(request, updated_user)
# Handles clearing the failed login counter upon password reset.
if LoginFailures.is_feature_enabled():
LoginFailures.clear_lockout_counter(updated_user)
send_password_reset_success_email(updated_user, request)
return response
......@@ -763,6 +767,11 @@ class LogistrationPasswordResetView(APIView): # lint-amnesty, pylint: disable=m
except ObjectDoesNotExist:
err = 'Account recovery process initiated without AccountRecovery instance for user {username}'
log.error(err.format(username=user.username))
# Handles clearing the failed login counter upon password reset.
if LoginFailures.is_feature_enabled():
LoginFailures.clear_lockout_counter(user)
send_password_reset_success_email(user, request)
except ValidationError as err:
AUDIT_LOG.exception("Password validation failed")
......
......@@ -41,7 +41,7 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from common.djangoapps.student.tests.factories import TEST_PASSWORD, UserFactory
from common.djangoapps.student.tests.test_configuration_overrides import fake_get_value
from common.djangoapps.student.tests.test_email import mock_render_to_string
from common.djangoapps.student.models import AccountRecovery
from common.djangoapps.student.models import AccountRecovery, LoginFailures
from common.djangoapps.util.password_policy_validators import create_validator_config
from common.djangoapps.util.testing import EventTestMixin
......@@ -611,6 +611,66 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase):
self.assertRaises(Http404, PasswordResetConfirmWrapper.as_view(), reset_request, uidb36=self.uidb36,
token=self.token)
@override_settings(FEATURES={'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS': True}, MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=1)
def test_password_reset_with_login_failures_feature_enabled(self):
"""
Tests that user's login failures lockout counter is reset upon successful password reset.
"""
# Adding an entry in LoginFailures to verify the password reset endpoint
# reset the user's login failures lockout counter.
LoginFailures.increment_lockout_counter(self.user)
request_params = {'new_password1': 'password1', 'new_password2': 'password1'}
confirm_request = self.request_factory.post(self.password_reset_confirm_url, data=request_params)
self.setup_request_session_with_token(confirm_request)
confirm_request.user = self.user
# Make a password reset request.
resp = PasswordResetConfirmWrapper.as_view()(confirm_request, uidb36=self.uidb36, token=self.token)
# Verify the response status code is 302 with password reset success and also verify that
# the user's login failures lockout count is reset.
assert resp.status_code == 302
assert not LoginFailures.is_user_locked_out(confirm_request.user)
# Verify that the user's login failures lockout counter is not reset upon
# password reset failure.
LoginFailures.increment_lockout_counter(self.user)
request_params = {'new_password1': 'password1', 'new_password2': 'password2'}
confirm_request = self.request_factory.post(self.password_reset_confirm_url, data=request_params)
self.setup_request_session_with_token(confirm_request)
confirm_request.user = self.user
# Make a password reset request with mismatching passwords.
resp = PasswordResetConfirmWrapper.as_view()(confirm_request, uidb36=self.uidb36, token=self.token)
assert resp.status_code == 200
assert LoginFailures.is_user_locked_out(self.user)
@override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=1)
def test_password_reset_with_login_failures_feature_disabled(self):
"""
Tests that user's login failures lockout counter is not reset upon successful password reset.
"""
# Adding an entry in LoginFailures to verify the password reset endpoint
# does not reset the user's login failures lockout counter.
LoginFailures.increment_lockout_counter(self.user)
request_params = {'new_password1': 'password1', 'new_password2': 'password1'}
confirm_request = self.request_factory.post(self.password_reset_confirm_url, data=request_params)
self.setup_request_session_with_token(confirm_request)
confirm_request.user = self.user
# Make a password reset request.
resp = PasswordResetConfirmWrapper.as_view()(confirm_request, uidb36=self.uidb36, token=self.token)
# Verify that the user's login failures lockout count is not reset.
assert resp.status_code == 302
assert not LoginFailures.is_feature_enabled()
assert LoginFailures.is_user_locked_out(confirm_request.user)
@ddt.ddt
@skip_unless_lms
......@@ -872,3 +932,53 @@ class ResetPasswordAPITests(EventTestMixin, CacheIsolationTestCase):
assert response.status_code != 429
response = self.client.post(path, request_param)
assert response.status_code == 429
@override_settings(FEATURES={'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS': True}, MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=1)
def test_password_reset_request_with_login_failures_feature_enabled(self):
"""
Tests that user's login failures lockout counter is reset upon successful password reset.
"""
# Adding an entry in LoginFailures to verify the password reset endpoint
# reset the user's login failures lockout counter.
LoginFailures.increment_lockout_counter(self.user)
post_request = self.create_reset_request(self.uidb36, self.token, False)
post_request.user = AnonymousUser()
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'))
# Verify that the user's login failures lockout count is reset.
assert json_response.get('reset_status')
assert not LoginFailures.is_user_locked_out(self.user)
# Verify that the user's login failures lockout counter is not reset upon
# password reset failure.
LoginFailures.increment_lockout_counter(self.user)
post_request = self.create_reset_request(self.uidb36, self.token, False, 'new_password2')
post_request.user = AnonymousUser()
reset_view = LogistrationPasswordResetView.as_view()
reset_view(post_request, uidb36=self.uidb36, token=self.token).render()
assert LoginFailures.is_user_locked_out(self.user)
@override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=1)
def test_password_reset_request_with_login_failures_feature_disabled(self):
"""
Tests that user's login failures lockout counter is not reset upon successful password reset.
"""
# Adding an entry in LoginFailures to verify the password reset endpoint
# does not reset the user's login failures lockout counter.
LoginFailures.increment_lockout_counter(self.user)
post_request = self.create_reset_request(self.uidb36, self.token, False)
post_request.user = AnonymousUser()
reset_view = LogistrationPasswordResetView.as_view()
reset_view(post_request, uidb36=self.uidb36, token=self.token).render()
# Verify that the user's login failures lockout count is not reset.
assert not LoginFailures.is_feature_enabled()
assert LoginFailures.is_user_locked_out(self.user)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment