def is_user_lockable(request): """Check if the user has a profile with nolockout If so, then return the value to see if this user is special and doesn't get their account locked out """ if hasattr(request.user, 'nolockout'): return not request.user.nolockout if request.method != 'POST': return True try: field = getattr(get_user_model(), 'USERNAME_FIELD', 'username') kwargs = { field: get_client_username(request) } user = get_user_model().objects.get(**kwargs) if hasattr(user, 'nolockout'): # need to invert since we need to return # false for users that can't be blocked return not user.nolockout except get_user_model().DoesNotExist: # not a valid user return True # Default behavior for a user to be lockable return True
def is_user_lockable(request: HttpRequest, credentials: dict = None) -> bool: """ Check if the given request or credentials refer to a whitelisted user object A whitelisted user has the magic ``nolockout`` property set. If the property is unknown or False or the user can not be found, this implementation fails gracefully and returns True. """ username_field = getattr(get_user_model(), 'USERNAME_FIELD', 'username') username_value = get_client_username(request, credentials) kwargs = { username_field: username_value } UserModel = get_user_model() try: user = UserModel.objects.get(**kwargs) return not user.nolockout except (UserModel.DoesNotExist, AttributeError): pass return True
def get_cache_key(request_or_attempt, credentials: dict = None) -> str: """ Build cache key name from request or AccessAttempt object. :param request_or_attempt: HttpRequest or AccessAttempt object :param credentials: credentials containing user information :return cache_key: Hash key that is usable for Django cache backends """ if isinstance(request_or_attempt, AccessAttempt): username = request_or_attempt.username ip_address = request_or_attempt.ip_address user_agent = request_or_attempt.user_agent else: username = get_client_username(request_or_attempt, credentials) ip_address = get_client_ip(request_or_attempt) user_agent = get_client_user_agent(request_or_attempt) filter_kwargs = get_filter_kwargs(username, ip_address, user_agent) cache_key_components = ''.join(filter_kwargs.values()) cache_key_digest = md5(cache_key_components.encode()).hexdigest() cache_key = 'axes-{}'.format(cache_key_digest) return cache_key
def get_cache_key(request_or_obj, credentials=None): """ 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_client_ip(request_or_obj) un = get_client_username(request_or_obj, credentials) 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 and not settings.AXES_ONLY_USER_FAILURES: attributes += ua cache_hash_key = 'axes-{}'.format(md5(attributes).hexdigest()) return cache_hash_key
def lockout_response(request): context = { 'failure_limit': Settings.objects.first().failure_limit, 'username': get_client_username(request) or '' } cool_off = settings.AXES_COOLOFF_TIME if cool_off: if isinstance(cool_off, (int, float)): cool_off = timedelta(hours=cool_off) context.update({'cooloff_time': iso8601(cool_off)}) if request.is_ajax(): return HttpResponse( json.dumps(context), content_type='application/json', status=403, ) elif settings.AXES_LOCKOUT_TEMPLATE: return render(request, settings.AXES_LOCKOUT_TEMPLATE, context, status=403) elif settings.AXES_LOCKOUT_URL: return HttpResponseRedirect(settings.AXES_LOCKOUT_URL) return HttpResponse(get_lockout_message(), status=403)
def test_default_get_client_username(self): expected = 'test-username' request = HttpRequest() request.POST['username'] = expected actual = get_client_username(request) self.assertEqual(expected, actual)
def test_custom_get_client_username(self): provided = 'test-username' expected = 'prefixed-' + provided request = HttpRequest() request.POST['username'] = provided actual = get_client_username(request) self.assertEqual(expected, actual)
def test_default_get_client_username_credentials(self): expected = 'test-username' expected_in_credentials = 'test-credentials-username' request = HttpRequest() request.POST['username'] = expected credentials = {'username': expected_in_credentials} actual = get_client_username(request, credentials) self.assertEqual(expected_in_credentials, actual)
def query_user_attempts(request: HttpRequest, credentials: dict = None) -> QuerySet: """ Return a queryset of AccessAttempts that match the given request and credentials. """ username = get_client_username(request, credentials) ip_address = get_client_ip(request) user_agent = get_client_user_agent(request) filter_kwargs = get_filter_kwargs(username, ip_address, user_agent) return AccessAttempt.objects.filter(**filter_kwargs)
def test_custom_get_client_username_from_credentials(self): provided = 'test-username' expected = 'prefixed-' + provided provided_in_credentials = 'test-credentials-username' expected_in_credentials = 'prefixed-' + provided_in_credentials request = HttpRequest() request.POST['username'] = provided credentials = {'username': provided_in_credentials} actual = get_client_username(request, credentials) self.assertEqual(expected_in_credentials, actual)
def test_default_get_client_username_credentials(self): expected = 'test-username' expected_in_credentials = 'test-credentials-username' request = HttpRequest() request.POST['username'] = expected credentials = { 'username': expected_in_credentials } actual = get_client_username(request, credentials) self.assertEqual(expected_in_credentials, actual)
def filter_user_attempts(request: HttpRequest, credentials: dict = None) -> QuerySet: """ Return a queryset of AccessAttempts that match the given request and credentials. """ username = get_client_username(request, credentials) ip_address = get_client_ip(request) user_agent = get_client_user_agent(request) filter_kwargs = get_filter_kwargs(username, ip_address, user_agent) return AccessAttempt.objects.filter(**filter_kwargs)
def is_user_lockable(request: HttpRequest, credentials: dict = None) -> bool: """ Check if the given request or credentials refer to a whitelisted user object A whitelisted user has the magic ``nolockout`` property set. If the property is unknown or False or the user can not be found, this implementation fails gracefully and returns True. """ username_field = getattr(get_user_model(), 'USERNAME_FIELD', 'username') username_value = get_client_username(request, credentials) kwargs = {username_field: username_value} UserModel = get_user_model() try: user = UserModel.objects.get(**kwargs) return not user.nolockout except (UserModel.DoesNotExist, AttributeError): pass return True
def _query_user_attempts(request): """Returns access attempt record if it exists. Otherwise return None. """ ip = get_client_ip(request) username = get_client_username(request) 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 and not settings.AXES_ONLY_USER_FAILURES: params['user_agent'] = ua attempts = AccessAttempt.objects.filter(**params) return attempts
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 test_get_client_username(self): self.assertEqual(get_client_username(HttpRequest(), {}), 'example')
def test_get_client_username_invalid_callable_too_many_arguments(self): with self.assertRaises(TypeError): get_client_username(HttpRequest(), {})
def test_get_client_username_too_many_arguments_invalid_callable(self): with self.assertRaises(TypeError): actual = get_client_username(HttpRequest(), {})
def test_get_client_username_invalid_callable_too_few_arguments(self): with self.assertRaises(TypeError): get_client_username(HttpRequest(), {})
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 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.')