Search code examples
pythondjangodjango-rest-frameworkdjango-allauthdjango-rest-auth

Why does django-allauth method seem to fail to make Reddit api call correctly?


I'm setting up social authentication through Reddit for an application using django-rest-auth and django-allauth. My problem is that django-allauth returns a 429 error from Reddit when I attempt to retrieve the access token using the django-rest-auth endpoint. However, when I try to call the the Reddit api directly, using everything outlined in the Reddit api documentation, I am able to do it successfully. I'd like to be able to make this call through django-rest-auth so I can benefit from the way it integrates with Django.

I have already quadruple-checked every setting outlined in the django-rest-auth documentation, including the usual culprits for Reddit returning a 429 error: redirect_uri and the User-Agent value in settings.py . I've even used a packet sniffer to intercept the HTTP request, although that didn't work out because it was encrypted, of course.

Here are the rest-auth urls:

path('rest-auth/',include('rest_auth.urls')),
path('rest-auth/registration/',include('rest_auth.registration.urls')),
path('rest-auth/reddit/', views.RedditLogin.as_view(),name='reddit_login'),
            ]

Here's the relevant view in views.py:


#imports for social authentication
from allauth.socialaccount.providers.reddit.views import RedditAdapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from rest_auth.registration.views import SocialLoginView

class RedditLogin(SocialLoginView):
    adapter_class = RedditAdapter
    callback_url = 'http://localhost:8080/register'
    client_class = OAuth2Client

Here are relevant settings in settings.py:

SOCIALACCOUNT_PROVIDERS = {
    'reddit': {
        'AUTH_PARAMS': {'duration':'permanent'},
        'SCOPE': [ 'identity','submit'],
        'USER_AGENT': 'web:applicationnamehere:v1.0 (by /u/myusername)',

        }

}

Here are the results of getting the access token using django-allauth and django-rest-auth with the /rest-auth/reddit/ endpoint:

Traceback:

File "/usr/local/lib/python3.5/site-packages/django/core/handlers/exception.py" in inner
  34.             response = get_response(request)

File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  126.                 response = self.process_exception_by_middleware(e, request)

File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py" in _get_response
  124.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/usr/local/lib/python3.5/site-packages/django/views/decorators/csrf.py" in wrapped_view
  54.         return view_func(*args, **kwargs)

File "/usr/local/lib/python3.5/site-packages/django/views/generic/base.py" in view
  68.             return self.dispatch(request, *args, **kwargs)

File "/usr/local/lib/python3.5/site-packages/django/utils/decorators.py" in _wrapper
  45.         return bound_method(*args, **kwargs)

File "/usr/local/lib/python3.5/site-packages/django/views/decorators/debug.py" in sensitive_post_parameters_wrapper
  76.             return view(request, *args, **kwargs)

File "/usr/local/lib/python3.5/site-packages/rest_auth/views.py" in dispatch
  49.         return super(LoginView, self).dispatch(*args, **kwargs)

File "/usr/local/lib/python3.5/site-packages/rest_framework/views.py" in dispatch
  483.             response = self.handle_exception(exc)

File "/usr/local/lib/python3.5/site-packages/rest_framework/views.py" in handle_exception
  443.             self.raise_uncaught_exception(exc)

File "/usr/local/lib/python3.5/site-packages/rest_framework/views.py" in dispatch
  480.             response = handler(request, *args, **kwargs)

File "/usr/local/lib/python3.5/site-packages/rest_auth/views.py" in post
  93.         self.serializer.is_valid(raise_exception=True)

File "/usr/local/lib/python3.5/site-packages/rest_framework/serializers.py" in is_valid
  236.                 self._validated_data = self.run_validation(self.initial_data)

File "/usr/local/lib/python3.5/site-packages/rest_framework/serializers.py" in run_validation
  437.             value = self.validate(value)

File "/usr/local/lib/python3.5/site-packages/rest_auth/registration/serializers.py" in validate
  112.             token = client.get_access_token(code)

File "/usr/local/lib/python3.5/site-packages/allauth/socialaccount/providers/oauth2/client.py" in get_access_token
  85.                               % resp.content)

Exception Type: OAuth2Error at /api/v1/rest-auth/reddit/
Exception Value: Error retrieving access token: b'{"message": "Too Many Requests", "error": 429}'

I expect the 'get_access_token' method that is defined in django-allauth's 'OAuth2Client' class(see here) to return the token from Reddit, instead of a rate limiting error from Reddit.

After all my work to make sure that my settings are correct and reproduce an api call to reddit manually with the same data(which was successful), the only thing left I can think of is that django-allauth is forming the api request in a way that Reddit rejects. How can I troubleshoot the way an external library is forming a POST request? Perhaps I could just overwrite the 'get_access_token' method? Or am I just totally missing something?


Solution

  • The problem that I encountered here can be resolved by troubleshooting the OAuth2Client.get_access_token method in django-allauth. That method can be troubleshooted using either monkey patching or using python's debugger. I ended up using monkey patching to override the get_access_token method views.py:

    #imports for social authentication
    from allauth.socialaccount.providers.reddit.views import RedditAdapter
    from allauth.socialaccount.providers.oauth2.client import OAuth2Client
    from rest_auth.registration.views import SocialLoginView
    
    class RedditLogin(SocialLoginView):
        adapter_class = RedditAdapter
        callback_url = 'http://localhost:8080/register'
        OAuth2Client.get_access_token = custom_get_token
        client_class = OAuth2Client
    

    Using python's logging revealed that the headers and body of the request that django was sending to reddit were incorrect. The main issue seemed to be that the incorrect user-agent header was being used. Reddit requires a very specific user agent. My solution was to overwrite get_access_token method like so:

    def custom_get_token(self, code):
    
        # The following code uses the 'requests' library retrieve the token directly.
        data = {
            'redirect_uri': self.callback_url,
            'grant_type': 'authorization_code',
            'code': code}
        # This code should generate the basicauth object that can be passed to the requests parameters.
        auth = requests.auth.HTTPBasicAuth(
            self.consumer_key,
            self.consumer_secret
        )
        # The User-Agent header has to be overridden in order for things to work, which wasn't happening before...
        headers = {
            'User-Agent': 'web:myapplication:v0.0 (by /u/reddituser)'
        }
    
        self._strip_empty_keys(data)
        url = 'https://www.reddit.com/api/v1/access_token' # This is also self.access_token_url
        access_token_method = 'POST' # I set this just to make sure
    
        resp = requests.request(
            access_token_method,
            url,
            data=data,
            headers=headers,
            auth=auth
        )
    
        access_token = None
    
        if resp.status_code in [200, 201]:
            # Weibo sends json via 'text/plain;charset=UTF-8'
            if (resp.headers['content-type'].split(
                    ';')[0] == 'application/json' or resp.text[:2] == '{"'):
                access_token = resp.json()
            else:
                access_token = dict(parse_qsl(resp.text))
        if not access_token or 'access_token' not in access_token:
            raise OAuth2Error('Error retrieving access token: %s'
                              % resp.content)
        return access_token
    

    Note that this solution is specifically designed for using django-allauth with Reddit. This method may have to be adjusted for other social providers.