class TestUsernamePasswordNonmatch(TestCase): """ Test that registration username and password fields differ """ def setUp(self): super(TestUsernamePasswordNonmatch, self).setUp() self.url = reverse('create_account') self.url_params = { 'username': '******', 'email': '*****@*****.**', 'name': 'username', 'terms_of_service': 'true', 'honor_code': 'true', } @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator' ) ]) def test_with_username_password_match(self): self.url_params['username'] = "******" self.url_params['password'] = "******" response = self.client.post(self.url, self.url_params) self.assertEquals(response.status_code, 400) obj = json.loads(response.content.decode('utf-8')) self.assertEqual( obj['value'], "The password is too similar to the username.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator' ) ]) def test_with_username_password_nonmatch(self): self.url_params['username'] = "******" self.url_params['password'] = "******" response = self.client.post(self.url, self.url_params) self.assertEquals(response.status_code, 200) obj = json.loads(response.content.decode('utf-8')) self.assertTrue(obj['success'])
class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): """ Tests that clicking reset password sends email, and doesn't activate the user """ request_factory = RequestFactory() ENABLED_CACHES = ['default'] def setUp(self): # pylint: disable=arguments-differ super(ResetPasswordTests, self).setUp( 'openedx.core.djangoapps.user_authn.views.password_reset.tracker') self.user = UserFactory.create() self.user.is_active = False self.user.save() self.token = default_token_generator.make_token(self.user) self.uidb36 = int_to_base36(self.user.id) self.user_bad_passwd = UserFactory.create() self.user_bad_passwd.is_active = False self.user_bad_passwd.password = UNUSABLE_PASSWORD_PREFIX self.user_bad_passwd.save() def setup_request_session_with_token(self, request): """ Internal helper to setup request session and add token in session. """ process_request(request) request.session[INTERNAL_RESET_SESSION_TOKEN] = self.token @patch( 'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) def test_user_bad_password_reset(self): """ Tests password reset behavior for user with password marked UNUSABLE_PASSWORD_PREFIX """ bad_pwd_req = self.request_factory.post( '/password_reset/', {'email': self.user_bad_passwd.email}) bad_pwd_req.user = AnonymousUser() bad_pwd_resp = password_reset(bad_pwd_req) # If they've got an unusable password, we return a successful response code self.assertEqual(bad_pwd_resp.status_code, 200) obj = json.loads(bad_pwd_resp.content.decode('utf-8')) self.assertEqual( obj, { 'success': True, 'value': "('registration/password_reset_done.html', [])", }) self.assert_no_events_were_emitted() @patch( 'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) def test_nonexist_email_password_reset(self): """ Now test the exception cases with of reset_password called with invalid email. """ bad_email_req = self.request_factory.post( '/password_reset/', {'email': self.user.email + "makeItFail"}) bad_email_req.user = AnonymousUser() bad_email_resp = password_reset(bad_email_req) # Note: even if the email is bad, we return a successful response code # This prevents someone potentially trying to "brute-force" find out which # emails are and aren't registered with edX self.assertEqual(bad_email_resp.status_code, 200) obj = json.loads(bad_email_resp.content.decode('utf-8')) self.assertEqual( obj, { 'success': True, 'value': "('registration/password_reset_done.html', [])", }) self.assert_no_events_were_emitted() @patch( 'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) def test_password_reset_ratelimited_for_non_existing_user(self): """ Test that reset password endpoint only allow one request per minute for non-existing user. """ self.assert_password_reset_ratelimitted('*****@*****.**', AnonymousUser()) self.assert_no_events_were_emitted() @patch( 'openedx.core.djangoapps.user_authn.views.password_reset.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) def test_password_reset_ratelimited_for_existing_user(self): """ Test that reset password endpoint only allow one request per minute for existing user. """ self.assert_password_reset_ratelimitted(self.user.email, self.user) self.assert_event_emission_count(SETTING_CHANGE_INITIATED, 1) def assert_password_reset_ratelimitted(self, email, user): """ Assert that password reset endpoint allow one request per minute per email. """ cache.clear() password_reset_req = self.request_factory.post('/password_reset/', {'email': email}) password_reset_req.user = user password_reset_req.site = Mock(domain='example.com') good_resp = password_reset(password_reset_req) self.assertEqual(good_resp.status_code, 200) # then the rate limiter should kick in and give a HttpForbidden response bad_resp = password_reset(password_reset_req) self.assertEqual(bad_resp.status_code, 403) cache.clear() def test_ratelimitted_from_same_ip_with_different_email(self): """ Test that password reset endpoint allow only one request per minute per IP. """ cache.clear() good_req = self.request_factory.post( '/password_reset/', {'email': '*****@*****.**'}) good_req.user = AnonymousUser() good_resp = password_reset(good_req) self.assertEqual(good_resp.status_code, 200) # change the email ID and verify that the rate limiter should kick in and # give a Forbidden response if the request is from same IP. bad_req = self.request_factory.post( '/password_reset/', {'email': '*****@*****.**'}) bad_req.user = AnonymousUser() bad_resp = password_reset(bad_req) self.assertEqual(bad_resp.status_code, 403) cache.clear() def test_ratelimited_from_different_ips_with_same_email(self): """ Test that password reset endpoint allow only two requests per hour per email address. """ cache.clear() self.request_password_reset(200) # now reset the time to 1 min from now in future and change the email and # verify that it will allow another request from same IP reset_time = datetime.now(UTC) + timedelta(seconds=61) with freeze_time(reset_time): for status in [200, 403]: self.request_password_reset(status) # Even changing the IP will not allow more than two requests for same email. new_ip = "8.8.8.8" self.request_password_reset(403, new_ip=new_ip) cache.clear() def request_password_reset(self, status, new_ip=None): extra_args = {} if new_ip: extra_args = {'REMOTE_ADDR': new_ip} reset_request = self.request_factory.post( '/password_reset/', {'email': '*****@*****.**'}, **extra_args) if new_ip: self.assertEqual(reset_request.META.get('REMOTE_ADDR'), new_ip) reset_request.user = AnonymousUser() response = password_reset(reset_request) self.assertEqual(response.status_code, status) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @ddt.data(( 'plain_text', "You're receiving this e-mail because you requested a password reset" ), ('html', "You're receiving this e-mail because you requested a password reset" )) @ddt.unpack def test_reset_password_email(self, body_type, expected_output): """Tests contents of reset password email, and that user is not active""" good_req = self.request_factory.post('/password_reset/', {'email': self.user.email}) good_req.user = self.user good_req.site = Mock(domain='example.com') dot_application = dot_factories.ApplicationFactory(user=self.user) dot_access_token = dot_factories.AccessTokenFactory( user=self.user, application=dot_application) dot_factories.RefreshTokenFactory(user=self.user, application=dot_application, access_token=dot_access_token) good_resp = password_reset(good_req) self.assertEqual(good_resp.status_code, 200) self.assertFalse( dot_models.AccessToken.objects.filter(user=self.user).exists()) self.assertFalse( dot_models.RefreshToken.objects.filter(user=self.user).exists()) obj = json.loads(good_resp.content.decode('utf-8')) self.assertTrue(obj['success']) self.assertIn('e-mailed you instructions for setting your password', obj['value']) from_email = configuration_helpers.get_value( 'email_from_address', settings.DEFAULT_FROM_EMAIL) sent_message = mail.outbox[0] bodies = { 'plain_text': sent_message.body, 'html': sent_message.alternatives[0][0], } body = bodies[body_type] self.assertIn("Password reset", sent_message.subject) self.assertIn(expected_output, body) self.assertEqual(sent_message.from_email, from_email) self.assertEqual(len(sent_message.to), 1) self.assertIn(self.user.email, sent_message.to) self.assert_event_emitted( SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None, ) # Test that the user is not active self.user = User.objects.get(pk=self.user.pk) self.assertFalse(self.user.is_active) self.assertIn('password_reset_confirm/', body) re.search( r'password_reset_confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/', body).groupdict() @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @ddt.data((False, 'http://'), (True, 'https://')) @ddt.unpack def test_reset_password_email_https(self, is_secure, protocol): """ Tests that the right url protocol is included in the reset password link """ req = self.request_factory.post('/password_reset/', {'email': self.user.email}) req.site = Mock(domain='example.com') req.is_secure = Mock(return_value=is_secure) req.user = self.user password_reset(req) sent_message = mail.outbox[0] msg = sent_message.body expected_msg = "Please go to the following page and choose a new password:\n\n" + protocol self.assertIn(expected_msg, msg) self.assert_event_emitted(SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @ddt.data(('Crazy Awesome Site', 'Crazy Awesome Site'), ('edX', 'edX')) @ddt.unpack def test_reset_password_email_site(self, site_name, platform_name): """ Tests that the right url domain and platform name is included in the reset password email """ with patch("django.conf.settings.PLATFORM_NAME", platform_name): with patch("django.conf.settings.SITE_NAME", site_name): req = self.request_factory.post('/password_reset/', {'email': self.user.email}) req.user = self.user req.site = Mock(domain='example.com') password_reset(req) sent_message = mail.outbox[0] msg = sent_message.body reset_msg = u"you requested a password reset for your user account at {}" reset_msg = reset_msg.format(site_name) self.assertIn(reset_msg, msg) sign_off = u"The {} Team".format(platform_name) self.assertIn(sign_off, msg) self.assert_event_emitted(SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @patch("openedx.core.djangoapps.site_configuration.helpers.get_value", fake_get_value) @ddt.data('plain_text', 'html') def test_reset_password_email_configuration_override(self, body_type): """ Tests that the right url domain and platform name is included in the reset password email """ req = self.request_factory.post('/password_reset/', {'email': self.user.email}) req.get_host = Mock(return_value=None) req.site = Mock(domain='example.com') req.user = self.user with patch('crum.get_current_request', return_value=req): password_reset(req) sent_message = mail.outbox[0] bodies = { 'plain_text': sent_message.body, 'html': sent_message.alternatives[0][0], } body = bodies[body_type] reset_msg = u"you requested a password reset for your user account at {}".format( fake_get_value('PLATFORM_NAME')) self.assertIn(reset_msg, body) self.assert_event_emitted(SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None) self.assertEqual(sent_message.from_email, "*****@*****.**") @ddt.data( ('invalidUid', 'invalid_token'), (None, 'invalid_token'), ('invalidUid', None), ) @ddt.unpack def test_reset_password_bad_token(self, uidb36, token): """ Tests bad token and uidb36 in password reset """ if uidb36 is None: uidb36 = self.uidb36 if token is None: token = self.token bad_request = self.request_factory.get( reverse("password_reset_confirm", kwargs={ "uidb36": uidb36, "token": token })) process_request(bad_request) bad_request.user = AnonymousUser() PasswordResetConfirmWrapper.as_view()(bad_request, uidb36=uidb36, token=token) self.user = User.objects.get(pk=self.user.pk) self.assertFalse(self.user.is_active) def test_reset_password_good_token(self): """ Tests good token and uidb36 in password reset. Scenario: When the password reset url is opened Then the page is redirected to url without token And token gets set in session When the redirected page is visited with token in session Then reset password page renders And inactive user is set to active """ url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) good_reset_req = self.request_factory.get(url) process_request(good_reset_req) good_reset_req.user = self.user redirect_response = PasswordResetConfirmWrapper.as_view()( good_reset_req, uidb36=self.uidb36, token=self.token) good_reset_req = self.request_factory.get(redirect_response.url) self.setup_request_session_with_token(good_reset_req) good_reset_req.user = self.user # set-password is the new token representation in the redirect url PasswordResetConfirmWrapper.as_view()(good_reset_req, uidb36=self.uidb36, token='set-password') self.user = User.objects.get(pk=self.user.pk) self.assertTrue(self.user.is_active) def test_reset_password_good_token_with_anonymous_user(self): """ Tests good token and uidb36 in password reset for anonymous user. Scenario: When the password reset url is opened with anonymous user in request Then the page is redirected to url without token And token gets set in session When the redirected page is visited with token in session Then reset password page renders And inactive user associated with token is set to active """ url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) good_reset_req = self.request_factory.get(url) process_request(good_reset_req) good_reset_req.user = AnonymousUser() redirect_response = PasswordResetConfirmWrapper.as_view()( good_reset_req, uidb36=self.uidb36, token=self.token) good_reset_req = self.request_factory.get(redirect_response.url) self.setup_request_session_with_token(good_reset_req) good_reset_req.user = AnonymousUser() # set-password is the new token representation in the redirect url PasswordResetConfirmWrapper.as_view()(good_reset_req, uidb36=self.uidb36, token='set-password') self.user = User.objects.get(pk=self.user.pk) self.assertTrue(self.user.is_active) def test_password_reset_fail(self): """ Tests that if we provide mismatched passwords, user is not marked as active. """ self.assertFalse(self.user.is_active) url = reverse('password_reset_confirm', kwargs={ 'uidb36': self.uidb36, 'token': self.token }) request_params = { 'new_password1': 'password1', 'new_password2': 'password2' } confirm_request = self.request_factory.post(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) # Verify the response status code is: 200 with password reset fail and also verify that # the user is not marked as active. self.assertEqual(resp.status_code, 200) self.assertFalse(User.objects.get(pk=self.user.pk).is_active) def test_password_reset_retired_user_fail(self): """ Tests that if a retired user attempts to reset their password, it fails. """ self.assertFalse(self.user.is_active) # Retire the user. UserRetirementRequest.create_retirement_request(self.user) url = reverse('password_reset_confirm', kwargs={ 'uidb36': self.uidb36, 'token': self.token }) reset_req = self.request_factory.get(url) reset_req.user = self.user resp = PasswordResetConfirmWrapper.as_view()(reset_req, uidb36=self.uidb36, token=self.token) # Verify the response status code is: 200 with password reset fail and also verify that # the user is not marked as active. self.assertEqual(resp.status_code, 200) self.assertFalse(User.objects.get(pk=self.user.pk).is_active) def test_password_reset_normalize_password(self): # pylint: disable=anomalous-unicode-escape-in-string """ Tests that if we provide a not properly normalized password, it is saved using our normalization method of NFKC. In this test, the input password is u'p\u212bssword'. It should be normalized to u'p\xc5ssword' """ url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) password = u'p\u212bssword' request_params = {'new_password1': password, 'new_password2': password} confirm_request = self.request_factory.post(url, data=request_params) process_request(confirm_request) confirm_request.session[INTERNAL_RESET_SESSION_TOKEN] = self.token confirm_request.user = self.user __ = PasswordResetConfirmWrapper.as_view()(confirm_request, uidb36=self.uidb36, token=self.token) user = User.objects.get(pk=self.user.pk) salt_val = user.password.split('$')[1] expected_user_password = make_password( unicodedata.normalize('NFKC', u'p\u212bssword'), salt_val) self.assertEqual(expected_user_password, user.password) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 2}), create_validator_config( 'util.password_policy_validators.MaximumLengthValidator', {'max_length': 10}) ]) @ddt.data( { 'password': '******', 'error_message': 'This password is too short. It must contain at least 2 characters.', }, { 'password': '******', 'error_message': 'This password is too long. It must contain no more than 10 characters.', }) def test_password_reset_with_invalid_length(self, password_dict): """ Tests that if we provide password characters less then PASSWORD_MIN_LENGTH, or more than PASSWORD_MAX_LENGTH, password reset will fail with error message. """ url = reverse('password_reset_confirm', kwargs={ 'uidb36': self.uidb36, 'token': self.token }) request_params = { 'new_password1': password_dict['password'], 'new_password2': password_dict['password'] } confirm_request = self.request_factory.post(url, data=request_params) self.setup_request_session_with_token(confirm_request) confirm_request.user = self.user # Make a password reset request with minimum/maximum passwords characters. response = PasswordResetConfirmWrapper.as_view()(confirm_request, uidb36=self.uidb36, token=self.token) self.assertEqual(response.context_data['err_msg'], password_dict['error_message']) @patch.object(PasswordResetConfirmView, 'dispatch') @patch("openedx.core.djangoapps.site_configuration.helpers.get_value", fake_get_value) def test_reset_password_good_token_configuration_override( self, reset_confirm): """ Tests password reset confirmation page for site configuration override. """ url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) good_reset_req = self.request_factory.get(url) process_request(good_reset_req) good_reset_req.user = self.user PasswordResetConfirmWrapper.as_view()(good_reset_req, uidb36=self.uidb36, token=self.token) confirm_kwargs = reset_confirm.call_args[1] self.assertEqual(confirm_kwargs['extra_context']['platform_name'], 'Fake University') self.user = User.objects.get(pk=self.user.pk) self.assertTrue(self.user.is_active) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @ddt.data('Crazy Awesome Site', 'edX') def test_reset_password_email_subject(self, platform_name): """ Tests that the right platform name is included in the reset password email subject """ with patch("django.conf.settings.PLATFORM_NAME", platform_name): req = self.request_factory.post('/password_reset/', {'email': self.user.email}) req.user = self.user req.site = Mock(domain='example.com') password_reset(req) sent_message = mail.outbox[0] subj = sent_message.subject self.assertIn(platform_name, subj) def test_reset_password_with_other_user_link(self): """ Tests that user should not be able to reset password through other user's token """ reset_url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) reset_request = self.request_factory.get(reset_url) reset_request.user = UserFactory.create() self.assertRaises(Http404, PasswordResetConfirmWrapper.as_view(), reset_request, uidb36=self.uidb36, token=self.token)
class TestPasswordReset(LoginEnrollmentTestCase): """ Go through some of the password reset use cases """ shard = 1 def _setup_user(self, is_staff=False, password=None): """ Override the base implementation to randomize the email """ email = 'foo_{0}@test.com'.format(uuid4().hex[:8]) password = password if password else 'foo' username = '******'.format(uuid4().hex[:8]) self.create_account(username, email, password) self.activate_user(email) # manually twiddle the is_staff bit, if needed if is_staff: user = User.objects.get(email=email) user.is_staff = True user.save() return email, password def assertPasswordResetError(self, response, error_message, valid_link=True): """ This method is a custom assertion that verifies that a password reset view returns an error response as expected. Args: response: response from calling a password reset endpoint error_message: message we expect to see in the response valid_link: if the current password reset link is still valid """ self.assertEqual(response.status_code, 200) self.assertEqual(response.context_data['validlink'], valid_link) self.assertIn(error_message, response.content) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 6}) ]) def test_password_policy_on_password_reset(self): """ This makes sure the proper asserts on password policy also works on password reset """ staff_email, _ = self._setup_user(is_staff=True, password='******') success_msg = 'Your Password Reset is Complete' # try to reset password, it should fail user = User.objects.get(email=staff_email) token = default_token_generator.make_token(user) uidb36 = int_to_base36(user.id) # try to do a password reset with the same password as before resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format( uidb36, token), { 'new_password1': 'foo', 'new_password2': 'foo', }, follow=True) self.assertNotIn(success_msg, resp.content) # try to reset password with a long enough password user = User.objects.get(email=staff_email) token = default_token_generator.make_token(user) uidb36 = int_to_base36(user.id) # try to do a password reset with the same password as before resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format( uidb36, token), { 'new_password1': 'foofoo', 'new_password2': 'foofoo', }, follow=True) self.assertIn(success_msg, resp.content) @ddt.data( ('foo', 'foobar', 'Error in resetting your password. Please try again.'), ('', '', 'This password is too short. It must contain at least'), ) @ddt.unpack def test_password_reset_form_invalid(self, password1, password2, err_msg): """ Tests that password reset fail when providing bad passwords and error message is displayed to the user. """ user_email, _ = self._setup_user() # try to reset password, it should fail user = User.objects.get(email=user_email) token = default_token_generator.make_token(user) uidb36 = int_to_base36(user.id) # try to do a password reset with the same password as before resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format( uidb36, token), { 'new_password1': password1, 'new_password2': password2, }, follow=True) self.assertPasswordResetError(resp, err_msg)
class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): """ Tests that clicking reset password sends email, and doesn't activate the user """ request_factory = RequestFactory() ENABLED_CACHES = ['default'] def setUp(self): # pylint: disable=arguments-differ super(ResetPasswordTests, self).setUp('student.views.management.tracker') self.user = UserFactory.create() self.user.is_active = False self.user.save() self.token = default_token_generator.make_token(self.user) self.uidb36 = int_to_base36(self.user.id) self.user_bad_passwd = UserFactory.create() self.user_bad_passwd.is_active = False self.user_bad_passwd.password = UNUSABLE_PASSWORD_PREFIX self.user_bad_passwd.save() @patch('student.views.management.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) def test_user_bad_password_reset(self): """ Tests password reset behavior for user with password marked UNUSABLE_PASSWORD_PREFIX """ bad_pwd_req = self.request_factory.post( '/password_reset/', {'email': self.user_bad_passwd.email}) bad_pwd_resp = password_reset(bad_pwd_req) # If they've got an unusable password, we return a successful response code self.assertEquals(bad_pwd_resp.status_code, 200) obj = json.loads(bad_pwd_resp.content.decode('utf-8')) self.assertEquals( obj, { 'success': True, 'value': "('registration/password_reset_done.html', [])", }) self.assert_no_events_were_emitted() @patch('student.views.management.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) def test_nonexist_email_password_reset(self): """ Now test the exception cases with of reset_password called with invalid email. """ bad_email_req = self.request_factory.post( '/password_reset/', {'email': self.user.email + "makeItFail"}) bad_email_resp = password_reset(bad_email_req) # Note: even if the email is bad, we return a successful response code # This prevents someone potentially trying to "brute-force" find out which # emails are and aren't registered with edX self.assertEquals(bad_email_resp.status_code, 200) obj = json.loads(bad_email_resp.content.decode('utf-8')) self.assertEquals( obj, { 'success': True, 'value': "('registration/password_reset_done.html', [])", }) self.assert_no_events_were_emitted() @patch('student.views.management.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) def test_password_reset_ratelimited(self): """ Try (and fail) resetting password 30 times in a row on an non-existant email address """ cache.clear() for i in range(30): good_req = self.request_factory.post( '/password_reset/', {'email': 'thisdoesnotexist{0}@foo.com'.format(i)}) good_resp = password_reset(good_req) self.assertEquals(good_resp.status_code, 200) # then the rate limiter should kick in and give a HttpForbidden response bad_req = self.request_factory.post( '/password_reset/', {'email': '*****@*****.**'}) bad_resp = password_reset(bad_req) self.assertEquals(bad_resp.status_code, 403) self.assert_no_events_were_emitted() cache.clear() @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @ddt.data(( 'plain_text', "You're receiving this e-mail because you requested a password reset" ), ('html', "You're receiving this e-mail because you requested a password reset" )) @ddt.unpack def test_reset_password_email(self, body_type, expected_output): """Tests contents of reset password email, and that user is not active""" good_req = self.request_factory.post('/password_reset/', {'email': self.user.email}) good_req.user = self.user good_req.site = Mock(domain='example.com') dop_client = ClientFactory() dop_access_token = AccessTokenFactory(user=self.user, client=dop_client) RefreshTokenFactory(user=self.user, client=dop_client, access_token=dop_access_token) dot_application = dot_factories.ApplicationFactory(user=self.user) dot_access_token = dot_factories.AccessTokenFactory( user=self.user, application=dot_application) dot_factories.RefreshTokenFactory(user=self.user, application=dot_application, access_token=dot_access_token) good_resp = password_reset(good_req) self.assertEquals(good_resp.status_code, 200) self.assertFalse( dop_models.AccessToken.objects.filter(user=self.user).exists()) self.assertFalse( dop_models.RefreshToken.objects.filter(user=self.user).exists()) self.assertFalse( dot_models.AccessToken.objects.filter(user=self.user).exists()) self.assertFalse( dot_models.RefreshToken.objects.filter(user=self.user).exists()) obj = json.loads(good_resp.content.decode('utf-8')) self.assertTrue(obj['success']) self.assertIn('e-mailed you instructions for setting your password', obj['value']) from_email = configuration_helpers.get_value( 'email_from_address', settings.DEFAULT_FROM_EMAIL) sent_message = mail.outbox[0] bodies = { 'plain_text': sent_message.body, 'html': sent_message.alternatives[0][0], } body = bodies[body_type] self.assertIn("Password reset", sent_message.subject) self.assertIn(expected_output, body) self.assertEquals(sent_message.from_email, from_email) self.assertEquals(len(sent_message.to), 1) self.assertIn(self.user.email, sent_message.to) self.assert_event_emitted( SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None, ) # Test that the user is not active self.user = User.objects.get(pk=self.user.pk) self.assertFalse(self.user.is_active) self.assertIn('password_reset_confirm/', body) re.search( r'password_reset_confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/', body).groupdict() @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @patch('django.core.mail.send_mail') @ddt.data((False, 'http://'), (True, 'https://')) @ddt.unpack def test_reset_password_email_https(self, is_secure, protocol, send_email): """ Tests that the right url protocol is included in the reset password link """ req = self.request_factory.post('/password_reset/', {'email': self.user.email}) req.site = Mock(domain='example.com') req.is_secure = Mock(return_value=is_secure) req.user = self.user password_reset(req) _, msg, _, _ = send_email.call_args[0] expected_msg = "Please go to the following page and choose a new password:\n\n" + protocol self.assertIn(expected_msg, msg) self.assert_event_emitted(SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @patch('django.core.mail.send_mail') @ddt.data(('Crazy Awesome Site', 'Crazy Awesome Site'), ('edX', 'edX')) @ddt.unpack def test_reset_password_email_site(self, site_name, platform_name, send_email): """ Tests that the right url domain and platform name is included in the reset password email """ with patch("django.conf.settings.PLATFORM_NAME", platform_name): with patch("django.conf.settings.SITE_NAME", site_name): req = self.request_factory.post('/password_reset/', {'email': self.user.email}) req.user = self.user req.site = Mock(domain='example.com') password_reset(req) _, msg, _, _ = send_email.call_args[0] reset_msg = u"you requested a password reset for your user account at {}" reset_msg = reset_msg.format(site_name) self.assertIn(reset_msg, msg) sign_off = u"The {} Team".format(platform_name) self.assertIn(sign_off, msg) self.assert_event_emitted(SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @patch("openedx.core.djangoapps.site_configuration.helpers.get_value", fake_get_value) @ddt.data('plain_text', 'html') def test_reset_password_email_configuration_override(self, body_type): """ Tests that the right url domain and platform name is included in the reset password email """ req = self.request_factory.post('/password_reset/', {'email': self.user.email}) req.get_host = Mock(return_value=None) req.site = Mock(domain='example.com') req.user = self.user with patch('crum.get_current_request', return_value=req): password_reset(req) sent_message = mail.outbox[0] bodies = { 'plain_text': sent_message.body, 'html': sent_message.alternatives[0][0], } body = bodies[body_type] reset_msg = u"you requested a password reset for your user account at {}".format( fake_get_value('PLATFORM_NAME')) self.assertIn(reset_msg, body) self.assert_event_emitted(SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None) self.assertEqual(sent_message.from_email, "*****@*****.**") @ddt.data( ('invalidUid', 'invalid_token'), (None, 'invalid_token'), ('invalidUid', None), ) @ddt.unpack def test_reset_password_bad_token(self, uidb36, token): """ Tests bad token and uidb36 in password reset """ if uidb36 is None: uidb36 = self.uidb36 if token is None: token = self.token bad_request = self.request_factory.get( reverse("password_reset_confirm", kwargs={ "uidb36": uidb36, "token": token })) bad_request.user = AnonymousUser() password_reset_confirm_wrapper(bad_request, uidb36, token) self.user = User.objects.get(pk=self.user.pk) self.assertFalse(self.user.is_active) def test_reset_password_good_token(self): """ Tests good token and uidb36 in password reset """ url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) good_reset_req = self.request_factory.get(url) good_reset_req.user = self.user password_reset_confirm_wrapper(good_reset_req, self.uidb36, self.token) self.user = User.objects.get(pk=self.user.pk) self.assertTrue(self.user.is_active) def test_reset_password_good_token_with_anonymous_user(self): """ Tests good token and uidb36 in password reset for anonymous user """ url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) good_reset_req = self.request_factory.get(url) good_reset_req.user = AnonymousUser() password_reset_confirm_wrapper(good_reset_req, self.uidb36, self.token) self.user = User.objects.get(pk=self.user.pk) self.assertTrue(self.user.is_active) def test_password_reset_fail(self): """ Tests that if we provide mismatched passwords, user is not marked as active. """ self.assertFalse(self.user.is_active) url = reverse('password_reset_confirm', kwargs={ 'uidb36': self.uidb36, 'token': self.token }) request_params = { 'new_password1': 'password1', 'new_password2': 'password2' } confirm_request = self.request_factory.post(url, data=request_params) confirm_request.user = self.user # Make a password reset request with mismatching passwords. resp = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token) # Verify the response status code is: 200 with password reset fail and also verify that # the user is not marked as active. self.assertEqual(resp.status_code, 200) self.assertFalse(User.objects.get(pk=self.user.pk).is_active) def test_password_reset_retired_user_fail(self): """ Tests that if a retired user attempts to reset their password, it fails. """ self.assertFalse(self.user.is_active) # Retire the user. UserRetirementRequest.create_retirement_request(self.user) url = reverse('password_reset_confirm', kwargs={ 'uidb36': self.uidb36, 'token': self.token }) reset_req = self.request_factory.get(url) reset_req.user = self.user resp = password_reset_confirm_wrapper(reset_req, self.uidb36, self.token) # Verify the response status code is: 200 with password reset fail and also verify that # the user is not marked as active. self.assertEqual(resp.status_code, 200) self.assertFalse(User.objects.get(pk=self.user.pk).is_active) def test_password_reset_prevent_auth_user_writes(self): with waffle().override(PREVENT_AUTH_USER_WRITES, True): url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) for request in [ self.request_factory.get(url), self.request_factory.post(url) ]: request.user = self.user response = password_reset_confirm_wrapper( request, self.uidb36, self.token) assert response.context_data[ 'err_msg'] == SYSTEM_MAINTENANCE_MSG self.user.refresh_from_db() assert not self.user.is_active def test_password_reset_normalize_password(self): # pylint: disable=anomalous-unicode-escape-in-string """ Tests that if we provide a not properly normalized password, it is saved using our normalization method of NFKC. In this test, the input password is u'p\u212bssword'. It should be normalized to u'p\xc5ssword' """ url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) password = u'p\u212bssword' request_params = {'new_password1': password, 'new_password2': password} confirm_request = self.request_factory.post(url, data=request_params) confirm_request.user = self.user __ = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token) user = User.objects.get(pk=self.user.pk) salt_val = user.password.split('$')[1] expected_user_password = make_password( unicodedata.normalize('NFKC', u'p\u212bssword'), salt_val) self.assertEqual(expected_user_password, user.password) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 2}), create_validator_config( 'util.password_policy_validators.MaximumLengthValidator', {'max_length': 10}) ]) @ddt.data( { 'password': '******', 'error_message': 'This password is too short. It must contain at least 2 characters.', }, { 'password': '******', 'error_message': 'This password is too long. It must contain no more than 10 characters.', }) def test_password_reset_with_invalid_length(self, password_dict): """ Tests that if we provide password characters less then PASSWORD_MIN_LENGTH, or more than PASSWORD_MAX_LENGTH, password reset will fail with error message. """ url = reverse('password_reset_confirm', kwargs={ 'uidb36': self.uidb36, 'token': self.token }) request_params = { 'new_password1': password_dict['password'], 'new_password2': password_dict['password'] } confirm_request = self.request_factory.post(url, data=request_params) confirm_request.user = self.user # Make a password reset request with minimum/maximum passwords characters. response = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token) self.assertEqual(response.context_data['err_msg'], password_dict['error_message']) @patch('student.views.management.password_reset_confirm') @patch("openedx.core.djangoapps.site_configuration.helpers.get_value", fake_get_value) def test_reset_password_good_token_configuration_override( self, reset_confirm): """ Tests password reset confirmation page for site configuration override. """ url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) good_reset_req = self.request_factory.get(url) good_reset_req.user = self.user password_reset_confirm_wrapper(good_reset_req, self.uidb36, self.token) confirm_kwargs = reset_confirm.call_args[1] self.assertEquals(confirm_kwargs['extra_context']['platform_name'], 'Fake University') self.user = User.objects.get(pk=self.user.pk) self.assertTrue(self.user.is_active) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @patch('django.core.mail.send_mail') @ddt.data('Crazy Awesome Site', 'edX') def test_reset_password_email_subject(self, platform_name, send_email): """ Tests that the right platform name is included in the reset password email subject """ with patch("django.conf.settings.PLATFORM_NAME", platform_name): req = self.request_factory.post('/password_reset/', {'email': self.user.email}) req.user = self.user req.site = Mock(domain='example.com') password_reset(req) subj, _, _, _ = send_email.call_args[0] self.assertIn(platform_name, subj) def test_reset_password_with_other_user_link(self): """ Tests that user should not be able to reset password through other user's token """ reset_url = reverse("password_reset_confirm", kwargs={ "uidb36": self.uidb36, "token": self.token }) reset_request = self.request_factory.get(reset_url) reset_request.user = UserFactory.create() self.assertRaises(Http404, password_reset_confirm_wrapper, reset_request, self.uidb36, self.token)
class TestPasswordPolicy(TestCase): """ Go through some password policy tests to make sure things are properly working """ def setUp(self): super(TestPasswordPolicy, self).setUp() self.url = reverse('create_account') self.request_factory = RequestFactory() self.url_params = { 'username': '******', 'email': '*****@*****.**', 'name': 'username', 'terms_of_service': 'true', 'honor_code': 'true', } @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 6}) ]) def test_password_length_too_short(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content.decode('utf-8')) self.assertEqual( obj['value'], "This password is too short. It must contain at least 6 characters.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 6}) ]) def test_password_length_long_enough(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content.decode('utf-8')) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MaximumLengthValidator', {'max_length': 12}) ]) def test_password_length_too_long(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content.decode('utf-8')) self.assertEqual( obj['value'], "This password is too long. It must contain no more than 12 characters.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 3}) ]) def test_password_not_enough_uppercase(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content.decode('utf-8')) self.assertEqual( obj['value'], "This password must contain at least 3 uppercase letters.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 3}) ]) def test_password_enough_uppercase(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content.decode('utf-8')) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 3}) ]) def test_password_not_enough_lowercase(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content.decode('utf-8')) self.assertEqual( obj['value'], "This password must contain at least 3 lowercase letters.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 3}) ]) def test_password_enough_lowercase(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content.decode('utf-8')) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 3}) ]) def test_not_enough_punctuations(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content.decode('utf-8')) self.assertEqual( obj['value'], "This password must contain at least 3 punctuation marks.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 3}) ]) def test_enough_punctuations(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content.decode('utf-8')) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 3}) ]) def test_not_enough_numeric_characters(self): # The unicode ២ is the number 2 in Khmer and the ٧ is the Arabic-Indic number 7 self.url_params['password'] = u'thisShouldFail២٧' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content.decode('utf-8')) self.assertEqual( obj['value'], "This password must contain at least 3 numbers.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 3}) ]) def test_enough_numeric_characters(self): # The unicode ២ is the number 2 in Khmer self.url_params['password'] = u'thisShouldPass២33' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content.decode('utf-8')) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.AlphabeticValidator', {'min_alphabetic': 3}) ]) def test_not_enough_alphabetic_characters(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content.decode('utf-8')) self.assertEqual( obj['value'], "This password must contain at least 3 letters.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.AlphabeticValidator', {'min_alphabetic': 3}) ]) def test_enough_alphabetic_characters(self): self.url_params['password'] = u'𝒯𝓗Ï𝓼𝒫å𝓼𝓼𝔼𝓼' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content.decode('utf-8')) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 3}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 3}), create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 3}), create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 3}), ]) def test_multiple_errors_fail(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content.decode('utf-8')) errstring = ( "This password must contain at least 3 uppercase letters. " "This password must contain at least 3 numbers. " "This password must contain at least 3 punctuation marks.") self.assertEqual(obj['value'], errstring) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 3}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 3}), create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 3}), create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 3}), create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 3}), ]) def test_multiple_errors_pass(self): self.url_params['password'] = u'tH1s Sh0u!d P3#$!' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content.decode('utf-8')) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'django.contrib.auth.password_validation.CommonPasswordValidator') ]) def test_common_password_fail(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content.decode('utf-8')) self.assertEqual( obj['value'], "This password is too common.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'django.contrib.auth.password_validation.CommonPasswordValidator') ]) def test_common_password_pass(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content.decode('utf-8')) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 6}), create_validator_config( 'util.password_policy_validators.MaximumLengthValidator', {'max_length': 75}), ]) def test_with_unicode(self): self.url_params['password'] = u'四節比分和七年前' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content.decode('utf-8')) self.assertTrue(obj['success'])
class PasswordPolicyValidatorsTestCase(CacheIsolationTestCase): """ Tests for password validator utility functions The general framework I went with for testing the validators was to test: 1) requiring a single check (also checks proper singular message) 2) requiring multiple instances of the check (also checks proper plural message) 3) successful check """ def validation_errors_checker(self, password, msg, user=None): """ This helper function is used to check the proper error messages are being displayed based on the password and validator. Parameters: password (unicode): the password to validate on user (django.contrib.auth.models.User): user object to use in validation. This is an optional parameter unless the validator requires a user object. msg (str): The expected ValidationError message """ if msg is None: validate_password(password, user) else: with self.assertRaises(ValidationError) as cm: validate_password(password, user) self.assertIn(msg, ' '.join(cm.exception.messages)) def test_unicode_password(self): """ Tests that validate_password enforces unicode """ byte_str = b'𤭮' unicode_str = u'𤭮' # Sanity checks and demonstration of why this test is useful self.assertEqual(len(byte_str), 4) self.assertEqual(len(unicode_str), 1) # Test length check self.validation_errors_checker( byte_str, 'This password is too short. It must contain at least 2 characters.' ) self.validation_errors_checker(byte_str + byte_str, None) # Test badly encoded password self.validation_errors_checker(b'\xff\xff', 'Invalid password.') def test_password_unicode_normalization(self): """ Tests that validate_password normalizes passwords """ # s ̣ ̇ (s with combining dot below and combining dot above) not_normalized_password = u'\u0073\u0323\u0307' self.assertEqual(len(not_normalized_password), 3) # When the flag is not set, the validation should succeed since len > 2 self.validation_errors_checker(not_normalized_password, None) # When we normalize we expect the not_normalized password to fail # because it should be normalized to u'\u1E69' -> ṩ with PASSWORD_UNICODE_NORMALIZE_FLAG.override(active=True): self.validation_errors_checker( not_normalized_password, 'This password is too short. It must contain at least 2 characters.' ) @data( ([ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 2}) ], 'at least 2 characters.'), ([ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 2}), create_validator_config( 'util.password_policy_validators.AlphabeticValidator', {'min_alphabetic': 2}), ], 'characters, including 2 letters.'), ([ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 2}), create_validator_config( 'util.password_policy_validators.AlphabeticValidator', {'min_alphabetic': 2}), create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 1}), ], 'characters, including 2 letters & 1 number.'), ([ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 2}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 3}), create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 1}), create_validator_config( 'util.password_policy_validators.SymbolValidator', {'min_symbol': 2}), ], 'including 3 uppercase letters & 1 number & 2 symbols.'), ) @unpack def test_password_instructions(self, config, msg): """ Tests password instructions """ with override_settings(AUTH_PASSWORD_VALIDATORS=config): self.assertIn(msg, password_validators_instruction_texts()) @data( (u'userna', u'username', '*****@*****.**', 'The password is too similar to the username.'), (u'password', u'username', '*****@*****.**', 'The password is too similar to the email address.'), (u'password', u'username', '*****@*****.**', 'The password is too similar to the email address.'), (u'password', u'username', '*****@*****.**', None), ) @unpack @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator' ) ]) def test_user_attribute_similarity_validation_errors( self, password, username, email, msg): """ Tests validate_password error messages for the UserAttributeSimilarityValidator """ user = User(username=username, email=email) self.validation_errors_checker(password, msg, user) @data( ([ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 1}) ], u'', 'This password is too short. It must contain at least 1 character.'), ([ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 8}) ], u'd', 'This password is too short. It must contain at least 8 characters.'), ([ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 8}) ], u'longpassword', None), ) @unpack def test_minimum_length_validation_errors(self, config, password, msg): """ Tests validate_password error messages for the MinimumLengthValidator """ with override_settings(AUTH_PASSWORD_VALIDATORS=config): self.validation_errors_checker(password, msg) @data( ([ create_validator_config( 'util.password_policy_validators.MaximumLengthValidator', {'max_length': 1}) ], u'longpassword', 'This password is too long. It must contain no more than 1 character.' ), ([ create_validator_config( 'util.password_policy_validators.MaximumLengthValidator', {'max_length': 10}) ], u'longpassword', 'This password is too long. It must contain no more than 10 characters.' ), ([ create_validator_config( 'util.password_policy_validators.MaximumLengthValidator', {'max_length': 20}) ], u'shortpassword', None), ) @unpack def test_maximum_length_validation_errors(self, config, password, msg): """ Tests validate_password error messages for the MaximumLengthValidator """ with override_settings(AUTH_PASSWORD_VALIDATORS=config): self.validation_errors_checker(password, msg) @data( (u'password', 'This password is too common.'), (u'good_password', None), ) @unpack @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'django.contrib.auth.password_validation.CommonPasswordValidator') ]) def test_common_password_validation_errors(self, password, msg): """ Tests validate_password error messages for the CommonPasswordValidator """ self.validation_errors_checker(password, msg) @data( ([ create_validator_config( 'util.password_policy_validators.AlphabeticValidator', {'min_alphabetic': 1}) ], u'12345', 'This password must contain at least 1 letter.'), ([ create_validator_config( 'util.password_policy_validators.AlphabeticValidator', {'min_alphabetic': 5}) ], u'test123', 'This password must contain at least 5 letters.'), ([ create_validator_config( 'util.password_policy_validators.AlphabeticValidator', {'min_alphabetic': 2}) ], u'password', None), ) @unpack def test_alphabetic_validation_errors(self, config, password, msg): """ Tests validate_password error messages for the AlphabeticValidator """ with override_settings(AUTH_PASSWORD_VALIDATORS=config): self.validation_errors_checker(password, msg) @data( ([ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 1}) ], u'test', 'This password must contain at least 1 number.'), ([ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 4}) ], u'test123', 'This password must contain at least 4 numbers.'), ([ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 2}) ], u'password123', None), ) @unpack def test_numeric_validation_errors(self, config, password, msg): """ Tests validate_password error messages for the NumericValidator """ with override_settings(AUTH_PASSWORD_VALIDATORS=config): self.validation_errors_checker(password, msg) @data( ([ create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 1}) ], u'lowercase', 'This password must contain at least 1 uppercase letter.'), ([ create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 6}) ], u'NOTenough', 'This password must contain at least 6 uppercase letters.'), ([ create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 1}) ], u'camelCase', None), ) @unpack def test_upper_case_validation_errors(self, config, password, msg): """ Tests validate_password error messages for the UppercaseValidator """ with override_settings(AUTH_PASSWORD_VALIDATORS=config): self.validation_errors_checker(password, msg) @data( ([ create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 1}) ], u'UPPERCASE', 'This password must contain at least 1 lowercase letter.'), ([ create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 4}) ], u'notENOUGH', 'This password must contain at least 4 lowercase letters.'), ([ create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 1}) ], u'goodPassword', None), ) @unpack def test_lower_case_validation_errors(self, config, password, msg): """ Tests validate_password error messages for the LowercaseValidator """ with override_settings(AUTH_PASSWORD_VALIDATORS=config): self.validation_errors_checker(password, msg) @data( ([ create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 1}) ], u'no punctuation', 'This password must contain at least 1 punctuation mark.'), ([ create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 7}) ], u'p@$$w0rd$!', 'This password must contain at least 7 punctuation marks.'), ([ create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 3}) ], u'excl@m@t!on', None), ) @unpack def test_punctuation_validation_errors(self, config, password, msg): """ Tests validate_password error messages for the PunctuationValidator """ with override_settings(AUTH_PASSWORD_VALIDATORS=config): self.validation_errors_checker(password, msg) @data( ([ create_validator_config( 'util.password_policy_validators.SymbolValidator', {'min_symbol': 1}) ], u'no symbol', 'This password must contain at least 1 symbol.'), ([ create_validator_config( 'util.password_policy_validators.SymbolValidator', {'min_symbol': 3}) ], u'☹️boo☹️', 'This password must contain at least 3 symbols.'), ([ create_validator_config( 'util.password_policy_validators.SymbolValidator', {'min_symbol': 2}) ], u'☪symbols!☹️', None), ) @unpack def test_symbol_validation_errors(self, config, password, msg): """ Tests validate_password error messages for the SymbolValidator """ with override_settings(AUTH_PASSWORD_VALIDATORS=config): self.validation_errors_checker(password, msg)
class SessionApiSecurityTest(TestCase): """ Test edx_solutions_api_integration.session.session_list view """ def setUp(self): # pylint: disable=E7601 """ Create one user and save it to the database """ self.user = UserFactory.build(username='******', email='*****@*****.**') self.user.set_password('test_password') self.user.save() profile = UserProfile(user=self.user) profile.city = 'Boston' profile.save() # Create the test client self.client = Client() cache.clear() self.session_url = '/api/server/sessions' self.user_url = '/api/server/users' @override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10) def test_login_ratelimited_success(self): """ Try (and fail) logging in with fewer attempts than the limit of 10 and verify that you can still successfully log in afterwards. """ for i in range(9): password = '******'.format(i) response, mock_audit_log = self._do_request(self.session_url, 'test', password, secure=True) self.assertEqual(response.status_code, 401) # now try logging in with a valid password and check status response, mock_audit_log = self._do_request(self.session_url, 'test', 'test_password', secure=True) self._assert_response(response, status=201) @override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10) def test_login_blockout(self): """ Try (and fail) logging in with 10 attempts and verify that user is blocked out. """ for i in range(10): password = '******'.format(i) response, mock_audit_log = self._do_request(self.session_url, 'test', password, secure=True) self.assertEqual(response.status_code, 401) # check to see if this response indicates blockout response, mock_audit_log = self._do_request(self.session_url, 'test', 'test_password', secure=True) message = _( 'This account has been temporarily locked due to excessive login failures. Try again later.' ) self._assert_response(response, status=403, message=message) @override_settings(MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED=10, MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS=1800) def test_blockout_reset_time_period(self): """ Try logging in 10 times to block user and then login with right credentials(after 30 minutes) to verify blocked out time expired and user can login successfully. """ for i in range(10): password = '******'.format(i) response, mock_audit_log = self._do_request(self.session_url, 'test', password, secure=True) self.assertEqual(response.status_code, 401) self._assert_audit_log(mock_audit_log, 'warn', [ "API::User authentication failed with user-id - {}".format( self.user.id) ]) self._assert_not_in_audit_log(mock_audit_log, 'warn', ['test']) # check to see if this response indicates blockout response, mock_audit_log = self._do_request(self.session_url, 'test', 'test_password', secure=True) message = _( 'This account has been temporarily locked due to excessive login failures. Try again later.' ) self._assert_response(response, status=403, message=message) # now reset the time to 30 from now in future reset_time = datetime.now(UTC) + timedelta(seconds=1800) with freeze_time(reset_time): response, mock_audit_log = self._do_request(self.session_url, 'test', 'test_password', secure=True) self._assert_response(response, status=201) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 4}) ]) def test_with_short_password(self): """ Try (and fail) user creation with shorter password """ response, mock_audit_log = self._do_request(self.user_url, 'test', 'abc', email='*****@*****.**', first_name='John', last_name='Doe', secure=True) message = _( 'Password: This password is too short. It must contain at least 4 characters.' ) self._assert_response(response, status=400, message=message) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MaximumLengthValidator', {'max_length': 12}) ]) def test_with_long_password(self): """ Try (and fail) user creation with longer password """ response, mock_audit_log = self._do_request(self.user_url, 'test', 'test_password', email='*****@*****.**', first_name='John', last_name='Doe', secure=True) message = _( 'Password: This password is too long. It must contain no more than 12 characters.' ) self._assert_response(response, status=400, message=message) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 2}), create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 2}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 2}), create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 2}) ]) def test_password_without_uppercase(self): """ Try (and fail) user creation since password should have atleast 2 upper characters """ response, mock_audit_log = self._do_request(self.user_url, 'test', 'test.pa64!', email='*****@*****.**', first_name='John', last_name='Doe', secure=True) message = _( 'Password: This password must contain at least 2 uppercase letters.' ) self._assert_response(response, status=400, message=message) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 2}), create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 2}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 2}), create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 2}) ]) def test_password_without_lowercase(self): """ Try (and fail) user creation since password should have atleast 2 lower characters """ response, mock_audit_log = self._do_request(self.user_url, 'test', 'TEST.PA64!', email='*****@*****.**', first_name='John', last_name='Doe', secure=True) message = _( 'Password: This password must contain at least 2 lowercase letters.' ) self._assert_response(response, status=400, message=message) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 2}), create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 2}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 2}), create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 2}) ]) def test_password_without_punctuation(self): """ Try (and fail) user creation without any punctuation in password """ response, mock_audit_log = self._do_request( self.user_url, 'test', 'test64Ss', email='*****@*****.**', # pylint: disable=W0612,C0301 first_name='John', last_name='Doe', secure=True) message = _( 'Password: This password must contain at least 2 uppercase letters.; ' 'This password must contain at least 2 punctuation marks.') self._assert_response(response, status=400, message=message) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 2}), create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 2}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 2}), create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 2}) ]) def test_password_without_numeric(self): """ Try (and fail) user creation without any numeric characters in password """ response, mock_audit_log = self._do_request( self.user_url, 'test', 'test.paSs!', email='*****@*****.**', # pylint: disable=W0612,C0301 first_name='John', last_name='Doe', secure=True) message = _( 'Password: This password must contain at least 2 numbers.; ' 'This password must contain at least 2 uppercase letters.') self._assert_response(response, status=400, message=message) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 2}), create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 2}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 2}), create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 2}) ]) def test_password_with_complexity(self): """ This should pass since it has everything needed for a complex password """ response, mock_audit_log = self._do_request( self.user_url, str(uuid.uuid4()), 'Test.Me64!', email='*****@*****.**', first_name='John', last_name='Doe', secure=True, patched_audit_log= 'edx_solutions_api_integration.users.views.AUDIT_LOG') # pylint: disable=C0301 self._assert_response(response, status=201) self._assert_audit_log(mock_audit_log, 'info', ['API::New account created with user-id']) self._assert_not_in_audit_log(mock_audit_log, 'info', ['*****@*****.**']) def test_user_with_invalid_email(self): """ Try (and fail) user creation with invalid email address """ response, mock_audit_log = self._do_request(self.user_url, 'test', 'Test.Me64!', email='test-edx.org', first_name='John', last_name='Doe', secure=True) message = _('Valid e-mail is required.') self._assert_response(response, status=400, message=message) def test_user_with_invalid_username(self): """ Try (and fail) user creation with invalid username """ response, mock_audit_log = self._do_request(self.user_url, 'user name', 'Test.Me64!', email='*****@*****.**', first_name='John', last_name='Doe', secure=True) message = _( 'Username should only consist of A-Z and 0-9, with no spaces.') self._assert_response(response, status=400, message=message) def test_user_with_unknown_username(self): """ Try (and fail) user login with unknown credentials """ response, mock_audit_log = self._do_request(self.session_url, 'unknown', 'UnKnown.Pass', secure=True) self._assert_response(response, status=404) self._assert_audit_log( mock_audit_log, 'warn', ['API::Failed login attempt with unknown email/username']) def test_successful_logout(self): """ Try login of user first and then logout user successfully and test audit log """ response, mock_audit_log = self._do_request(self.session_url, 'test', 'test_password', secure=True) self._assert_response(response, status=201) self._assert_audit_log(mock_audit_log, 'info', [ "API::User logged in successfully with user-id - {}".format( self.user.id) ]) self._assert_not_in_audit_log(mock_audit_log, 'info', ['test']) response_dict = json.loads(response.content.decode("utf-8")) response, mock_audit_log = self._do_request(self.session_url + '/' + response_dict['token'], 'test', 'test_password', secure=True, request_method='DELETE') self._assert_response(response, status=204) self._assert_audit_log(mock_audit_log, 'info', [ 'API::User session terminated for user-id - {}'.format( self.user.id) ]) def _do_request(self, url, username, password, **kwargs): """ Make Post/Delete/Get requests with params """ post_params, extra, = {'username': username, 'password': password}, {} patched_audit_log = 'edx_solutions_api_integration.sessions.views.AUDIT_LOG' request_method = kwargs.get('request_method', 'POST') if kwargs.get('email'): post_params['email'] = kwargs.get('email') if kwargs.get('first_name'): post_params['first_name'] = kwargs.get('first_name') if kwargs.get('last_name'): post_params['last_name'] = kwargs.get('last_name') if kwargs.get('secure', False): extra['wsgi.url_scheme'] = 'https' if kwargs.get('patched_audit_log'): patched_audit_log = kwargs.get('patched_audit_log') headers = { 'X-Edx-Api-Key': TEST_API_KEY, 'Content-Type': 'application/json' } with patch(patched_audit_log) as mock_audit_log: if request_method == 'POST': result = self.client.post(url, post_params, headers=headers, **extra) elif request_method == 'DELETE': result = self.client.delete(url, post_params, headers=headers, **extra) return result, mock_audit_log def _assert_response(self, response, status=200, message=None): """ Assert that the response had status 200 and returned a valid JSON-parseable dict. If success is provided, assert that the response had that value for 'success' in the JSON dict. If message is provided, assert that the response contained that value for 'message' in the JSON dict. """ self.assertEqual(response.status_code, status) # Return if response has not content if response.status_code == 204: return response_dict = json.loads(response.content.decode("utf-8")) if message is not None: msg = ("'%s' did not contain '%s'" % (response_dict['message'], message)) self.assertTrue(message in response_dict['message'], msg) def _assert_audit_log(self, mock_audit_log, level, log_strings): """ Check that the audit log has received the expected call as its last call. """ method_calls = mock_audit_log.method_calls name, args, _kwargs = method_calls[-1] self.assertEqual(name, level) self.assertEqual(len(args), 1) format_string = args[0] for log_string in log_strings: self.assertIn(log_string, format_string) def _assert_not_in_audit_log(self, mock_audit_log, level, log_strings): """ Check that the audit log has received the expected call as its last call. """ method_calls = mock_audit_log.method_calls name, args, _kwargs = method_calls[-1] self.assertEqual(name, level) self.assertEqual(len(args), 1) format_string = args[0] for log_string in log_strings: self.assertNotIn(log_string, format_string)
class TestPasswordPolicy(TestCase): """ Go through some password policy tests to make sure things are properly working """ def setUp(self): super(TestPasswordPolicy, self).setUp() self.url = reverse('create_account') self.request_factory = RequestFactory() self.url_params = { 'username': '******', 'email': '*****@*****.**', 'name': 'username', 'terms_of_service': 'true', 'honor_code': 'true', } @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 6}) ]) def test_password_length_too_short(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content) self.assertEqual( obj['value'], "This password is too short. It must contain at least 6 characters.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 6}) ]) def test_password_length_long_enough(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MaximumLengthValidator', {'max_length': 12}) ]) def test_password_length_too_long(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content) self.assertEqual( obj['value'], "This password is too long. It must contain no more than 12 characters.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 3}) ]) def test_password_not_enough_uppercase(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content) self.assertEqual( obj['value'], "This password must contain at least 3 uppercase letters.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 3}) ]) def test_password_enough_uppercase(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 3}) ]) def test_password_not_enough_lowercase(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content) self.assertEqual( obj['value'], "This password must contain at least 3 lowercase letters.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 3}) ]) def test_password_enough_lowercase(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 3}) ]) def test_not_enough_punctuations(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content) self.assertEqual( obj['value'], "This password must contain at least 3 punctuation marks.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 3}) ]) def test_enough_punctuations(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 3}) ]) def test_not_enough_numeric_characters(self): # The unicode ២ is the number 2 in Khmer and the ٧ is the Arabic-Indic number 7 self.url_params['password'] = u'thisShouldFail២٧' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content) self.assertEqual( obj['value'], "This password must contain at least 3 numbers.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 3}) ]) def test_enough_numeric_characters(self): # The unicode ២ is the number 2 in Khmer self.url_params['password'] = u'thisShouldPass២33' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.AlphabeticValidator', {'min_alphabetic': 3}) ]) def test_not_enough_alphabetic_characters(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content) self.assertEqual( obj['value'], "This password must contain at least 3 letters.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.AlphabeticValidator', {'min_alphabetic': 3}) ]) def test_enough_alphabetic_characters(self): self.url_params['password'] = u'𝒯𝓗Ï𝓼𝒫å𝓼𝓼𝔼𝓼' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 3}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 3}), create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 3}), create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 3}), ]) def test_multiple_errors_fail(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content) errstring = ( "This password must contain at least 3 uppercase letters. " "This password must contain at least 3 numbers. " "This password must contain at least 3 punctuation marks.") self.assertEqual(obj['value'], errstring) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 3}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 3}), create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 3}), create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 3}), create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 3}), ]) def test_multiple_errors_pass(self): self.url_params['password'] = u'tH1s Sh0u!d P3#$!' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'django.contrib.auth.password_validation.CommonPasswordValidator') ]) def test_common_password_fail(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 400) obj = json.loads(response.content) self.assertEqual( obj['value'], "This password is too common.", ) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'django.contrib.auth.password_validation.CommonPasswordValidator') ]) def test_common_password_pass(self): self.url_params['password'] = '******' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 6}), create_validator_config( 'util.password_policy_validators.MaximumLengthValidator', {'max_length': 75}), ]) def test_with_unicode(self): self.url_params['password'] = u'四節比分和七年前' response = self.client.post(self.url, self.url_params) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success']) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 6}) ], SESSION_ENGINE='django.contrib.sessions.backends.cache') def test_ext_auth_password_length_too_short(self): """ Tests that even if password policy is enforced, ext_auth registrations aren't subject to it """ self.url_params['password'] = u'aaa' # shouldn't pass validation request = self.request_factory.post(self.url, self.url_params) request.site = SiteFactory.create() # now indicate we are doing ext_auth by setting 'ExternalAuthMap' in the session. request.session = import_module( settings.SESSION_ENGINE).SessionStore() # empty session extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='*****@*****.**', internal_password=self.url_params['password'], external_domain='shib:https://idp.stanford.edu/') request.session['ExternalAuthMap'] = extauth request.user = AnonymousUser() with patch('edxmako.request_context.get_current_request', return_value=request): response = create_account(request) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success'])
class TestPasswordHistory(LoginEnrollmentTestCase): """ Go through some of the PasswordHistory use cases """ shard = 1 def _login(self, email, password, should_succeed=True, err_msg_check=None): """ Override the base implementation so we can do appropriate asserts """ resp = self.client.post(reverse('login'), { 'email': email, 'password': password }) data = json.loads(resp.content) self.assertEqual(resp.status_code, 200) if should_succeed: self.assertTrue(data['success']) else: self.assertFalse(data['success']) if err_msg_check: self.assertIn(err_msg_check, data['value']) def _setup_user(self, is_staff=False, password=None): """ Override the base implementation to randomize the email """ email = 'foo_{0}@test.com'.format(uuid4().hex[:8]) password = password if password else 'foo' username = '******'.format(uuid4().hex[:8]) self.create_account(username, email, password) self.activate_user(email) # manually twiddle the is_staff bit, if needed if is_staff: user = User.objects.get(email=email) user.is_staff = True user.save() return email, password def _update_password(self, email, new_password): """ Helper method to reset a password """ user = User.objects.get(email=email) user.set_password(new_password) user.save() history = PasswordHistory() history.create(user) def assertPasswordResetError(self, response, error_message, valid_link=True): """ This method is a custom assertion that verifies that a password reset view returns an error response as expected. Args: response: response from calling a password reset endpoint error_message: message we expect to see in the response valid_link: if the current password reset link is still valid """ self.assertEqual(response.status_code, 200) self.assertEqual(response.context_data['validlink'], valid_link) self.assertIn(error_message, response.content) @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DAYS_FOR_STAFF_ACCOUNTS_PASSWORD_RESETS': None}) @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DAYS_FOR_STUDENT_ACCOUNTS_PASSWORD_RESETS': None}) def test_no_forced_password_change(self): """ Makes sure default behavior is correct when we don't have this turned on """ email, password = self._setup_user() self._login(email, password) email, password = self._setup_user(is_staff=True) self._login(email, password) @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DAYS_FOR_STAFF_ACCOUNTS_PASSWORD_RESETS': 1}) @patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DAYS_FOR_STUDENT_ACCOUNTS_PASSWORD_RESETS': 5}) def test_forced_password_change(self): """ Make sure password are viewed as expired in LMS after the policy time has elapsed """ student_email, student_password = self._setup_user() staff_email, staff_password = self._setup_user(is_staff=True) self._login(student_email, student_password) self._login(staff_email, staff_password) staff_reset_time = timezone.now() + timedelta(days=1) with freeze_time(staff_reset_time): self._login(student_email, student_password) # staff should fail because password expired self._login( staff_email, staff_password, should_succeed=False, err_msg_check= "Your password has expired due to password policy on this account" ) # if we reset the password, we should be able to log in self._update_password(staff_email, "updated") self._login(staff_email, "updated") student_reset_time = timezone.now() + timedelta(days=5) with freeze_time(student_reset_time): # Both staff and student logins should fail because user must # reset the password self._login( student_email, student_password, should_succeed=False, err_msg_check= "Your password has expired due to password policy on this account" ) self._update_password(student_email, "updated") self._login(student_email, "updated") self._login( staff_email, staff_password, should_succeed=False, err_msg_check= "Your password has expired due to password policy on this account" ) self._update_password(staff_email, "updated2") self._login(staff_email, "updated2") def test_allow_all_password_reuse(self): """ Tests that password_reset flows work as expected if reuse config is missing, meaning passwords can always be reused """ student_email, _ = self._setup_user() user = User.objects.get(email=student_email) err_msg = 'You are re-using a password that you have used recently.' token = default_token_generator.make_token(user) uidb36 = int_to_base36(user.id) # try to do a password reset with the same password as before resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format( uidb36, token), { 'new_password1': 'foo', 'new_password2': 'foo' }, follow=True) self.assertNotIn(err_msg, resp.content) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 6}) ]) def test_password_policy_on_password_reset(self): """ This makes sure the proper asserts on password policy also works on password reset """ staff_email, _ = self._setup_user(is_staff=True, password='******') success_msg = 'Your Password Reset is Complete' # try to reset password, it should fail user = User.objects.get(email=staff_email) token = default_token_generator.make_token(user) uidb36 = int_to_base36(user.id) # try to do a password reset with the same password as before resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format( uidb36, token), { 'new_password1': 'foo', 'new_password2': 'foo', }, follow=True) self.assertNotIn(success_msg, resp.content) # try to reset password with a long enough password user = User.objects.get(email=staff_email) token = default_token_generator.make_token(user) uidb36 = int_to_base36(user.id) # try to do a password reset with the same password as before resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format( uidb36, token), { 'new_password1': 'foofoo', 'new_password2': 'foofoo', }, follow=True) self.assertIn(success_msg, resp.content) @ddt.data( ('foo', 'foobar', 'Error in resetting your password. Please try again.'), ('', '', 'This password is too short. It must contain at least'), ) @ddt.unpack def test_password_reset_form_invalid(self, password1, password2, err_msg): """ Tests that password reset fail when providing bad passwords and error message is displayed to the user. """ user_email, _ = self._setup_user() # try to reset password, it should fail user = User.objects.get(email=user_email) token = default_token_generator.make_token(user) uidb36 = int_to_base36(user.id) # try to do a password reset with the same password as before resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format( uidb36, token), { 'new_password1': password1, 'new_password2': password2, }, follow=True) self.assertPasswordResetError(resp, err_msg)
class UserPasswordResetTest(CacheIsolationTestCase): """ Test edx_solutions_api_integration.session.session_list view """ ENABLED_CACHES = ['default'] def setUp(self): # pylint: disable=E7601 """ setup the api urls """ self.session_url = '/api/server/sessions' self.user_url = '/api/server/users' cache.clear() @override_settings(ADVANCED_SECURITY_CONFIG={ 'MIN_DAYS_FOR_STUDENT_ACCOUNTS_PASSWORD_RESETS': 5 }) def test_user_must_reset_password_after_n_days(self): """ Test to ensure that User session login fails after N days. User must reset his/her password after N days to login again """ response = self._do_post_request(self.user_url, 'test2', 'Test.Me64!', email='*****@*****.**', first_name='John', last_name='Doe', secure=True) self._assert_response(response, status=201) user_id = response.data['id'] # pylint: disable=E1101 response = self._do_post_request(self.session_url, 'test2', 'Test.Me64!', secure=True) self.assertEqual(response.status_code, 201) reset_time = timezone.now() + timedelta(days=5) with patch.object(timezone, 'now', return_value=reset_time): response = self._do_post_request(self.session_url, 'test2', 'Test.Me64!', secure=True) message = _( 'Your password has expired due to password policy on this account. ' 'You must reset your password before you can log in again.') self._assert_response(response, status=403, message=message) #reset the password and then try login pass_reset_url = "%s/%s" % (self.user_url, str(user_id)) response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) self.assertEqual(response.status_code, 200) #login successful after reset password response = self._do_post_request(self.session_url, 'test2', 'Test.Me64@', secure=True) self.assertEqual(response.status_code, 201) @override_settings( ADVANCED_SECURITY_CONFIG={ 'MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE': 4, 'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 0 }) def test_password_reset_not_allowable_reuse(self): """ Try resetting user password < 4 and > 4 times and then use one of the passwords that you have used before """ response = self._do_post_request(self.user_url, 'test2', 'Test.Me64!', email='*****@*****.**', first_name='John', last_name='Doe', secure=True) self._assert_response(response, status=201) user_id = response.data['id'] # pylint: disable=E1101 pass_reset_url = "%s/%s" % (self.user_url, str(user_id)) response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) self._assert_response(response, status=200) response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) self._assert_response(response, status=200) response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) self._assert_response(response, status=200) #now use previously used password response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) message = _( "You are re-using a password that you have used recently. You must " "have 4 distinct password(s) before reusing a previous password.") self._assert_response(response, status=403, message=message) response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) self._assert_response(response, status=200) #now use previously used password response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) self._assert_response(response, status=200) @override_settings( ADVANCED_SECURITY_CONFIG={ 'MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE': 20, 'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 0 }) def test_password_reset_not_allowable_reuse_staff_user(self): """ Try resetting staff user password with an already-used password Hits a very specific LOC in the view code """ response = self._do_post_request(self.user_url, 'test2', 'Test.Me64!', email='*****@*****.**', first_name='John', last_name='Doe', secure=True, is_staff=True) self._assert_response(response, status=201) user_id = response.data['id'] # pylint: disable=E1101 pass_reset_url = "%s/%s" % (self.user_url, str(user_id)) response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) self._assert_response(response, status=200) response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) message = _( "You are re-using a password that you have used recently. You must " "have 20 distinct password(s) before reusing a previous password.") self._assert_response(response, status=403, message=message) @override_settings(ADVANCED_SECURITY_CONFIG={ 'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 1 }) def test_is_password_reset_too_frequent(self): """ Try reset user password before and after the MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS """ response = self._do_post_request(self.user_url, 'test2', 'Test.Me64!', email='*****@*****.**', first_name='John', last_name='Doe', secure=True) self._assert_response(response, status=201) user_id = response.data['id'] # pylint: disable=E1101 pass_reset_url = "%s/%s" % (self.user_url, str(user_id)) response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) message = _( "You are resetting passwords too frequently. Due to security policies, " "1 day(s) must elapse between password resets") self._assert_response(response, status=403, message=message) reset_time = timezone.now() + timedelta(days=1) with patch.object(timezone, 'now', return_value=reset_time): response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) self._assert_response(response, status=200) @override_settings(ADVANCED_SECURITY_CONFIG={ 'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 0 }) def test_password_reset_rate_limiting_unblock(self): """ Try (and fail) login user 40 times on invalid password and then unblock it after 5 minutes """ response = self._do_post_request(self.user_url, 'test2', 'Test.Me64!', email='*****@*****.**', first_name='John', last_name='Doe', secure=True) self._assert_response(response, status=201) user_id = response.data['id'] # pylint: disable=E1101 pass_reset_url = '{}/{}'.format(self.user_url, user_id) for i in xrange(30): password = u'test_password{0}'.format(i) response = self._do_post_pass_reset_request('{}/{}'.format( self.user_url, i + 200), password=password, secure=True) self._assert_response(response, status=404) response = self._do_post_pass_reset_request('{}/{}'.format( self.user_url, 31), password='******', secure=True) message = _('Rate limit exceeded in password_reset.') self._assert_response(response, status=403, message=message) # now reset the time to 5 mins from now in future in order to unblock reset_time = datetime.now(UTC) + timedelta(seconds=300) with freeze_time(reset_time): response = self._do_post_pass_reset_request(pass_reset_url, password='******', secure=True) self._assert_response(response, status=200) @override_settings(AUTH_PASSWORD_VALIDATORS=[ create_validator_config( 'util.password_policy_validators.NumericValidator', {'min_numeric': 2}), create_validator_config( 'util.password_policy_validators.LowercaseValidator', {'min_lower': 2}), create_validator_config( 'util.password_policy_validators.UppercaseValidator', {'min_upper': 2}), create_validator_config( 'util.password_policy_validators.PunctuationValidator', {'min_punctuation': 2}), create_validator_config( 'util.password_policy_validators.MinimumLengthValidator', {'min_length': 11}), create_validator_config( 'util.password_policy_validators.SymbolValidator', {'min_symbol': 1}), create_validator_config( 'util.password_policy_validators.AlphabeticValidator', {'min_alphabetic': 5}) ]) def test_minimum_password_complexity_scenarios(self): """ Test Password complexity using complex passwords scenarios """ # test meet the minimum password criteria password = '******' response = self._do_post_request(self.user_url, 'test', password, email='*****@*****.**', first_name='John', last_name='Doe', secure=True) self._assert_response(response, status=201) # test meet the minimum password criteria password = '******' response = self._do_post_request(self.user_url, 'test_user', password, email='*****@*****.**', first_name='John', last_name='Doe', secure=True) self._assert_response(response, status=201) # test meet the minimum password criteria password = '******' response = self._do_post_request(self.user_url, 'test1', password, email='*****@*****.**', first_name='John1', last_name='Doe1', secure=True) self._assert_response(response, status=201) # test meet the minimum password criteria password = "******" response = self._do_post_request(self.user_url, 'test2', password, email='*****@*****.**', first_name='John2', last_name='Doe2', secure=True) self._assert_response(response, status=201) # test will not meet the minimum password criteria password = '******' response = self._do_post_request(self.user_url, 'test3', password, email='*****@*****.**', first_name='John3', last_name='Doe3', secure=True) message = _( 'Password: This password is too short. It must contain at least 11 characters.' ) self._assert_response(response, status=400, message=message) # test will not meet the minimum password criteria password = '******' response = self._do_post_request(self.user_url, 'test4', password, email='*****@*****.**', first_name='John4', last_name='Doe4', secure=True) message = _('Password: This password must contain at least 2 numbers.') self._assert_response(response, status=400, message=message) # test will not meet the minimum password criteria password = '******' response = self._do_post_request(self.user_url, 'test5', password, email='*****@*****.**', first_name='John5', last_name='Doe5', secure=True) message = _( 'Password: This password must contain at least 2 lowercase letters.; ' 'This password must contain at least 2 uppercase letters.; ' 'This password must contain at least 2 punctuation marks.') self._assert_response(response, status=400, message=message) # test will not meet the minimum password criteria password = '******' response = self._do_post_request(self.user_url, 'test6', password, email='*****@*****.**', first_name='John6', last_name='Doe6', secure=True) message = _('Password: This password must contain at least 5 letters.') self._assert_response(response, status=400, message=message) # test will not meet the minimum password criteria password = '******' response = self._do_post_request(self.user_url, 'test7', password, email='*****@*****.**', first_name='John7', last_name='Doe7', secure=True) message = _('Password: This password must contain at least 1 symbol.') self._assert_response(response, status=400, message=message) def _do_post_request(self, url, username, password, **kwargs): """ Post the login info """ post_params, extra = {'username': username, 'password': password}, {} if kwargs.get('email'): post_params['email'] = kwargs.get('email') if kwargs.get('first_name'): post_params['first_name'] = kwargs.get('first_name') if kwargs.get('last_name'): post_params['last_name'] = kwargs.get('last_name') if kwargs.get('is_staff'): post_params['is_staff'] = kwargs.get('is_staff') headers = { 'X-Edx-Api-Key': TEST_API_KEY, 'Content-Type': 'application/json' } if kwargs.get('secure', False): extra['wsgi.url_scheme'] = 'https' return self.client.post(url, post_params, headers=headers, **extra) def _do_post_pass_reset_request(self, url, password, **kwargs): """ Post the Password Reset info """ post_params, extra = {'password': password}, {} headers = { 'X-Edx-Api-Key': TEST_API_KEY, 'Content-Type': 'application/json' } if kwargs.get('secure', False): extra['wsgi.url_scheme'] = 'https' return self.client.post(url, post_params, headers=headers, **extra) def _assert_response(self, response, status=200, message=None): """ Assert that the response had status 200 and returned a valid JSON-parseable dict. If message is provided, assert that the response contained that value for 'message' in the JSON dict. """ self.assertEqual(response.status_code, status) response_dict = json.loads(response.content) if message is not None: msg = ("'%s' did not contain '%s'" % (response_dict['message'], message)) self.assertTrue(message in response_dict['message'], msg)