diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index 4f99c18..2d47848 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -83,13 +83,22 @@ class SocialLoginSerializer(serializers.Serializer): # Case 2: We received the authorization code elif attrs.get('code'): - self.callback_url = getattr(view, 'callback_url', None) - self.client_class = getattr(view, 'client_class', None) + + # allow client to potentially pass in a value for `callback_url` + self.callback_url = attrs.get('callback_url') + + # or if that is not provided, fall back on an attribute + # on the view + if not self.callback_url: + self.callback_url = getattr(view, 'callback_url', None) if not self.callback_url: raise serializers.ValidationError( _("Define callback_url in view") ) + + self.client_class = getattr(view, 'client_class', None) + if not self.client_class: raise serializers.ValidationError( _("Define client_class in view") diff --git a/rest_auth/tests/mixins.py b/rest_auth/tests/mixins.py index 30b3d58..5e311cf 100644 --- a/rest_auth/tests/mixins.py +++ b/rest_auth/tests/mixins.py @@ -87,6 +87,7 @@ class TestsMixin(object): self.user_url = reverse('rest_user_details') self.verify_email_url = reverse('rest_verify_email') self.fb_login_url = reverse('fb_login') + self.fb_login_auth_code_url = reverse('fb_login_auth_code') self.tw_login_url = reverse('tw_login') self.tw_login_no_view_url = reverse('tw_login_no_view') self.tw_login_no_adapter_url = reverse('tw_login_no_adapter') diff --git a/rest_auth/tests/test_social.py b/rest_auth/tests/test_social.py index 830e631..622ba0e 100644 --- a/rest_auth/tests/test_social.py +++ b/rest_auth/tests/test_social.py @@ -1,5 +1,7 @@ import json +from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter + from django.test import TestCase from django.contrib.auth import get_user_model from django.test.utils import override_settings @@ -19,6 +21,22 @@ from rest_framework import status from .mixins import TestsMixin +facebook_resp_body = { + "id": "123123123123", + "first_name": "John", + "gender": "male", + "last_name": "Smith", + "link": "https://www.facebook.com/john.smith", + "locale": "en_US", + "name": "John Smith", + "timezone": 2, + "updated_time": "2014-08-13T10:14:38+0000", + "username": "john.smith", + "verified": True +} + + + @override_settings(ROOT_URLCONF="tests.urls") class TestSocialAuth(TestsMixin, TestCase): @@ -73,25 +91,10 @@ class TestSocialAuth(TestsMixin, TestCase): @responses.activate def test_social_auth(self): - # fake response for facebook call - resp_body = { - "id": "123123123123", - "first_name": "John", - "gender": "male", - "last_name": "Smith", - "link": "https://www.facebook.com/john.smith", - "locale": "en_US", - "name": "John Smith", - "timezone": 2, - "updated_time": "2014-08-13T10:14:38+0000", - "username": "john.smith", - "verified": True - } - responses.add( responses.GET, self.graph_api_url, - body=json.dumps(resp_body), + body=json.dumps(facebook_resp_body), status=200, content_type='application/json' ) @@ -110,6 +113,75 @@ class TestSocialAuth(TestsMixin, TestCase): self.assertIn('key', self.response.json.keys()) self.assertEqual(get_user_model().objects.all().count(), users_count + 1) + @responses.activate + def test_social_auth_with_code(self): + # fake response exchanging the authorization code for an access token + responses.add( + responses.Response( + responses.POST, + FacebookOAuth2Adapter.access_token_url, + json={'access_token': 'donkeyface'}, + status=200, + ) + ) + # fake response for facebook call + responses.add( + responses.GET, + self.graph_api_url, + body=json.dumps(facebook_resp_body), + status=200, + content_type='application/json' + ) + users_count = get_user_model().objects.all().count() + payload = { + 'code': 'abc123', + } + + self.post(self.fb_login_auth_code_url, data=payload, status_code=200) + self.assertIn('key', self.response.json.keys()) + self.assertEqual(get_user_model().objects.all().count(), users_count + 1) + + # make sure that second request will not create a new user + self.post(self.fb_login_auth_code_url, data=payload, status_code=200) + self.assertIn('key', self.response.json.keys()) + self.assertEqual(get_user_model().objects.all().count(), users_count + 1) + + @responses.activate + def test_social_auth_with_code_and_invalid_callback(self): + payload = { + 'code': 'abc123', + 'callback_url': 'Iamnotaurl', + } + response = self.post(self.fb_login_auth_code_url, data=payload, status_code=400) + self.assertIn('callback_url', response.json) + + @responses.activate + def test_social_auth_with_code_and_valid_callback(self): + # fake response exchanging the authorization code for an access token + responses.add( + responses.Response( + responses.POST, + FacebookOAuth2Adapter.access_token_url, + json={'access_token': 'donkeyface'}, + status=200, + ) + ) + # fake response for facebook call + responses.add( + responses.GET, + self.graph_api_url, + body=json.dumps(facebook_resp_body), + status=200, + content_type='application/json' + ) + payload = { + 'code': 'abc123', + 'callback_url': 'https://another.1231sda1D123.url.com/', + } + self.post(self.fb_login_auth_code_url, data=payload, status_code=200) + # test that the custom callback URL has been used in the code exchange + self.assertIn('1231sda1D123', responses.calls[0].request.body) + def _twitter_social_auth(self): # fake response for twitter call resp_body = { diff --git a/rest_auth/tests/urls.py b/rest_auth/tests/urls.py index 401f23a..f0dbcd0 100644 --- a/rest_auth/tests/urls.py +++ b/rest_auth/tests/urls.py @@ -3,8 +3,9 @@ from django.views.generic import TemplateView from . import django_urls from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter +from allauth.socialaccount.providers.oauth2.client import OAuth2Client from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter - +from rest_framework import serializers from rest_framework.decorators import api_view from rest_auth.urls import urlpatterns @@ -12,6 +13,7 @@ from rest_auth.registration.views import ( SocialLoginView, SocialConnectView, SocialAccountListView, SocialAccountDisconnectView ) +from rest_auth.registration.serializers import SocialLoginSerializer from rest_auth.social_serializers import ( TwitterLoginSerializer, TwitterConnectSerializer ) @@ -21,6 +23,19 @@ class FacebookLogin(SocialLoginView): adapter_class = FacebookOAuth2Adapter +class SocialLoginWithClientCallbackSerializer(SocialLoginSerializer): + # An example of a serializer allowing the client to supply their + # own `callback_url` value + callback_url = serializers.URLField(required=False) + + +class FacebookAuthCodeLogin(SocialLoginView): + adapter_class = FacebookOAuth2Adapter + serializer_class = SocialLoginWithClientCallbackSerializer + callback_url = 'https://some.test.url.com' + client_class = OAuth2Client + + class TwitterLogin(SocialLoginView): adapter_class = TwitterOAuthAdapter serializer_class = TwitterLoginSerializer @@ -60,6 +75,7 @@ urlpatterns += [ url(r'^account-confirm-email/(?P[-:\w]+)/$', TemplateView.as_view(), name='account_confirm_email'), url(r'^social-login/facebook/$', FacebookLogin.as_view(), name='fb_login'), + url(r'^social-login/facebook-authcode/$', FacebookAuthCodeLogin.as_view(), name='fb_login_auth_code'), url(r'^social-login/twitter/$', TwitterLogin.as_view(), name='tw_login'), url(r'^social-login/twitter-no-view/$', twitter_login_view, name='tw_login_no_view'), url(r'^social-login/twitter-no-adapter/$', TwitterLoginNoAdapter.as_view(), name='tw_login_no_adapter'),