Repository: pennersr/django-allauth

Issue:
Alternative to X-REQUESTED-WITH for `fetch`
As `fetch` doesn't send that header. This confused me for a while, as I wasn't able to get any JSON responses.

I suggest that if the _request_'s content type is `application/json` then it is treated as an ajax/fetch request.


Discussion:
Django documents this:

> The HttpRequest.is_ajax() method is deprecated as it relied on a jQuery-specific way of signifying AJAX calls, while current usage tends to use the JavaScript Fetch API. Depending on your use case, you can either write your own AJAX detection method, or use the new HttpRequest.accepts() method if your code depends on the client Accept HTTP header.

I think that the request should be considered ajax if one of the following holds:

- X-REQUESTED-WITH points to XMLHttpRequest

- The request content type is `application/json`.

- The accept header does not contain `text/html`.


Resolving patch:
diff --git a/allauth/account/adapter.py b/allauth/account/adapter.py
--- a/allauth/account/adapter.py
+++ b/allauth/account/adapter.py
@@ -519,7 +519,11 @@ def authentication_failed(self, request, **credentials):
             cache.set(cache_key, data, app_settings.LOGIN_ATTEMPTS_TIMEOUT)
 
     def is_ajax(self, request):
-        return request.is_ajax()
+        return any([
+            request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest',
+            request.content_type == 'application/json',
+            request.META.get('HTTP_ACCEPT') == 'application/json',
+        ])
 
 
 def get_adapter(request=None):
diff --git a/allauth/socialaccount/models.py b/allauth/socialaccount/models.py
--- a/allauth/socialaccount/models.py
+++ b/allauth/socialaccount/models.py
@@ -297,7 +297,7 @@ def state_from_request(cls, request):
     @classmethod
     def stash_state(cls, request):
         state = cls.state_from_request(request)
-        verifier = get_random_string()
+        verifier = get_random_string(12)
         request.session['socialaccount_state'] = (state, verifier)
         return verifier
 


Test patch:
diff --git a/allauth/account/tests.py b/allauth/account/tests.py
--- a/allauth/account/tests.py
+++ b/allauth/account/tests.py
@@ -1351,3 +1351,39 @@ def test_user_email_domain_unicode_collision_nonexistent(self):
         data = {'email': 'mike@ıxample.org'}
         form = ResetPasswordForm(data)
         self.assertFalse(form.is_valid())
+
+
+class RequestAjaxTests(TestCase):
+
+    def _send_post_request(self, **kwargs):
+        return self.client.post(
+            reverse('account_signup'), {
+                'username': 'johndoe',
+                'email': 'john@example.org',
+                'email2': 'john@example.org',
+                'password1': 'johndoe',
+                'password2': 'johndoe'
+            },
+            **kwargs
+        )
+
+    def test_no_ajax_header(self):
+        resp = self._send_post_request()
+        self.assertEqual(302, resp.status_code)
+        self.assertRedirects(
+            resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
+        )
+
+    def test_ajax_header_x_requested_with(self):
+        resp = self._send_post_request(
+            HTTP_X_REQUESTED_WITH='XMLHttpRequest'
+        )
+        self.assertEqual(200, resp.status_code)
+        self.assertEqual(settings.LOGIN_REDIRECT_URL, resp.json()['location'])
+
+    def test_ajax_header_http_accept(self):
+        resp = self._send_post_request(
+            HTTP_ACCEPT='application/json'
+        )
+        self.assertEqual(200, resp.status_code)
+        self.assertEqual(settings.LOGIN_REDIRECT_URL, resp.json()['location'])