Repository: jazzband/django-axes

Issue:
Add Django REST Framework JSON payload support for login
Consider the following curl request:

```
curl --request POST \
  --url http://localhost:8000/login/ \
  --header 'content-type: application/json' \
  --data '{
    "username" : "auser@foo.com",
    "password" : "dshtyh"
}'
```


Django rest framework places the data payload for the above in its `request.data` as opposed to the `request.POST` dictionary

https://www.django-rest-framework.org/tutorial/2-requests-and-responses/#request-objects

As such, any username is not picked up which practically breaks any integration that doesn't use IP or user agent to cross protect because of the following line of code

https://github.com/jazzband/django-axes/blob/master/axes/helpers.py#L145


Discussion:
Hey,

and good catch. You can use the `AXES_USERNAME_CALLABLE` setting to circumvent this problem in your `settings.py`:

    AXES_USERNAME_CALLABLE = lambda request, credentials: request.data["username"]

Remember to check that you have the [JSON parser in your DRF parsers](https://www.django-rest-framework.org/api-guide/parsers/).

If you'd like, improving this via PR would be great. If the `request` has the `data` attribute it could be better to use that than the request.POST attribute, so this could be a fairly simple improvement to implement.

Just bear in mind, that there are many users with many different configurations running that supply many different kinds of request objects to the backend, so the solution should ideally be non-invasive.
Hey @aleksihakli, 

Thanks for the suggestion, thats good to know thanks. Wasn't sure how you'd actually go about implementing this from the library point of view but just thought it might be worth mentioning in the docs for the drf integration at least if needed :)
@msaysell this information is valuable to people that come looking a solution for the problem. They can see it's logged in this issue and can find the related discussion, so I'll leave the issue open until we can write the updates for the docs. 

If you can implement a clean fix for this on the library level, I think it would be neat.

One option would be to check `request.data` is present and/or check that `rest_framework` is in `INSTALLED_APPS`, and use the `request.data` instead of `request.POST` in those cases, as it would always contain the correct and parsed data.

Another option would be to use a feature flag such as `AXES_REST_FRAMEWORK_ACTIVE` or similar so that the user can toggle the use of `request.data` or `request.POST` explicitly?

Implementing this would need tests that check the configuration with and without DRF. Implementing tests would potentially be fairly simple with the correct setup. Django offers the `override_settings` class decorator that can be used to toggle flags in a specific test class and you can implement a few different test settings for use with and without DRF.
Sorry I clicked the wrong button and didn't mean to close the issue...

The fix I used locally is:

```
def _get_username(request, credentials):
    data_username = None
    if hasattr(request, 'data'):
        data_username = request.data.get("username")
    return data_username or request.POST.get("username")


AXES_USERNAME_CALLABLE = _get_username
```
(we use a linting tool that doesn't allow lambdas)
That looks like a good solution. It could be nice to extend that into the library itself and use a feature flag with it :)
@msaysell would you be interested in opening a PR for enabling DRF support for others with this?

Resolving patch:
diff --git a/axes/conf.py b/axes/conf.py
--- a/axes/conf.py
+++ b/axes/conf.py
@@ -93,3 +93,6 @@ class AxesAppConf(AppConf):
         "AXES_META_PRECEDENCE_ORDER",
         getattr(settings, "IPWARE_META_PRECEDENCE_ORDER", ("REMOTE_ADDR",)),
     )
+
+    # set to `True` if using with Django REST Framework
+    REST_FRAMEWORK_ACTIVE = False
diff --git a/axes/helpers.py b/axes/helpers.py
--- a/axes/helpers.py
+++ b/axes/helpers.py
@@ -143,7 +143,9 @@ def get_client_username(request, credentials: dict = None) -> str:
     log.debug(
         "Using parameter request.POST to get username with key settings.AXES_USERNAME_FORM_FIELD"
     )
-    return request.POST.get(settings.AXES_USERNAME_FORM_FIELD, None)
+
+    request_data = request.data if settings.AXES_REST_FRAMEWORK_ACTIVE else request.POST
+    return request_data.get(settings.AXES_USERNAME_FORM_FIELD, None)
 
 
 def get_client_ip_address(request) -> str:


Test patch:
diff --git a/axes/tests/test_utils.py b/axes/tests/test_utils.py
--- a/axes/tests/test_utils.py
+++ b/axes/tests/test_utils.py
@@ -400,6 +400,21 @@ def test_default_get_client_username(self):
 
         self.assertEqual(expected, actual)
 
+    @override_settings(AXES_REST_FRAMEWORK_ACTIVE=True)
+    def test_default_get_client_username_drf(self):
+        class DRFRequest:
+            def __init__(self):
+                self.data = {}
+
+        expected = "test-username"
+
+        request = DRFRequest()
+        request.data["username"] = expected
+
+        actual = get_client_username(request)
+
+        self.assertEqual(expected, actual)
+
     @override_settings(AXES_USERNAME_FORM_FIELD="username")
     def test_default_get_client_username_credentials(self):
         expected = "test-username"