jazzband / django-axes

Keep track of failed login attempts in Django-powered sites.
MIT License
1.5k stars 348 forks source link

FEATURE REQUEST: Permit using IpWare in non-strict mode #1251

Open OscarVanL opened 1 month ago

OscarVanL commented 1 month ago

Is your feature request related to a problem? Please describe. My application resides behind a Google Application Load Balancer. To parse the client's IP I must use the X-Forwarded-For header, which is documented in GCP's docs here.

To achieve this, I set the following config:

AXES_IPWARE_META_PRECEDENCE_ORDER = ['HTTP_X_FORWARDED_FOR']
AXES_IPWARE_PROXY_COUNT = 1

This works fine when the header is formed like so:

X-Forwarded-For: <client-ip>,<load-balancer-ip>

However, because the Google loadbalancer will propagate the user's own supplied X-Forwarded-For header, it may also be in this format:

X-Forwarded-For: <supplied-value>,<client-ip>,<load-balancer-ip>

As this is a valid header, I want axes to continue to use <client-ip>, which is fully determinable because we set PROXY_COUNT accordingly, so it should use the value PROXY_COUNT away from the right-most IP address.

However, because axes uses IpWare in strict mode, it will reject this header and instead populate the client's IP as None.

Here's how this currently works:

  1. Axes calls IpWare using this helper function: https://github.com/jazzband/django-axes/blob/9acda1f8924b7c01ef271569528a2b2fb3232a74/axes/helpers.py#L196-L203
  2. And IpWare handles this here.
    • In the exert, this calls get_client_ip hardcoded with strict=True. This is not exposed as a configurable value
  3. Because we have strict=True means the is_proxy_count_valid check in python-ipware fails in my described scenario. See here.

    • This is because when the user supplies their own value the ip_count is not exactly proxy_count+1 parts. There is an indefinite number of parts (as the user-supplied value may be any value with any number of parts).

    • However, if I could use the same logic with strict=False, all my problems would go away, because the get_best_ip function would still resolve to the correct client IP (right-most, offset by the configured proxy count):

      if self.proxy_count is not None:
          best_client_ip_index = self.proxy_count + 1
          best_client_ip = ip_list[-best_client_ip_index]
          return best_client_ip, True

Describe the solution you'd like I would like django-axes to expose a AXES_IPWARE_STRICT config item. The value could default to True for backwards-compatibility reasons. This should get propagated to ipware's get_client_ip arg:

    def get_client_ip(
        self,
        meta: Dict[str, str],
        strict: bool = False,
    ) -> Tuple[OptionalIpAddressType, bool]:

Describe alternatives you've considered Alternatively, I could use django-axes's config item AXES_CLIENT_IP_CALLABLE to define my own IP resolver function. This would allow me to define my own logic, such as calling IpWare myself with the desired config.

aleksihakli commented 1 month ago

Hey,

if you're up to it, I think adding the flag for strict or non-strict resolution via e.g. your proposed AXES_IPWARE_STRICT or similarly named flag sounds like a good solution 👍