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?
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.