def is_already_locked(request): ip = get_ip(request) if (settings.AXES_ONLY_USER_FAILURES or settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP ) and request.method == 'GET': return False if settings.AXES_NEVER_LOCKOUT_WHITELIST and ip_in_whitelist(ip): return False if settings.AXES_ONLY_WHITELIST and not ip_in_whitelist(ip): return True if ip_in_blacklist(ip): return True if not is_user_lockable(request): return False cache_hash_key = get_cache_key(request) failures_cached = cache.get(cache_hash_key) if failures_cached is not None: return (failures_cached >= settings.AXES_FAILURE_LIMIT and settings.AXES_LOCK_OUT_AT_FAILURE) else: for attempt in get_user_attempts(request): if (attempt.failures_since_start >= settings.AXES_FAILURE_LIMIT and settings.AXES_LOCK_OUT_AT_FAILURE): return True return False
def get_cache_key(request_or_obj): """ Build cache key name from request or AccessAttempt object. :param request_or_obj: Request or AccessAttempt object :return cache-key: String, key to be used in cache system """ if isinstance(request_or_obj, AccessAttempt): ip = request_or_obj.ip_address un = request_or_obj.username ua = request_or_obj.user_agent else: ip = get_ip(request_or_obj) un = request_or_obj.POST.get(settings.AXES_USERNAME_FORM_FIELD, None) ua = request_or_obj.META.get('HTTP_USER_AGENT', '<unknown>')[:255] ip = ip.encode('utf-8') if ip else ''.encode('utf-8') un = un.encode('utf-8') if un else ''.encode('utf-8') ua = ua.encode('utf-8') if ua else ''.encode('utf-8') if settings.AXES_ONLY_USER_FAILURES: attributes = un elif settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP: attributes = ip + un else: attributes = ip if settings.AXES_USE_USER_AGENT: attributes += ua cache_hash_key = 'axes-{}'.format(md5(attributes).hexdigest()) return cache_hash_key
def test_custom_header_parsing(self): self.ip = '2001:db8:cafe::17' valid_headers = [ ' 2001:db8:cafe::17 , 2001:db8:cafe::18', ] for header in valid_headers: self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = header self.assertEqual(self.ip, get_ip(self.request))
def test_iis_ipv4_port_stripping(self): self.ip = '192.168.1.1' valid_headers = [ '192.168.1.1:6112', '192.168.1.1:6033, 192.168.1.2:9001', ] for header in valid_headers: self.request.META['HTTP_X_FORWARDED_FOR'] = header self.assertEqual(self.ip, get_ip(self.request))
def test_header_ordering(self): self.ip = '2.2.2.2' valid_headers = [ '4.4.4.4, 3.3.3.3, 2.2.2.2, 1.1.1.1', ' 3.3.3.3, 2.2.2.2, 1.1.1.1', ' 2.2.2.2, 1.1.1.1', ] for header in valid_headers: self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = header self.assertEqual(self.ip, get_ip(self.request))
def test_valid_ipv6_parsing(self): self.ip = '2001:db8:cafe::17' valid_headers = [ '2001:db8:cafe::17', '2001:db8:cafe::17 , 2001:db8:cafe::18', '2001:db8:cafe::17, 2001:db8:cafe::18, 192.168.1.1', ] for header in valid_headers: self.request.META['HTTP_X_FORWARDED_FOR'] = header self.assertEqual(self.ip, get_ip(self.request))
def log_user_logged_in(sender, request, user, **kwargs): """ When a user logs in, update the access log """ username = user.get_username() ip_address = get_ip(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] log.info('AXES: Successful login by {0}.'.format( get_client_str(username, ip_address, user_agent, path_info))) if not settings.AXES_DISABLE_SUCCESS_ACCESS_LOG: AccessLog.objects.create( user_agent=user_agent, ip_address=ip_address, username=username, http_accept=http_accept, path_info=path_info, trusted=True, )
def _query_user_attempts(request): """Returns access attempt record if it exists. Otherwise return None. """ ip = get_ip(request) username = request.POST.get(settings.AXES_USERNAME_FORM_FIELD, None) if settings.AXES_ONLY_USER_FAILURES: attempts = AccessAttempt.objects.filter(username=username) elif settings.AXES_USE_USER_AGENT: ua = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255] attempts = AccessAttempt.objects.filter(user_agent=ua, ip_address=ip, username=username, trusted=True) else: attempts = AccessAttempt.objects.filter(ip_address=ip, username=username, trusted=True) if not attempts: params = {'trusted': False} if settings.AXES_ONLY_USER_FAILURES: params['username'] = username elif settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP: params['username'] = username params['ip_address'] = ip else: params['ip_address'] = ip if settings.AXES_USE_USER_AGENT: params['user_agent'] = ua attempts = AccessAttempt.objects.filter(**params) return attempts
def test_invalid_headers_no_ip(self): self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = '' with self.assertRaises(Warning): get_ip(self.request)
def log_user_login_failed(sender, credentials, request, **kwargs): """ Create an AccessAttempt record if the login wasn't successful """ username_field = get_user_model().USERNAME_FIELD if request is None or username_field not in credentials: log.error('Attempt to authenticate with a custom backend failed.') return ip_address = get_ip(request) username = credentials[username_field] 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 = 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 cache.set(cache_hash_key, failures, cache_timeout) # has already attempted, update the info if len(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() fail_msg = 'AXES: Repeated login failure by {0}.'.format( get_client_str(username, ip_address, user_agent, path_info)) count_msg = 'Count = {0} of {1}'.format( failures, settings.AXES_FAILURE_LIMIT) log.info('{0} {1}'.format(fail_msg, count_msg)) 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 {0}. Creating access record.'.format( 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 {0} after repeated login attempts.'.format( 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 decorated_login(request, *args, **kwargs): # share some useful information if func.__name__ != 'decorated_login' and VERBOSE: log_decorated_call(func, args, kwargs) # TODO: create a class to hold the attempts records and perform checks # with its methods? or just store attempts=get_user_attempts here and # pass it to the functions # also no need to keep accessing these: # ip = request.META.get('REMOTE_ADDR', '') # ua = request.META.get('HTTP_USER_AGENT', '<unknown>') # username = request.POST.get(USERNAME_FORM_FIELD, None) # if the request is currently under lockout, do not proceed to the # login function, go directly to lockout url, do not pass go, do not # collect messages about this login attempt if is_already_locked(request): return lockout_response(request, populate_login_form=True) # call the login function response = func(request, *args, **kwargs) if func.__name__ == 'decorated_login': # if we're dealing with this function itself, don't bother checking # for invalid login attempts. I suppose there's a bunch of # recursion going on here that used to cause one failed login # attempt to generate 10+ failed access attempt records (with 3 # failed attempts each supposedly) return response if request.method == 'POST': # see if the login was successful if request.is_ajax(): login_unsuccessful = is_ajax_login_failed(response) else: login_unsuccessful = is_login_failed(response) # create a log of a login attempt create_access_log(request, login_unsuccessful) user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255] http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')[:1025] path_info = request.META.get('PATH_INFO', '<unknown>')[:255] if not DISABLE_ACCESS_LOG: username = request.POST.get(USERNAME_FORM_FIELD, None) ip_address = get_ip(request) if login_unsuccessful or not DISABLE_SUCCESS_ACCESS_LOG: AccessLog.objects.create( user_agent=user_agent, ip_address=ip_address, username=username, http_accept=http_accept, path_info=path_info, trusted=not login_unsuccessful, ) if not login_unsuccessful and not DISABLE_SUCCESS_ACCESS_LOG: log_successful_attempt(username, ip_address, user_agent, path_info) if check_request(request, login_unsuccessful): return response return lockout_response(request, populate_login_form=True) return response