Implement connect social accounts functionality

Add serializers and views for connecting accounts with minimal
specialization of the existing django-rest-auth interfaces.

Also add viewset which enables listing social account infmration
via the REST API for social authentication driven client applications.

Resolves #347 in GitHub.
This commit is contained in:
Aleksi Häkli 2017-12-02 16:00:16 +02:00 committed by Aleksi Häkli
parent a892ca3fa5
commit 8a4afe746c
No known key found for this signature in database
GPG Key ID: 3E7146964D726BBE
4 changed files with 212 additions and 86 deletions

View File

@ -111,11 +111,15 @@ Facebook
.. code-block:: python .. code-block:: python
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter
from rest_auth.registration.views import SocialLoginView from rest_auth.registration.views import SocialLoginView, SocialConnectView
class FacebookLogin(SocialLoginView): class FacebookLogin(SocialLoginView):
adapter_class = FacebookOAuth2Adapter adapter_class = FacebookOAuth2Adapter
# Add a connect view if you want to allow connecting existing accounts
class FacebookConnect(SocialConnectView):
adapter_class = FacebookOAuth2Adapter
4. Create url for FacebookLogin view: 4. Create url for FacebookLogin view:
.. code-block:: python .. code-block:: python
@ -123,6 +127,7 @@ Facebook
urlpatterns += [ urlpatterns += [
..., ...,
url(r'^rest-auth/facebook/$', FacebookLogin.as_view(), name='fb_login') url(r'^rest-auth/facebook/$', FacebookLogin.as_view(), name='fb_login')
url(r'^rest-auth/facebook/connect/$', FacebookConnect.as_view(), name='fb_connect')
] ]
@ -136,13 +141,19 @@ If you are using Twitter for your social authentication, it is a bit different s
.. code-block:: python .. code-block:: python
from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter
from rest_auth.views import LoginView from rest_auth.registration.views import SocialLoginView
from rest_auth.social_serializers import TwitterLoginSerializer from rest_auth.social_serializers import TwitterLoginSerializer, TwitterConnectSerializer
class TwitterLogin(LoginView): class TwitterLogin(SocialLoginView):
serializer_class = TwitterLoginSerializer serializer_class = TwitterLoginSerializer
adapter_class = TwitterOAuthAdapter adapter_class = TwitterOAuthAdapter
# Add a connect view if you want to allow connecting existing accounts
class TwitterConnect(SocialConnectView):
serializer_class = TwitterConnectSerializer
adapter_class = TwitterOAuthAdapter
4. Create url for TwitterLogin view: 4. Create url for TwitterLogin view:
.. code-block:: python .. code-block:: python
@ -150,6 +161,7 @@ If you are using Twitter for your social authentication, it is a bit different s
urlpatterns += [ urlpatterns += [
..., ...,
url(r'^rest-auth/twitter/$', TwitterLogin.as_view(), name='twitter_login') url(r'^rest-auth/twitter/$', TwitterLogin.as_view(), name='twitter_login')
url(r'^rest-auth/twitter/connect/$', TwitterConnect.as_view(), name='twitter_login')
] ]
.. note:: Starting from v0.21.0, django-allauth has dropped support for context processors. Check out http://django-allauth.readthedocs.org/en/latest/changelog.html#from-0-21-0 for more details. .. note:: Starting from v0.21.0, django-allauth has dropped support for context processors. Check out http://django-allauth.readthedocs.org/en/latest/changelog.html#from-0-21-0 for more details.

View File

@ -14,10 +14,6 @@ except ImportError:
from rest_framework import serializers from rest_framework import serializers
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
# Import is needed only if we are using social login, in which
# case the allauth.socialaccount will be declared
if 'allauth.socialaccount' in settings.INSTALLED_APPS:
from allauth.socialaccount.helpers import complete_social_login
class SocialLoginSerializer(serializers.Serializer): class SocialLoginSerializer(serializers.Serializer):
@ -186,3 +182,44 @@ class RegisterSerializer(serializers.Serializer):
class VerifyEmailSerializer(serializers.Serializer): class VerifyEmailSerializer(serializers.Serializer):
key = serializers.CharField() key = serializers.CharField()
# Import is needed only if we are using social login, in which
# case the allauth.socialaccount will be declared
if 'allauth.socialaccount' in settings.INSTALLED_APPS:
from allauth.socialaccount.helpers import complete_social_login
from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.providers.base import AuthProcess
class SocialAccountSerializer(serializers.ModelSerializer):
"""
serialize allauth SocialAccounts for use with a REST API
"""
class Meta:
model = SocialAccount
fields = (
'id',
'provider',
'uid',
'last_login',
'date_joined',
'extra_data',
)
class SocialConnectMixin(object):
def get_social_login(self, *args, **kwargs):
"""
set the social login process state to connect rather than login
Refer to the implementation of get_social_login in base class and to the
allauth.socialaccount.helpers module complete_social_login function.
"""
social_login = super(SocialConnectMixin, self).get_social_login(*args, **kwargs)
social_login.state['process'] = AuthProcess.CONNECT
return social_login
class SocialConnectSerializer(SocialConnectMixin, SocialLoginSerializer):
pass

View File

@ -5,11 +5,13 @@ from django.views.decorators.debug import sensitive_post_parameters
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import AllowAny from rest_framework.permissions import (AllowAny,
IsAuthenticated)
from rest_framework.decorators import detail_route
from rest_framework.viewsets import GenericViewSet
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
from rest_framework import status from rest_framework import status
from allauth.account.adapter import get_adapter
from allauth.account.views import ConfirmEmailView from allauth.account.views import ConfirmEmailView
from allauth.account.utils import complete_signup from allauth.account.utils import complete_signup
from allauth.account import app_settings as allauth_settings from allauth.account import app_settings as allauth_settings
@ -19,7 +21,8 @@ from rest_auth.app_settings import (TokenSerializer,
create_token) create_token)
from rest_auth.models import TokenModel from rest_auth.models import TokenModel
from rest_auth.registration.serializers import (SocialLoginSerializer, from rest_auth.registration.serializers import (SocialLoginSerializer,
VerifyEmailSerializer) VerifyEmailSerializer,
SocialConnectSerializer)
from rest_auth.utils import jwt_encode from rest_auth.utils import jwt_encode
from rest_auth.views import LoginView from rest_auth.views import LoginView
from .app_settings import RegisterSerializer, register_permission_classes from .app_settings import RegisterSerializer, register_permission_classes
@ -91,31 +94,98 @@ class VerifyEmailView(APIView, ConfirmEmailView):
return Response({'detail': _('ok')}, status=status.HTTP_200_OK) return Response({'detail': _('ok')}, status=status.HTTP_200_OK)
class SocialLoginView(LoginView): if 'allauth.socialaccount' in settings.INSTALLED_APPS:
""" from allauth.socialaccount import signals
class used for social authentications from allauth.socialaccount.models import SocialAccount
example usage for facebook with access_token from allauth.socialaccount.adapter import get_adapter
-------------
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter
class FacebookLogin(SocialLoginView): from rest_auth.registration.serializers import SocialAccountSerializer
adapter_class = FacebookOAuth2Adapter
-------------
example usage for facebook with code
------------- class SocialLoginView(LoginView):
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter """
from allauth.socialaccount.providers.oauth2.client import OAuth2Client class used for social authentications
example usage for facebook with access_token
-------------
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter
class FacebookLogin(SocialLoginView): class FacebookLogin(SocialLoginView):
adapter_class = FacebookOAuth2Adapter adapter_class = FacebookOAuth2Adapter
client_class = OAuth2Client -------------
callback_url = 'localhost:8000'
-------------
"""
serializer_class = SocialLoginSerializer example usage for facebook with code
def process_login(self): -------------
get_adapter(self.request).login(self.request, self.user) from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
class FacebookLogin(SocialLoginView):
adapter_class = FacebookOAuth2Adapter
client_class = OAuth2Client
callback_url = 'localhost:8000'
-------------
"""
serializer_class = SocialLoginSerializer
def process_login(self):
get_adapter(self.request).login(self.request, self.user)
class SocialConnectView(SocialLoginView):
"""
class used for social account linking
example usage for facebook with access_token
-------------
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter
class FacebookConnect(SocialConnectView):
adapter_class = FacebookOAuth2Adapter
-------------
"""
serializer_class = SocialConnectSerializer
permission_classes = (IsAuthenticated,)
class SocialAccountViewSet(GenericViewSet):
"""
allauth SocialAccount REST API read and disconnect views for logged in users
Refer to the django-allauth package implementation of the models and
account handling logic for more details, this viewset emulates the allauth web UI.
"""
serializer_class = SocialAccountSerializer
permission_classes = (IsAuthenticated,)
queryset = SocialAccount.objects.none()
def get_queryset(self):
return SocialAccount.objects.filter(user=self.request.user)
def list(self, request):
"""
list SocialAccounts for the currently logged in user
"""
return Response(self.get_serializer(self.get_queryset(), many=True).data)
@detail_route(methods=['POST'])
def disconnect(self, request, pk):
"""
disconnect SocialAccount from remote service for the currently logged in user
"""
accounts = self.get_queryset()
account = accounts.get(pk=pk)
get_adapter(self.request).validate_disconnect(account, accounts)
account.delete()
signals.social_account_removed.send(
sender=SocialAccount,
request=self.request,
socialaccount=account
)
return Response(self.get_serializer(account).data)

View File

@ -1,6 +1,7 @@
from django.conf import settings from django.conf import settings
from django.http import HttpRequest from django.http import HttpRequest
from rest_framework import serializers from rest_framework import serializers
# Import is needed only if we are using social login, in which # Import is needed only if we are using social login, in which
# case the allauth.socialaccount will be declared # case the allauth.socialaccount will be declared
if 'allauth.socialaccount' in settings.INSTALLED_APPS: if 'allauth.socialaccount' in settings.INSTALLED_APPS:
@ -8,68 +9,74 @@ if 'allauth.socialaccount' in settings.INSTALLED_APPS:
from allauth.socialaccount.models import SocialToken from allauth.socialaccount.models import SocialToken
from allauth.socialaccount.providers.oauth.client import OAuthError from allauth.socialaccount.providers.oauth.client import OAuthError
from rest_auth.registration.serializers import SocialConnectMixin
class TwitterLoginSerializer(serializers.Serializer):
access_token = serializers.CharField()
token_secret = serializers.CharField()
def _get_request(self): class TwitterLoginSerializer(serializers.Serializer):
request = self.context.get('request') access_token = serializers.CharField()
if not isinstance(request, HttpRequest): token_secret = serializers.CharField()
request = request._request
return request
def get_social_login(self, adapter, app, token, response): def _get_request(self):
""" request = self.context.get('request')
:param adapter: allauth.socialaccount Adapter subclass. if not isinstance(request, HttpRequest):
Usually OAuthAdapter or Auth2Adapter request = request._request
:param app: `allauth.socialaccount.SocialApp` instance return request
:param token: `allauth.socialaccount.SocialToken` instance
:param response: Provider's response for OAuth1. Not used in the
:returns: A populated instance of the
`allauth.socialaccount.SocialLoginView` instance
"""
request = self._get_request()
social_login = adapter.complete_login(request, app, token,
response=response)
social_login.token = token
return social_login
def validate(self, attrs): def get_social_login(self, adapter, app, token, response):
view = self.context.get('view') """
request = self._get_request() :param adapter: allauth.socialaccount Adapter subclass.
Usually OAuthAdapter or Auth2Adapter
:param app: `allauth.socialaccount.SocialApp` instance
:param token: `allauth.socialaccount.SocialToken` instance
:param response: Provider's response for OAuth1. Not used in the
:returns: A populated instance of the
`allauth.socialaccount.SocialLoginView` instance
"""
request = self._get_request()
social_login = adapter.complete_login(request, app, token,
response=response)
social_login.token = token
return social_login
if not view: def validate(self, attrs):
raise serializers.ValidationError( view = self.context.get('view')
"View is not defined, pass it as a context variable" request = self._get_request()
)
adapter_class = getattr(view, 'adapter_class', None) if not view:
if not adapter_class: raise serializers.ValidationError(
raise serializers.ValidationError("Define adapter_class in view") "View is not defined, pass it as a context variable"
)
adapter = adapter_class(request) adapter_class = getattr(view, 'adapter_class', None)
app = adapter.get_provider().get_app(request) if not adapter_class:
raise serializers.ValidationError("Define adapter_class in view")
access_token = attrs.get('access_token') adapter = adapter_class(request)
token_secret = attrs.get('token_secret') app = adapter.get_provider().get_app(request)
request.session['oauth_api.twitter.com_access_token'] = { access_token = attrs.get('access_token')
'oauth_token': access_token, token_secret = attrs.get('token_secret')
'oauth_token_secret': token_secret,
}
token = SocialToken(token=access_token, token_secret=token_secret)
token.app = app
try: request.session['oauth_api.twitter.com_access_token'] = {
login = self.get_social_login(adapter, app, token, access_token) 'oauth_token': access_token,
complete_social_login(request, login) 'oauth_token_secret': token_secret,
except OAuthError as e: }
raise serializers.ValidationError(str(e)) token = SocialToken(token=access_token, token_secret=token_secret)
token.app = app
if not login.is_existing: try:
login.lookup() login = self.get_social_login(adapter, app, token, access_token)
login.save(request, connect=True) complete_social_login(request, login)
attrs['user'] = login.account.user except OAuthError as e:
raise serializers.ValidationError(str(e))
return attrs if not login.is_existing:
login.lookup()
login.save(request, connect=True)
attrs['user'] = login.account.user
return attrs
class TwitterConnectSerializer(SocialConnectMixin, TwitterLoginSerializer):
pass