Repository: jazzband/django-axes

Issue:
Always blocking the user until he solves the captcha
Currently, if a user tries to login for 3 times and fails to, he/she will be blocked. If he tries the fourth time with correct credentials, they will be successfully logged in.

I am trying to block a user even if he tries to login with the correct credentials and always redirect him/her to a captcha page. The `AccessAttemp` record will be deleted from the database only when the user solves the captcha. I am wondering how can this be accomplished within `axes`?


Discussion:
@nasief What is your configuration with Axes? Are you using the [`@watch_login`](https://github.com/jazzband/django-axes/blob/master/axes/decorators.py#L199) decorator with login functions? 

In another project we used Axes for a 2-phase login where one login attempt was normal login and the second was MFA confirmation. Both were watched with Axes and both login views worked nicely with the decorator.

Resolving patch:
diff --git a/axes/decorators.py b/axes/decorators.py
--- a/axes/decorators.py
+++ b/axes/decorators.py
@@ -22,12 +22,18 @@
 log = logging.getLogger(LOGGER)
 if VERBOSE:
     log.info('AXES: BEGIN LOG')
-    log.info('Using django-axes ' + axes.get_version())
+    log.info('AXES: Using django-axes ' + axes.get_version())
 
 
 if BEHIND_REVERSE_PROXY:
-    log.debug('Axes is configured to be behind reverse proxy')
-    log.debug('Looking for header value %s', REVERSE_PROXY_HEADER)
+    log.debug('AXES: Axes is configured to be behind reverse proxy')
+    log.debug('AXES: Looking for header value %s', REVERSE_PROXY_HEADER)
+    log.debug(
+        'AXES: Number of proxies configured: {} '
+        '(please check this if you are using a custom header)'.format(
+            NUM_PROXIES
+        )
+    )
 
 
 def is_ipv6(ip):
@@ -50,27 +56,37 @@ def get_ip(request):
         # Please see RFC7239 for additional information:
         #   https://tools.ietf.org/html/rfc7239#section-5
 
+        # The REVERSE_PROXY_HEADER HTTP header is a list
+        # of potentionally unsecure IPs, for example:
+        #   X-Forwarded-For: 1.1.1.1, 11.11.11.11:8080, 111.111.111.111
         ip = request.META.get(REVERSE_PROXY_HEADER, '')
 
+        # We need to know the number of proxies present in the request chain
+        # in order to securely calculate the one IP that is the real client IP.
+        #
+        # This is because IP headers can have multiple IPs in different
+        # configurations, with e.g. the X-Forwarded-For header containing
+        # the originating client IP, proxies and possibly spoofed values.
+        #
+        # If you are using a special header for client calculation such as
+        # the X-Real-IP or the like with nginx, please check this configuration.
+        #
+        # Please see discussion for more information:
+        #   https://github.com/jazzband/django-axes/issues/224
+        ip = [ip.strip() for ip in ip.split(',')][-NUM_PROXIES]
+
+        # Fix IIS adding client port number to 'X-Forwarded-For' header (strip port)
+        if not is_ipv6(ip):
+            ip = ip.split(':', 1)[0]
+
         if not ip:
             raise Warning(
-                'Axes is configured for operation behind a reverse proxy '
+                'AXES: Axes is configured for operation behind a reverse proxy '
                 'but could not find an HTTP header value. Check your proxy '
                 'server settings to make sure this header value is being '
-                'passed. Header value {0}'.format(REVERSE_PROXY_HEADER)
+                'passed. Header name {0}'.format(REVERSE_PROXY_HEADER)
             )
 
-        # X-Forwarded-For IPs can have multiple IPs of which the first one is the
-        # originating reverse and the rest are proxies that are between the client
-        ip = ip.split(',', 1)[0]
-
-        # As spaces are permitted between given X-Forwarded-For IP addresses, strip them as well
-        ip = ip.strip()
-
-        # Fix IIS adding client port number to 'X-Forwarded-For' header (strip port)
-        if not is_ipv6(ip):
-            ip = ip.split(':', 1)[0]
-
         return ip
 
     return request.META.get('REMOTE_ADDR', '')
diff --git a/axes/settings.py b/axes/settings.py
--- a/axes/settings.py
+++ b/axes/settings.py
@@ -22,6 +22,9 @@
 # see if the django app is sitting behind a reverse proxy
 BEHIND_REVERSE_PROXY = getattr(settings, 'AXES_BEHIND_REVERSE_PROXY', False)
 
+# if we are behind a proxy, we need to know how many proxies there are
+NUM_PROXIES = getattr(settings, 'AXES_NUM_PROXIES', 0)
+
 # if the django app is behind a reverse proxy, look for the ip address using this HTTP header value
 REVERSE_PROXY_HEADER = \
     getattr(settings, 'AXES_REVERSE_PROXY_HEADER', 'HTTP_X_FORWARDED_FOR')


Test patch: