From 512452e5800979ddf821f3f549b7e92ff7794407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksi=20H=C3=A4kli?= Date: Wed, 11 Apr 2018 21:13:48 +0300 Subject: [PATCH] Add proxy precendence and count configuration Fixes #286 --- axes/attempts.py | 10 +++---- axes/conf.py | 20 ++++++++++++++ axes/signals.py | 8 ++---- axes/tests/test_access_attempt.py | 2 +- axes/utils.py | 17 ++++++++++++ docs/configuration.rst | 46 +++++++++++++++++++------------ setup.py | 2 +- 7 files changed, 75 insertions(+), 30 deletions(-) diff --git a/axes/attempts.py b/axes/attempts.py index 8bea37d..a645249 100644 --- a/axes/attempts.py +++ b/axes/attempts.py @@ -4,18 +4,16 @@ from hashlib import md5 from django.contrib.auth import get_user_model from django.utils import timezone -from ipware.ip2 import get_client_ip - from axes.conf import settings from axes.models import AccessAttempt -from axes.utils import get_axes_cache +from axes.utils import get_axes_cache, get_client_ip def _query_user_attempts(request): """Returns access attempt record if it exists. Otherwise return None. """ - ip, _ = get_client_ip(request) + ip = get_client_ip(request) username = request.POST.get(settings.AXES_USERNAME_FORM_FIELD, None) if settings.AXES_ONLY_USER_FAILURES: @@ -60,7 +58,7 @@ def get_cache_key(request_or_obj): un = request_or_obj.username ua = request_or_obj.user_agent else: - ip, _ = get_client_ip(request_or_obj) + ip = get_client_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', '')[:255] @@ -176,7 +174,7 @@ def is_user_lockable(request): def is_already_locked(request): - ip, _ = get_client_ip(request) + ip = get_client_ip(request) if ( settings.AXES_ONLY_USER_FAILURES or diff --git a/axes/conf.py b/axes/conf.py index 19f5fbe..f74d2fd 100644 --- a/axes/conf.py +++ b/axes/conf.py @@ -53,3 +53,23 @@ class MyAppConf(AppConf): # message to show when locked out and have cooloff disabled PERMALOCK_MESSAGE = 'Account locked: too many login attempts. Contact an admin to unlock your account.' + + # if your deployment is using reverse proxies, set this value to 'left-most' or 'right-most' per your configuration + PROXY_ORDER = 'left-most' + + # if your deployment is using reverse proxies, set this value to the number of proxies in front of Django + PROXY_COUNT = None + + # if your deployment is using reverse proxies, set to your trusted proxy IP addresses prefixes if needed + PROXY_TRUSTED_IPS = None + + # set to the names of request.META attributes that should be checked for the IP address of the client + # if your deployment is using reverse proxies, ensure that the header attributes are securely set by the proxy + # ensure that the client can not spoof the headers by setting them and sending them through the proxy + META_PRECEDENCE_ORDER = getattr( + settings, 'AXES_META_PRECEDENCE_ORDER', getattr( + settings, 'IPWARE_META_PRECEDENCE_ORDER', ( + 'REMOTE_ADDR', + ) + ) + ) diff --git a/axes/signals.py b/axes/signals.py index 910b712..c1c65a1 100644 --- a/axes/signals.py +++ b/axes/signals.py @@ -8,8 +8,6 @@ from django.dispatch import receiver from django.dispatch import Signal from django.utils import timezone -from ipware.ip import get_ip - from axes.conf import settings from axes.attempts import get_cache_key from axes.attempts import get_cache_timeout @@ -19,7 +17,7 @@ from axes.attempts import ip_in_whitelist from axes.models import AccessLog, AccessAttempt from axes.utils import get_client_str from axes.utils import query2str -from axes.utils import get_axes_cache +from axes.utils import get_axes_cache, get_client_ip log = logging.getLogger(settings.AXES_LOGGER) @@ -36,7 +34,7 @@ def log_user_login_failed(sender, credentials, request, **kwargs): log.error('Attempt to authenticate with a custom backend failed.') return - ip_address = get_ip(request) + ip_address = get_client_ip(request) username = credentials[settings.AXES_USERNAME_FORM_FIELD] user_agent = request.META.get('HTTP_USER_AGENT', '')[:255] path_info = request.META.get('PATH_INFO', '')[:255] @@ -128,7 +126,7 @@ 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) + ip_address = get_client_ip(request) user_agent = request.META.get('HTTP_USER_AGENT', '')[:255] path_info = request.META.get('PATH_INFO', '')[:255] http_accept = request.META.get('HTTP_ACCEPT', '')[:1025] diff --git a/axes/tests/test_access_attempt.py b/axes/tests/test_access_attempt.py index dfe7952..3d3d7dd 100644 --- a/axes/tests/test_access_attempt.py +++ b/axes/tests/test_access_attempt.py @@ -189,7 +189,7 @@ class AccessAttemptTest(TestCase): # Make a login attempt again self.test_valid_login() - @patch('ipware.ip.get_ip', return_value='127.0.0.1') + @patch('axes.utils.get_client_ip', return_value='127.0.0.1') def test_get_cache_key(self, get_ip_mock): """ Test the cache key format""" # Getting cache key from request diff --git a/axes/utils.py b/axes/utils.py index 3b53e5a..b5ce124 100644 --- a/axes/utils.py +++ b/axes/utils.py @@ -7,6 +7,8 @@ from socket import inet_pton, AF_INET6, error from django.core.cache import cache, caches from django.utils import six +import ipware.ip2 + from axes.conf import settings from axes.models import AccessAttempt @@ -49,6 +51,21 @@ def get_client_str(username, ip_address, user_agent=None, path_info=None): return client +def get_client_ip(request): + client_ip_attribute = 'axes_client_ip' + + if not hasattr(request, client_ip_attribute): + client_ip, _ = ipware.ip2.get_client_ip( + request, + proxy_order=settings.AXES_PROXY_ORDER, + proxy_count=settings.AXES_PROXY_COUNT, + proxy_trusted_ips=settings.AXES_PROXY_TRUSTED_IPS, + request_header_order=settings.AXES_META_PRECEDENCE_ORDER, + ) + setattr(request, client_ip_attribute, client_ip) + return getattr(request, client_ip_attribute) + + def is_ipv6(ip): try: inet_pton(AF_INET6, ip) diff --git a/docs/configuration.rst b/docs/configuration.rst index 5d104f1..2b5379a 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -3,7 +3,7 @@ Configuration ============= -Add `axes` to your ``INSTALLED_APPS``:: +Add ``axes`` to your ``INSTALLED_APPS``:: INSTALLED_APPS = ( 'django.contrib.admin', @@ -27,25 +27,15 @@ Add ``axes.backends.AxesModelBackend`` to the top of ``AUTHENTICATION_BACKENDS`` Run ``python manage.py migrate`` to sync the database. -Configure `django-ipware `_ to your liking. Pay close attention to the `IPWARE_META_PRECEDENCE_ORDER `_ setting. Please note that this configuration is required for functional security in your project. A good starting point for a project running without a reverse proxy could be:: - - IPWARE_META_PRECEDENCE_ORDER = ( - 'REMOTE_ADDR', - ) - -Things to you might need to change in your code, especially if you get a ``AxesModelBackend.RequestParameterRequired``: - -- make sure any calls to ``django.contrib.auth.authenticate`` pass the request. - -- make sure any auth libraries you use that call the authentication middleware stack pass request. Notably Django Rest - Framework (DRF) ``BasicAuthentication`` does not pass request. `Here is an example workaround for DRF`_. - -.. _Here is an example workaround for DRF: https://gist.github.com/markddavidoff/7e442b1ea2a2e68d390e76731c35afe7 - - Known configuration problems ---------------------------- +Axes has a few configuration issues with external packages and specific cache backends +due to their internal implementations. + +Cache problems +~~~~~~~~~~~~~~ + If you are running Axes on a deployment with in-memory Django cache, the ``axes_reset`` functionality might not work predictably. @@ -80,6 +70,28 @@ to your ``settings.py`` file:: There are no known problems in other cache backends such as ``DummyCache``, ``FileBasedCache``, or ``MemcachedCache`` backends. +Authentication backend problems +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you get ``AxesModelBackend.RequestParameterRequired`` exceptions, +make sure any auth libraries and middleware you use pass the request object to authenticate. +Notably Django Rest Framework (DRF) ``BasicAuthentication`` does not pass request. +`Here is an example workaround for DRF `_. + +Reverse proxy configuration +--------------------------- + +Django Axes makes use of ``django-ipware`` package to detect the IP address of the client +and uses some conservative configuration parameters by default for security. + +If you are using reverse proxies, you will need to configure one or more of the +following settings to suit your set up to correctly resolve client IP addresses: + +* ``AXES_PROXY_COUNT``: The number of reverse proxies in front of Django as an integer. Default: ``None`` +* ``AXES_META_PRECEDENCE_ORDER``: The names of ``request.META`` attributes as a tuple of strings + to check to get the client IP address. Check the Django documentation for header naming conventions. + Default: ``IPWARE_META_PRECEDENCE_ORDER`` setting if set, else ``('REMOTE_ADDR', )`` + Customizing Axes ---------------- diff --git a/setup.py b/setup.py index 0a7b546..5cd5552 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setup( install_requires=[ 'pytz', 'django-appconf', - 'django-ipware', + 'django-ipware>=2.0.2', 'win_inet_pton ; python_version < "3.4" and sys_platform == "win32"' ], include_package_data=True,