def test_get_cache_key(self, get_ip_mock): """ Test the cache key format""" # Getting cache key from request ip_address = '127.0.0.1' cache_hash_key = 'axes-{}'.format( hashlib.md5(ip_address.encode()).hexdigest()) request_factory = RequestFactory() request = request_factory.post('/admin/login/', data={ 'username': self.VALID_USERNAME, 'password': '******' }) self.assertEqual(cache_hash_key, get_cache_key(request)) # Getting cache key from AccessAttempt Object attempt = AccessAttempt( user_agent='<unknown>', ip_address=ip_address, username=self.VALID_USERNAME, get_data='', post_data='', http_accept=request.META.get('HTTP_ACCEPT', '<unknown>'), path_info=request.META.get('PATH_INFO', '<unknown>'), failures_since_start=0, ) self.assertEqual(cache_hash_key, get_cache_key(attempt))
def test_get_cache_key_credentials(self, _): """ Test the cache key format. """ # Getting cache key from request ip_address = '127.0.0.1' cache_hash_key = 'axes-{}'.format( hashlib.md5(ip_address.encode()).hexdigest() ) request_factory = RequestFactory() request = request_factory.post('/admin/login/', data={ 'username': self.VALID_USERNAME, 'password': '******' }) # Difference between the upper test: new call signature with credentials credentials = {'username': self.VALID_USERNAME} self.assertEqual(cache_hash_key, get_cache_key(request, credentials)) # Getting cache key from AccessAttempt Object attempt = AccessAttempt( user_agent='<unknown>', ip_address=ip_address, username=self.VALID_USERNAME, get_data='', post_data='', http_accept=request.META.get('HTTP_ACCEPT', '<unknown>'), path_info=request.META.get('PATH_INFO', '<unknown>'), failures_since_start=0, ) self.assertEqual(cache_hash_key, get_cache_key(attempt))
def post_delete_access_attempt(self, instance, **kwargs): # pylint: disable=unused-argument """ Update cache after deleting AccessAttempts. """ cache_hash_key = get_cache_key(instance) get_axes_cache().delete(cache_hash_key)
def post_save_access_attempt(self, instance, **kwargs): # pylint: disable=unused-argument """ Update cache after saving AccessAttempts. """ cache_hash_key = get_cache_key(instance) if not get_axes_cache().get(cache_hash_key): cache_timeout = get_cache_timeout() get_axes_cache().set(cache_hash_key, instance.failures_since_start, cache_timeout)
def post_save_access_attempt(self, instance, **kwargs): # pylint: disable=unused-argument """ Update cache after saving AccessAttempts. """ cache_key = get_cache_key(instance) if not get_axes_cache().get(cache_key): get_axes_cache().set( cache_key, instance.failures_since_start, get_cache_timeout(), )
def get_context_data(self, **kwargs): cache_hash_key = get_cache_key(self.request) attempt = get_axes_cache().get(cache_hash_key) if not attempt: attempt = 0 username = self.request.POST.get('auth-username', None) time = settings.AXES_COOLOFF_TIME attempts_left = (settings.AXES_FAILURE_LIMIT - attempt) kwargs= dict(kwargs, attempt=attempt, username=username, time=time, attempts_left=attempts_left) context = super().get_context_data(**kwargs) self.set_partner_site_info() context[self.redirect_field_name] = self.request.POST.get( self.redirect_field_name, self.request.GET.get(self.redirect_field_name, '') ) return context
def reset(ip=None, username=None): """Reset records that match ip or username, and return the count of removed attempts. """ count = 0 attempts = AccessAttempt.objects.all() if ip: attempts = attempts.filter(ip_address=ip) if username: attempts = attempts.filter(username=username) if attempts: count = attempts.count() for attempt in attempts: cache_hash_key = get_cache_key(attempt) if cache.get(cache_hash_key): cache.delete(cache_hash_key) attempts.delete() return count
def reset(ip=None, username=None): """Reset records that match ip or username, and return the count of removed attempts. """ count = 0 attempts = AccessAttempt.objects.all() if ip: attempts = attempts.filter(ip_address=ip) if username: attempts = attempts.filter(username=username) if attempts: count = attempts.count() # import should be here to avoid circular dependency with get_ip from axes.attempts import get_cache_key for attempt in attempts: cache_hash_key = get_cache_key(attempt) if cache.get(cache_hash_key): cache.delete(cache_hash_key) attempts.delete() return count
def user_login_failed(self, sender, credentials, request, **kwargs): # pylint: disable=unused-argument """ When user login fails, save AccessAttempt record in database and lock user out if necessary. :raises AxesSignalPermissionDenied: if user should is locked out """ if request is None: log.warning( 'AxesHandler.user_login_failed does not function without a request.' ) return username = get_client_username(request, credentials) ip_address = get_client_ip(request) user_agent = get_client_user_agent(request) path_info = request.META.get('PATH_INFO', '<unknown>')[:255] http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')[:1025] client_str = get_client_str(username, ip_address, user_agent, path_info) if settings.AXES_NEVER_LOCKOUT_WHITELIST and ip_in_whitelist( ip_address): log.info('Login failed from whitelisted IP %s.', ip_address) return attempts = get_user_attempts(request, credentials) cache_key = get_cache_key(request, credentials) num_failures_cached = get_axes_cache().get(cache_key) if num_failures_cached: failures = num_failures_cached elif attempts: failures = attempts.aggregate( Max('failures_since_start'), )['failures_since_start__max'] else: failures = 0 # add a failed attempt for this user failures += 1 get_axes_cache().set( cache_key, failures, get_cache_timeout(), ) if attempts: # Update existing attempt information but do not touch the username, ip_address, or user_agent fields, # because attackers can request the site with multiple different usernames, addresses, or programs. for attempt in attempts: template = '{}\n---------\n{}' attempt.get_data = template.format( attempt.get_data, query2str(request.GET), ) attempt.post_data = template.format(attempt.post_data, query2str(request.POST)) attempt.http_accept = http_accept attempt.path_info = path_info attempt.failures_since_start = failures attempt.attempt_time = now() attempt.save() log.info( 'AXES: Repeated login failure by %s. Count = %d of %d', client_str, failures, settings.AXES_FAILURE_LIMIT, ) else: # Record failed attempt. Whether or not the IP address or user agent is # used in counting failures is handled elsewhere, so we just record # everything here. AccessAttempt.objects.create( username=username, ip_address=ip_address, user_agent=user_agent, get_data=query2str(request.GET), post_data=query2str(request.POST), http_accept=http_accept, path_info=path_info, failures_since_start=failures, ) log.info( 'AXES: New login failure by %s. Creating access record.', client_str, ) if is_already_locked(request, credentials): log.warning( 'AXES: Locked out %s after repeated login failures.', client_str, ) user_locked_out.send( 'axes', request=request, username=username, ip_address=ip_address, ) raise AxesSignalPermissionDenied( 'Locked out due to repeated login failures.')
def log_user_login_failed(sender, credentials, request, **kwargs): # pylint: disable=unused-argument """ Create an AccessAttempt record if the login wasn't successful """ if request is None: log.warning('Attempt to authenticate with a custom backend failed.') return ip_address = get_client_ip(request) username = get_client_username(request) user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255] path_info = request.META.get('PATH_INFO', '<unknown>')[:255] http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')[:1025] if settings.AXES_NEVER_LOCKOUT_WHITELIST and ip_in_whitelist(ip_address): return failures = 0 attempts = get_user_attempts(request) cache_hash_key = get_cache_key(request) cache_timeout = get_cache_timeout() failures_cached = get_axes_cache().get(cache_hash_key) if failures_cached is not None: failures = failures_cached else: for attempt in attempts: failures = max(failures, attempt.failures_since_start) # add a failed attempt for this user failures += 1 get_axes_cache().set(cache_hash_key, failures, cache_timeout) # has already attempted, update the info if attempts: for attempt in attempts: attempt.get_data = '%s\n---------\n%s' % ( attempt.get_data, query2str(request.GET), ) attempt.post_data = '%s\n---------\n%s' % (attempt.post_data, query2str(request.POST)) attempt.http_accept = http_accept attempt.path_info = path_info attempt.failures_since_start = failures attempt.attempt_time = timezone.now() attempt.save() log.info( 'AXES: Repeated login failure by %s. Count = %d of %d', get_client_str(username, ip_address, user_agent, path_info), failures, settings.AXES_FAILURE_LIMIT) else: # Record failed attempt. Whether or not the IP address or user agent is # used in counting failures is handled elsewhere, so we just record # everything here. AccessAttempt.objects.create( user_agent=user_agent, ip_address=ip_address, username=username, get_data=query2str(request.GET), post_data=query2str(request.POST), http_accept=http_accept, path_info=path_info, failures_since_start=failures, ) log.info('AXES: New login failure by %s. Creating access record.', get_client_str(username, ip_address, user_agent, path_info)) # no matter what, we want to lock them out if they're past the number of # attempts allowed, unless the user is set to notlockable if (failures >= settings.AXES_FAILURE_LIMIT and settings.AXES_LOCK_OUT_AT_FAILURE and is_user_lockable(request)): log.warning( 'AXES: locked out %s after repeated login attempts.', get_client_str(username, ip_address, user_agent, path_info)) # send signal when someone is locked out. user_locked_out.send('axes', request=request, username=username, ip_address=ip_address)
def delete_cache_after_delete(instance, **kwargs): # pylint: disable=unused-argument cache_hash_key = get_cache_key(instance) get_axes_cache().delete(cache_hash_key)
def update_cache_after_save(instance, **kwargs): # pylint: disable=unused-argument cache_hash_key = get_cache_key(instance) if not get_axes_cache().get(cache_hash_key): cache_timeout = get_cache_timeout() get_axes_cache().set(cache_hash_key, instance.failures_since_start, cache_timeout)
def user_login_failed(self, sender, credentials, request, **kwargs): # pylint: disable=unused-argument """ When user login fails, save AccessAttempt record in database and lock user out if necessary. :raises AxesSignalPermissionDenied: if user should is locked out """ if request is None: log.warning('AxesHandler.user_login_failed does not function without a request.') return ip_address = get_client_ip(request) username = get_client_username(request, credentials) user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255] path_info = request.META.get('PATH_INFO', '<unknown>')[:255] http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')[:1025] if settings.AXES_NEVER_LOCKOUT_WHITELIST and ip_in_whitelist(ip_address): log.info('Login failed from whitelisted IP %s.', ip_address) return failures = 0 attempts = get_user_attempts(request, credentials) cache_hash_key = get_cache_key(request, credentials) cache_timeout = get_cache_timeout() failures_cached = get_axes_cache().get(cache_hash_key) if failures_cached is not None: failures = failures_cached else: for attempt in attempts: failures = max(failures, attempt.failures_since_start) # add a failed attempt for this user failures += 1 get_axes_cache().set(cache_hash_key, failures, cache_timeout) # has already attempted, update the info if attempts: for attempt in attempts: attempt.get_data = '%s\n---------\n%s' % ( attempt.get_data, query2str(request.GET), ) attempt.post_data = '%s\n---------\n%s' % ( attempt.post_data, query2str(request.POST) ) attempt.http_accept = http_accept attempt.path_info = path_info attempt.failures_since_start = failures attempt.attempt_time = now() attempt.save() log.info( 'AXES: Repeated login failure by %s. Count = %d of %d', get_client_str(username, ip_address, user_agent, path_info), failures, settings.AXES_FAILURE_LIMIT, ) else: # Record failed attempt. Whether or not the IP address or user agent is # used in counting failures is handled elsewhere, so we just record # everything here. AccessAttempt.objects.create( user_agent=user_agent, ip_address=ip_address, username=username, get_data=query2str(request.GET), post_data=query2str(request.POST), http_accept=http_accept, path_info=path_info, failures_since_start=failures, ) log.info( 'AXES: New login failure by %s. Creating access record.', get_client_str(username, ip_address, user_agent, path_info), ) # no matter what, we want to lock them out if they're past the number of # attempts allowed, unless the user is set to notlockable if ( failures >= settings.AXES_FAILURE_LIMIT and settings.AXES_LOCK_OUT_AT_FAILURE and is_user_lockable(request, credentials) ): log.warning( 'AXES: Locked out %s after repeated login failures.', get_client_str(username, ip_address, user_agent, path_info), ) # send signal when someone is locked out. user_locked_out.send( 'axes', request=request, username=username, ip_address=ip_address, ) raise AxesSignalPermissionDenied('Locked out due to repeated login failures.')
def delete_cache_after_delete(instance, **kwargs): cache_hash_key = get_cache_key(instance) cache.delete(cache_hash_key)
def update_cache_after_save(instance, **kwargs): cache_hash_key = get_cache_key(instance) if not cache.get(cache_hash_key): cache_timeout = get_cache_timeout() cache.set(cache_hash_key, instance.failures_since_start, cache_timeout)