def login_attempts(request): """ Track number of login attempts made by a specific IP within a specified amount of time """ ip, username = check_lockout(request) attempts_key = safe_key("{}{}-{}".format(LOGIN_ATTEMPTS, ip, username)) attempts = cache.get(attempts_key) if attempts: cache.incr(attempts_key) attempts = cache.get(attempts_key) if attempts >= getattr(settings, "MAX_LOGIN_ATTEMPTS", 10): lockout_key = safe_key("{}{}-{}".format(LOCKOUT_IP, ip, username)) lockout = cache.get(lockout_key) if not lockout: send_lockout_email(username, ip) cache.set( lockout_key, datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), getattr(settings, "LOCKOUT_TIME", 1800), ) check_lockout(request) return attempts return attempts cache.set(attempts_key, 1) return cache.get(attempts_key)
def test_new_project_set_to_cache(self): """ Test that newly created project is set to cache """ data = { 'name': u'demo', 'owner': 'http://testserver/api/v1/users/%s' % self.user, 'metadata': {'description': 'Some description', 'location': 'Naivasha, Kenya', 'category': 'governance'}, 'public': False } # clear cache cache.delete(safe_key(f'{PROJ_OWNER_CACHE}1')) self.assertIsNone(cache.get(safe_key(f'{PROJ_OWNER_CACHE}1'))) # Create the project self._project_create(data) self.assertIsNotNone(self.project_data) request = self.factory.get('/', **self.extra) request.user = self.user serializer = ProjectSerializer( self.project, context={'request': request}).data self.assertEqual( cache.get(f'{PROJ_OWNER_CACHE}{self.project.pk}'), serializer) # clear cache cache.delete(safe_key(f'{PROJ_OWNER_CACHE}{self.project.pk}'))
def check_lockout(request) -> Tuple[Optional[str], Optional[str]]: """Check request user is not locked out on authentication. Returns the username if not locked out, None if request path is in LOCKOUT_EXCLUDED_PATHS. Raises AuthenticationFailed on lockout. """ uri_path = request.get_full_path() if not any(part in LOCKOUT_EXCLUDED_PATHS for part in uri_path.split("/")): ip, username = retrieve_user_identification(request) if ip and username: lockout = cache.get( safe_key("{}{}-{}".format(LOCKOUT_IP, ip, username))) if lockout: time_locked_out = datetime.now() - datetime.strptime( lockout, "%Y-%m-%dT%H:%M:%S") remaining_time = round( (getattr(settings, "LOCKOUT_TIME", 1800) - time_locked_out.seconds) / 60) raise AuthenticationFailed( _("Locked out. Too many wrong username" "/password attempts. " "Try again in {} minutes.".format(remaining_time))) return ip, username return None, None
def check_lockout(request): """Check request user is not locked out on authentication. Returns the username if not locked out, None if request path is in LOCKOUT_EXCLUDED_PATHS. Raises AuthenticationFailed on lockout. """ uri_path = request.get_full_path() if any(part in LOCKOUT_EXCLUDED_PATHS for part in uri_path.split("/")): return None try: if isinstance(request.META["HTTP_AUTHORIZATION"], bytes): username = (request.META["HTTP_AUTHORIZATION"].decode( "utf-8").split('"')[1]) else: username = request.META["HTTP_AUTHORIZATION"].split('"')[1] except (TypeError, AttributeError, IndexError): pass else: lockout = cache.get(safe_key("{}{}".format(LOCKOUT_USER, username))) if lockout: time_locked_out = datetime.now() - datetime.strptime( lockout, "%Y-%m-%dT%H:%M:%S") remaining_time = round((getattr(settings, "LOCKOUT_TIME", 1800) - time_locked_out.seconds) / 60) raise AuthenticationFailed( _("Locked out. Too many wrong username/password attempts. " "Try again in {} minutes.".format(remaining_time))) return username return None
def check_lockout(request): try: if isinstance(request.META["HTTP_AUTHORIZATION"], bytes): username = (request.META["HTTP_AUTHORIZATION"].decode( "utf-8").split('"')[1]) else: username = request.META["HTTP_AUTHORIZATION"].split('"')[1] except (TypeError, AttributeError, IndexError): return else: lockout = cache.get(safe_key("{}{}".format(LOCKOUT_USER, username))) if lockout: time_locked_out = datetime.now() - datetime.strptime( lockout, "%Y-%m-%dT%H:%M:%S") remaining_time = round((getattr(settings, "LOCKOUT_TIME", 1800) - time_locked_out.seconds) / 60) raise AuthenticationFailed( _("Locked out. Too many wrong username/password attempts. " "Try again in {} minutes.".format(remaining_time))) return username
def test_login_attempts(self, send_account_lockout_email): view = ConnectViewSet.as_view( {'get': 'list'}, authentication_classes=(DigestAuthentication,)) auth = DigestAuth('bob', 'bob') # clear cache cache.delete(safe_key("login_attempts-bob")) cache.delete(safe_key("lockout_user-bob")) self.assertIsNone(cache.get(safe_key('login_attempts-bob'))) self.assertIsNone(cache.get(safe_key('lockout_user-bob'))) request = self._get_request_session_with_auth(view, auth) # first time it creates a cache response = view(request) self.assertEqual(response.status_code, 401) self.assertEqual(response.data['detail'], u"Invalid username/password. For security reasons, " u"after 9 more failed login attempts you'll have to " u"wait 30 minutes before trying again.") self.assertEqual(cache.get(safe_key('login_attempts-bob')), 1) # cache value increments with subsequent attempts response = view(request) self.assertEqual(response.status_code, 401) self.assertEqual(response.data['detail'], u"Invalid username/password. For security reasons, " u"after 8 more failed login attempts you'll have to " u"wait 30 minutes before trying again.") self.assertEqual(cache.get(safe_key('login_attempts-bob')), 2) # login_attempts doesn't increase with correct login auth = DigestAuth('bob', 'bobbob') request = self._get_request_session_with_auth(view, auth) response = view(request) self.assertEqual(response.status_code, 200) self.assertEqual(cache.get(safe_key('login_attempts-bob')), 2) # lockout_user cache created upon fifth attempt auth = DigestAuth('bob', 'bob') request = self._get_request_session_with_auth(view, auth) self.assertFalse(send_account_lockout_email.called) cache.set(safe_key('login_attempts-bob'), 9) self.assertIsNone(cache.get(safe_key('lockout_user-bob'))) response = view(request) self.assertEqual(response.status_code, 401) self.assertEqual(response.data['detail'], u"Locked out. Too many wrong username/password " u"attempts. Try again in 30 minutes.") self.assertEqual(cache.get(safe_key('login_attempts-bob')), 10) self.assertIsNotNone(cache.get(safe_key('lockout_user-bob'))) lockout = datetime.strptime( cache.get(safe_key('lockout_user-bob')), '%Y-%m-%dT%H:%M:%S') self.assertIsInstance(lockout, datetime) # email sent upon limit being reached with right arguments subject_path = 'account_lockout/lockout_email_subject.txt' self.assertTrue(send_account_lockout_email.called) email_subject = render_to_string(subject_path) self.assertIn( email_subject, send_account_lockout_email.call_args[1]['args']) self.assertEqual( send_account_lockout_email.call_count, 2, "Called twice") # subsequent login fails after lockout even with correct credentials auth = DigestAuth('bob', 'bobbob') request = self._get_request_session_with_auth(view, auth) response = view(request) self.assertEqual(response.status_code, 401) self.assertEqual(response.data['detail'], u"Locked out. Too many wrong username/password " u"attempts. Try again in 30 minutes.") # clear cache cache.delete(safe_key("login_attempts-bob")) cache.delete(safe_key("lockout_user-bob"))
def test_safe_key(self): """Test safe_key() function returns a hashed key""" self.assertEqual( safe_key("hello world"), "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")