├── tests ├── __init__.py ├── utils.py └── test_social.py ├── example_project ├── users │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── admin.py │ ├── models.py │ ├── social_pipeline.py │ └── views.py ├── config │ ├── __init__.py │ ├── settings_test.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── static │ └── anonymous.png ├── manage.py └── templates │ ├── base.html │ ├── home_token.html │ ├── home_session.html │ └── home_jwt.html ├── setup.cfg ├── pytest.ini ├── test.sh ├── rest_social_auth ├── __init__.py ├── urls_session.py ├── urls_jwt.py ├── urls_token.py ├── strategy.py ├── serializers.py └── views.py ├── requirements_test.txt ├── requirements.txt ├── MANIFEST.in ├── .gitignore ├── .travis.yml ├── tox.ini ├── LICENSE ├── RELEASE_NOTES.md ├── setup.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example_project/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example_project/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example_project/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = config.settings_test 3 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | PYTHONPATH=`pwd`:`pwd`/example_project:$PYTHONPATH py.test "$@" 2 | -------------------------------------------------------------------------------- /rest_social_auth/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'st4lk' 2 | __version__ = '0.4.2' 3 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest-django==2.8.0 2 | httpretty==0.8.10 3 | unittest2==1.0.1 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=1.6,<1.10 2 | djangorestframework<4.0 3 | python-social-auth>=0.2.9 4 | -------------------------------------------------------------------------------- /example_project/users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example_project/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include RELEASE_NOTES.md 3 | include README.rst 4 | include requirements.txt 5 | -------------------------------------------------------------------------------- /example_project/static/anonymous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-rest-social-auth/master/example_project/static/anonymous.png -------------------------------------------------------------------------------- /example_project/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db import models 3 | 4 | 5 | class CustomUser(AbstractUser): 6 | social_thumb = models.URLField(null=True, blank=True) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.TODO 3 | *.sublime-project 4 | *.sublime-workspace 5 | .tox/ 6 | *.egg-info 7 | dist/ 8 | README.rst 9 | RELEASE_NOTES.rst 10 | generate_rst.py 11 | build/ 12 | .coverage 13 | htmlcov/ 14 | *.sqlite3 15 | *.backup 16 | .cache/ 17 | -------------------------------------------------------------------------------- /example_project/config/settings_test.py: -------------------------------------------------------------------------------- 1 | " Settings for tests. " 2 | from .settings import * 3 | 4 | # Databases 5 | DATABASES = { 6 | 'default': { 7 | 'ENGINE': 'django.db.backends.sqlite3', 8 | 'NAME': ':memory:', 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /rest_social_auth/urls_session.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = ( 7 | url(r'^social/session/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 8 | views.SocialSessionAuthView.as_view(), 9 | name='login_social_session'),) 10 | -------------------------------------------------------------------------------- /example_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example_project/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /rest_social_auth/urls_jwt.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = ( 7 | # returns jwt + user_data 8 | url(r'^social/jwt_user/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 9 | views.SocialJWTUserAuthView.as_view(), 10 | name='login_social_jwt_user'), 11 | 12 | # returns jwt only 13 | url(r'^social/jwt/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 14 | views.SocialJWTOnlyAuthView.as_view(), 15 | name='login_social_jwt'),) 16 | -------------------------------------------------------------------------------- /rest_social_auth/urls_token.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = ( 7 | # returns token + user_data 8 | url(r'^social/token_user/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 9 | views.SocialTokenUserAuthView.as_view(), 10 | name='login_social_token_user'), 11 | 12 | # returns token only 13 | url(r'^social/token/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 14 | views.SocialTokenOnlyAuthView.as_view(), 15 | name='login_social_token'),) 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.7 5 | 6 | env: 7 | - TOXENV=py34-django17 8 | - TOXENV=py34-django18 9 | - TOXENV=py34-django19 10 | - TOXENV=py35-django18 11 | - TOXENV=py35-django19 12 | - TOXENV=py27-django17 13 | - TOXENV=py27-django18 14 | - TOXENV=py27-django19 15 | 16 | branches: 17 | only: 18 | - master 19 | 20 | install: pip install --quiet tox 21 | 22 | # command to run tests 23 | script: tox 24 | 25 | after_script: 26 | - if [ $TOXENV == "py27-django19" ]; then 27 | pip install --quiet coveralls; 28 | coveralls; 29 | fi 30 | 31 | addons: 32 | apt: 33 | sources: 34 | - deadsnakes 35 | packages: 36 | - python3.5 37 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{27,34}-django{17,18,19}, 4 | py{35}-django{18,19} 5 | 6 | [testenv] 7 | setenv = 8 | PYTHONPATH = {toxinidir}/example_project 9 | LC_ALL = en_US.utf-8 10 | basepython = 11 | py27: python2.7 12 | py34: python3.4 13 | py35: python3.5 14 | deps = 15 | djangorestframework<4.0 16 | python-social-auth==0.2.9 17 | django17: Django>=1.7,<1.8 18 | django18: Django>=1.8,<1.9 19 | django19: Django>=1.9,<1.10 20 | py27-django19: djangorestframework-jwt 21 | py27-django19: coverage 22 | -rrequirements_test.txt 23 | commands = 24 | py.test 25 | 26 | [testenv:py27-django19] 27 | commands = 28 | coverage run --source=rest_social_auth -m py.test 29 | coverage report 30 | -------------------------------------------------------------------------------- /rest_social_auth/strategy.py: -------------------------------------------------------------------------------- 1 | from social.strategies.django_strategy import DjangoStrategy 2 | 3 | 4 | class DRFStrategy(DjangoStrategy): 5 | 6 | def __init__(self, storage, request=None, tpl=None): 7 | self.request = request 8 | self.session = {} 9 | 10 | if request: 11 | try: 12 | self.session = request.session 13 | except AttributeError: 14 | # in case of token auth session can be disabled at all 15 | pass 16 | 17 | super(DjangoStrategy, self).__init__(storage, tpl) 18 | 19 | def request_data(self, merge=True): 20 | if self.request: 21 | return getattr(self.request, 'auth_data', {}) 22 | else: 23 | return {} 24 | -------------------------------------------------------------------------------- /example_project/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Django-rest-framework and OAuth example

8 | 13 | 14 | {% block content %} 15 | 16 | {% endblock content %} 17 | 18 | {% block scripts %} 19 | 20 | 21 | 22 | {% endblock scripts %} 23 | 24 | 25 | -------------------------------------------------------------------------------- /example_project/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | 4 | from users import views 5 | 6 | urlpatterns = [ 7 | url(r'^$', views.HomeSessionView.as_view(), name='home'), 8 | url(r'^session/$', views.HomeSessionView.as_view(), name='home_session'), 9 | url(r'^token/$', views.HomeTokenView.as_view(), name='home_token'), 10 | url(r'^jwt/$', views.HomeJWTView.as_view(), name='home_jwt'), 11 | 12 | url(r'^api/login/', include('rest_social_auth.urls_session')), 13 | url(r'^api/login/', include('rest_social_auth.urls_token')), 14 | url(r'^api/login/', include('rest_social_auth.urls_jwt')), 15 | 16 | url(r'^api/logout/session/$', views.LogoutSessionView.as_view(), name='logout_session'), 17 | url(r'^api/user/session/', views.UserSessionDetailView.as_view(), name="current_user_session"), 18 | url(r'^api/user/token/', views.UserTokenDetailView.as_view(), name="current_user_token"), 19 | url(r'^api/user/jwt/', views.UserJWTDetailView.as_view(), name="current_user_jwt"), 20 | url(r'^admin/', include(admin.site.urls)), 21 | ] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 st4lk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | rest_social_auth release notes 2 | ============================== 3 | 4 | v0.4.2 5 | ------ 6 | - Remove django.conf.urls.patterns from code 7 | - Exclude modifing immutable data 8 | - refactor tests 9 | - minor typo fixes 10 | 11 | Issues: #11, #17, #14 12 | 13 | v0.4.1 14 | ------ 15 | - Fix requirements.txt: allow django==1.9 16 | 17 | v0.4.0 18 | ------ 19 | - Add [JSON Web Tokens](http://jwt.io/) using [djangorestframework-jwt](https://github.com/GetBlimp/django-rest-framework-jwt) 20 | - Add Python 3.5 and Django 1.9 support 21 | 22 | Issues: #6 23 | 24 | v0.3.1 25 | ------ 26 | - Explicitly set token authentication for token views 27 | 28 | v0.3.0 29 | ------ 30 | - Add support for Oauth1 31 | - Add ability to override request parsing 32 | - Allow to specify provider in url 33 | - Drop Python 2.6 and Django 1.6 support 34 | 35 | Issues: #2, #3, #5 36 | 37 | v0.2.0 38 | ------ 39 | - Get domain from HTTP Origin 40 | - Add example of Google OAuth2.0 41 | - Add manual redirect uri (front-end can specify it) 42 | - Use GenericAPIView instead of APIView 43 | - Main serializer is output serializer, not input 44 | - Update docs 45 | - Minor code fixes 46 | 47 | v0.1.0 48 | ------ 49 | 50 | First version in pypi 51 | -------------------------------------------------------------------------------- /example_project/users/social_pipeline.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def auto_logout(*args, **kwargs): 5 | """Do not compare current user with new one""" 6 | return {'user': None} 7 | 8 | 9 | def save_avatar(strategy, details, user=None, *args, **kwargs): 10 | """Get user avatar from social provider.""" 11 | if user: 12 | backend_name = kwargs['backend'].__class__.__name__.lower() 13 | response = kwargs.get('response', {}) 14 | social_thumb = None 15 | if 'facebook' in backend_name: 16 | if 'id' in response: 17 | social_thumb = ("http://graph.facebook.com/{0}/picture?" 18 | "type=normal").format(response['id']) 19 | elif 'twitter' in backend_name and response.get('profile_image_url'): 20 | social_thumb = response['profile_image_url'] 21 | else: 22 | social_thumb = "http://www.gravatar.com/avatar/" 23 | social_thumb += hashlib.md5(user.email.lower().encode('utf8')).hexdigest() 24 | social_thumb += "?size=100" 25 | if social_thumb and user.social_thumb != social_thumb: 26 | user.social_thumb = social_thumb 27 | strategy.storage.user.changed(user) 28 | -------------------------------------------------------------------------------- /example_project/users/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | from django.contrib.auth import logout, get_user_model 3 | from django.utils.decorators import method_decorator 4 | from django.views.decorators.csrf import ensure_csrf_cookie 5 | from rest_framework import generics, status 6 | from rest_framework.permissions import IsAuthenticated 7 | from rest_framework.views import APIView 8 | from rest_framework.response import Response 9 | from rest_framework.authentication import SessionAuthentication, TokenAuthentication 10 | from rest_social_auth.serializers import UserSerializer 11 | from rest_social_auth.views import JWTAuthMixin 12 | 13 | 14 | class HomeSessionView(TemplateView): 15 | template_name = 'home_session.html' 16 | 17 | @method_decorator(ensure_csrf_cookie) 18 | def get(self, request, *args, **kwargs): 19 | return super(HomeSessionView, self).get(request, *args, **kwargs) 20 | 21 | 22 | class HomeTokenView(TemplateView): 23 | template_name = 'home_token.html' 24 | 25 | 26 | class HomeJWTView(TemplateView): 27 | template_name = 'home_jwt.html' 28 | 29 | 30 | class LogoutSessionView(APIView): 31 | 32 | def post(self, request, *args, **kwargs): 33 | logout(request) 34 | return Response(status=status.HTTP_204_NO_CONTENT) 35 | 36 | 37 | class BaseDeatilView(generics.RetrieveAPIView): 38 | permission_classes = IsAuthenticated, 39 | serializer_class = UserSerializer 40 | model = get_user_model() 41 | 42 | def get_object(self, queryset=None): 43 | return self.request.user 44 | 45 | 46 | class UserSessionDetailView(BaseDeatilView): 47 | authentication_classes = (SessionAuthentication, ) 48 | 49 | 50 | class UserTokenDetailView(BaseDeatilView): 51 | authentication_classes = (TokenAuthentication, ) 52 | 53 | 54 | class UserJWTDetailView(JWTAuthMixin, BaseDeatilView): 55 | pass 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, find_packages 4 | from rest_social_auth import __author__, __version__ 5 | 6 | 7 | def __read(fname): 8 | try: 9 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 10 | except IOError: 11 | return '' 12 | 13 | if sys.argv[-1] == 'publish': 14 | os.system('pandoc --from=markdown --to=rst --output=README.rst README.md') 15 | os.system('pandoc --from=markdown --to=rst --output=RELEASE_NOTES.rst RELEASE_NOTES.md') 16 | os.system('python setup.py sdist upload') 17 | os.system('python setup.py bdist_wheel upload') 18 | sys.exit() 19 | 20 | if sys.argv[-1] == 'generate_rst': 21 | os.system('pandoc --from=markdown --to=rst --output=README.rst README.md') 22 | os.system('pandoc --from=markdown --to=rst --output=RELEASE_NOTES.rst RELEASE_NOTES.md') 23 | sys.exit() 24 | 25 | if sys.argv[-1] == 'tag': 26 | print("Tagging the version on github:") 27 | os.system("git tag -a v%s -m 'version %s'" % (__version__, __version__)) 28 | os.system("git push --tags") 29 | sys.exit() 30 | 31 | 32 | install_requires = __read('requirements.txt').split() 33 | 34 | setup( 35 | name='rest_social_auth', 36 | author=__author__, 37 | author_email='alexevseev@gmail.com', 38 | version=__version__, 39 | description='Django rest framework resources for social auth', 40 | long_description=__read('README.rst') + '\n\n' + __read('RELEASE_NOTES.rst'), 41 | platforms=('Any'), 42 | packages=find_packages(), 43 | install_requires=install_requires, 44 | keywords='django social auth rest login signin signup oauth'.split(), 45 | include_package_data=True, 46 | license='BSD License', 47 | package_dir={'rest_social_auth': 'rest_social_auth'}, 48 | url='https://github.com/st4lk/django-rest-social-auth', 49 | classifiers=[ 50 | 'Environment :: Web Environment', 51 | 'Framework :: Django', 52 | 'Intended Audience :: Developers', 53 | 'License :: OSI Approved :: BSD License', 54 | 'Operating System :: OS Independent', 55 | 'Programming Language :: Python', 56 | 'Topic :: Utilities', 57 | ], 58 | ) 59 | -------------------------------------------------------------------------------- /rest_social_auth/serializers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import warnings 3 | from rest_framework import serializers 4 | from rest_framework.authtoken.models import Token 5 | from django.contrib.auth import get_user_model 6 | 7 | 8 | l = logging.getLogger(__name__) 9 | 10 | 11 | class OAuth2InputSerializer(serializers.Serializer): 12 | 13 | provider = serializers.CharField(required=False) 14 | code = serializers.CharField() 15 | redirect_uri = serializers.CharField(required=False) 16 | 17 | 18 | class OAuth1InputSerializer(serializers.Serializer): 19 | 20 | provider = serializers.CharField(required=False) 21 | oauth_token = serializers.CharField() 22 | oauth_verifier = serializers.CharField() 23 | 24 | 25 | class UserSerializer(serializers.ModelSerializer): 26 | 27 | class Meta: 28 | model = get_user_model() 29 | exclude = ('is_staff', 'is_active', 'date_joined', 'password', 30 | 'last_login', 'user_permissions', 'groups', 'is_superuser',) 31 | 32 | 33 | class TokenSerializer(serializers.Serializer): 34 | 35 | token = serializers.SerializerMethodField() 36 | 37 | def get_token(self, obj): 38 | token, created = Token.objects.get_or_create(user=obj) 39 | return token.key 40 | 41 | 42 | class UserTokenSerializer(TokenSerializer, UserSerializer): 43 | pass 44 | 45 | 46 | class JWTSerializer(TokenSerializer): 47 | 48 | def get_token(self, obj): 49 | try: 50 | from rest_framework_jwt.settings import api_settings 51 | except ImportError: 52 | warnings.warn('djangorestframework-jwt must be installed for JWT authentication', 53 | ImportWarning) 54 | raise 55 | 56 | jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER 57 | jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER 58 | 59 | payload = jwt_payload_handler(self.get_jwt_payload(obj)) 60 | token = jwt_encode_handler(payload) 61 | 62 | return token 63 | 64 | def get_jwt_payload(self, obj): 65 | """ 66 | Define here, what data shall be encoded in JWT. 67 | By default, entire object will be encoded. 68 | """ 69 | return obj 70 | 71 | 72 | class UserJWTSerializer(JWTSerializer, UserSerializer): 73 | pass 74 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.test import modify_settings 3 | except ImportError: 4 | from django.test.utils import override_settings, settings, six 5 | 6 | class modify_settings(override_settings): 7 | """ 8 | Like override_settings, but makes it possible to append, prepend or remove 9 | items instead of redefining the entire list. 10 | """ 11 | def __init__(self, *args, **kwargs): 12 | if args: 13 | # Hack used when instantiating from SimpleTestCase._pre_setup. 14 | assert not kwargs 15 | self.operations = args[0] 16 | else: 17 | assert not args 18 | self.operations = list(kwargs.items()) 19 | 20 | def save_options(self, test_func): 21 | if test_func._modified_settings is None: 22 | test_func._modified_settings = self.operations 23 | else: 24 | # Duplicate list to prevent subclasses from altering their parent. 25 | test_func._modified_settings = list( 26 | test_func._modified_settings) + self.operations 27 | 28 | def enable(self): 29 | self.options = {} 30 | for name, operations in self.operations: 31 | try: 32 | # When called from SimpleTestCase._pre_setup, values may be 33 | # overridden several times; cumulate changes. 34 | value = self.options[name] 35 | except KeyError: 36 | value = list(getattr(settings, name, [])) 37 | for action, items in operations.items(): 38 | # items my be a single value or an iterable. 39 | if isinstance(items, six.string_types): 40 | items = [items] 41 | if action == 'append': 42 | value = value + [item for item in items if item not in value] 43 | elif action == 'prepend': 44 | value = [item for item in items if item not in value] + value 45 | elif action == 'remove': 46 | value = [item for item in value if item not in items] 47 | else: 48 | raise ValueError("Unsupported action: %s" % action) 49 | self.options[name] = value 50 | super(modify_settings, self).enable() 51 | -------------------------------------------------------------------------------- /example_project/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.utils.timezone 6 | import django.core.validators 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('auth', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='CustomUser', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 20 | ('password', models.CharField(max_length=128, verbose_name='password')), 21 | ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login', null=True, blank=True)), 22 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 23 | ('username', models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, max_length=30, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')])), 24 | ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)), 25 | ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)), 26 | ('email', models.EmailField(max_length=75, verbose_name='email address', blank=True)), 27 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 28 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 29 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 30 | ('social_thumb', models.URLField(null=True, blank=True)), 31 | ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')), 32 | ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')), 33 | ], 34 | options={ 35 | 'abstract': False, 36 | 'verbose_name': 'user', 37 | 'verbose_name_plural': 'users', 38 | }, 39 | bases=(models.Model,), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /example_project/templates/home_token.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

rest_framework.authentication.TokenAuthentication

9 |
10 |
11 |
12 |
13 |
14 |
15 | (*) Currently in case of OAuth 1.0 you still need to have session enabled, as python-social-auth will store some data in session between requests. Maybe in future versions of rest-social-auth this will be solved. 16 |
17 |
18 |

Token user data

19 |
20 | {% verbatim %} 21 | 22 | {% endverbatim %} 23 |
24 |
25 | First name: 26 | 27 |
28 |
29 | Last name: 30 | 31 |
32 |
33 | Email: 34 | 35 |
36 |
37 |
38 |
39 | 40 | {% endblock content %} 41 | 42 | {% block scripts %} 43 | {{ block.super }} 44 | 106 | {% endblock scripts %} 107 | -------------------------------------------------------------------------------- /example_project/templates/home_session.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

rest_framework.authentication.SessionAuthentication

9 |
10 |
11 |
12 |
13 |
14 |
15 |

Session user data

16 |
17 | {% verbatim %} 18 | 19 | {% endverbatim %} 20 |
21 |
22 | First name: 23 | 24 |
25 |
26 | Last name: 27 | 28 |
29 |
30 | Email: 31 | 32 |
33 |
34 |
35 |
36 | {% endblock content %} 37 | 38 | {% block scripts %} 39 | {{ block.super }} 40 | 113 | {% endblock scripts %} 114 | -------------------------------------------------------------------------------- /example_project/config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example_project project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '5i830%k0$u*+@0290eq&lb!c%h3cxknj01ygyck-@el-5__y4y' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'rest_framework', 42 | 'rest_framework.authtoken', 43 | 'social.apps.django_app.default', 44 | 'rest_social_auth', 45 | 46 | 'users', 47 | ) 48 | 49 | MIDDLEWARE_CLASSES = ( 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | # 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', # django 1.6 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | # 'django.middleware.security.SecurityMiddleware', # django 1.7 58 | ) 59 | 60 | 61 | ROOT_URLCONF = 'config.urls' 62 | 63 | 64 | TEMPLATE_DIRS = ( 65 | os.path.join(BASE_DIR, 'templates'), 66 | ) 67 | 68 | WSGI_APPLICATION = 'config.wsgi.application' 69 | 70 | 71 | # Database 72 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 73 | 74 | DATABASES = { 75 | 'default': { 76 | 'ENGINE': 'django.db.backends.sqlite3', 77 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 78 | } 79 | } 80 | 81 | 82 | # Internationalization 83 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 84 | 85 | LANGUAGE_CODE = 'en-us' 86 | 87 | TIME_ZONE = 'UTC' 88 | 89 | USE_I18N = True 90 | 91 | USE_L10N = True 92 | 93 | USE_TZ = True 94 | 95 | 96 | # Static files (CSS, JavaScript, Images) 97 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 98 | 99 | STATIC_URL = '/static/' 100 | 101 | STATICFILES_DIRS = ( 102 | os.path.join(BASE_DIR, 'static'), 103 | ) 104 | 105 | AUTH_USER_MODEL = 'users.CustomUser' 106 | 107 | # DRF settings 108 | 109 | # social auth settings 110 | # valid redirect domain for all apps: http://restsocialexample.com:8000/ 111 | SOCIAL_AUTH_FACEBOOK_KEY = '295137440610143' 112 | SOCIAL_AUTH_FACEBOOK_SECRET = '4b4aef291799a7b9aaf016689339e97f' 113 | SOCIAL_AUTH_FACEBOOK_SCOPE = ['email', ] 114 | 115 | SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '976099811367-ihbmg1pfnniln9qgfacleiu41bhl3fqn.apps.googleusercontent.com' 116 | SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'JaiLLvY1BK97TSy5_xcGWDhp' 117 | SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['email', ] 118 | 119 | SOCIAL_AUTH_TWITTER_KEY = 'gCrpEpNxpWkAE6Cul98OzAWTk' 120 | SOCIAL_AUTH_TWITTER_SECRET = '7SYSRpYY4amW5kiNXUAxUDdWS7G3nHytRIGHbDVTByzBfsqDJl' 121 | 122 | 123 | AUTHENTICATION_BACKENDS = ( 124 | 'social.backends.facebook.FacebookOAuth2', 125 | 'social.backends.google.GoogleOAuth2', 126 | 'social.backends.twitter.TwitterOAuth', # OAuth1.0 127 | 'django.contrib.auth.backends.ModelBackend', 128 | ) 129 | 130 | SOCIAL_AUTH_PIPELINE = ( 131 | 'users.social_pipeline.auto_logout', # custom action 132 | 'social.pipeline.social_auth.social_details', 133 | 'social.pipeline.social_auth.social_uid', 134 | 'social.pipeline.social_auth.auth_allowed', 135 | 'social.pipeline.social_auth.social_user', 136 | 'social.pipeline.user.get_username', 137 | 'social.pipeline.user.create_user', 138 | 'social.pipeline.social_auth.associate_user', 139 | 'social.pipeline.social_auth.load_extra_data', 140 | 'social.pipeline.user.user_details', 141 | 'users.social_pipeline.save_avatar', # custom action 142 | ) 143 | 144 | LOGGING = { 145 | 'version': 1, 146 | 'disable_existing_loggers': False, 147 | 'handlers': { 148 | 'console': { 149 | 'level': 'DEBUG', 150 | 'class': 'logging.StreamHandler', 151 | }, 152 | }, 153 | 'loggers': { 154 | 'rest_social_auth': { 155 | 'handlers': ['console', ], 156 | 'level': "DEBUG", 157 | }, 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /example_project/templates/home_jwt.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block content %} 5 |

Note! djangorestframework-jwt must be installed for this method

6 |
7 |
8 |
9 |

rest_framework_jwt.authentication.JSONWebTokenAuthentication

10 |
11 |
12 |
13 |
14 |
15 |
16 | (*) Currently in case of OAuth 1.0 you still need to have session enabled, as python-social-auth will store some data in session between requests. Maybe in future versions of rest-social-auth this will be solved. 17 |
18 |
19 |

JWT user data

20 |
21 | {% verbatim %} 22 | 23 | {% endverbatim %} 24 |
25 |
26 | First name: 27 | 28 |
29 |
30 | Last name: 31 | 32 |
33 |
34 | Email: 35 | 36 |
37 |

Raw JWT payload

38 |
39 |
{% verbatim %}{{ ctrl.jwtPayload | json }}{% endverbatim %}
40 |
41 |
42 |
43 |
44 | 45 | {% endblock content %} 46 | 47 | {% block scripts %} 48 | {{ block.super }} 49 | 121 | {% endblock scripts %} 122 | -------------------------------------------------------------------------------- /rest_social_auth/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import warnings 3 | try: 4 | from urlparse import urlparse 5 | except ImportError: 6 | # python 3 7 | from urllib.parse import urlparse 8 | from django.conf import settings 9 | from django.views.decorators.cache import never_cache 10 | from django.utils.decorators import method_decorator 11 | from django.views.decorators.csrf import csrf_protect 12 | from django.utils.encoding import iri_to_uri 13 | from django.utils.six.moves.urllib.parse import urljoin 14 | from social.apps.django_app.utils import psa, STORAGE 15 | from social.backends.oauth import BaseOAuth1 16 | from social.strategies.utils import get_strategy 17 | from social.utils import user_is_authenticated, parse_qs 18 | from social.apps.django_app.views import _do_login as social_auth_login 19 | from social.exceptions import AuthException 20 | from rest_framework.generics import GenericAPIView 21 | from rest_framework.response import Response 22 | from rest_framework import status 23 | from rest_framework.authentication import TokenAuthentication 24 | from requests.exceptions import HTTPError 25 | 26 | from .serializers import (OAuth2InputSerializer, OAuth1InputSerializer, UserSerializer, 27 | TokenSerializer, UserTokenSerializer, JWTSerializer, UserJWTSerializer) 28 | 29 | 30 | l = logging.getLogger(__name__) 31 | 32 | 33 | REDIRECT_URI = getattr(settings, 'REST_SOCIAL_OAUTH_REDIRECT_URI', '/') 34 | DOMAIN_FROM_ORIGIN = getattr(settings, 'REST_SOCIAL_DOMAIN_FROM_ORIGIN', True) 35 | 36 | 37 | def load_strategy(request=None): 38 | return get_strategy('rest_social_auth.strategy.DRFStrategy', STORAGE, request) 39 | 40 | 41 | @psa(REDIRECT_URI, load_strategy=load_strategy) 42 | def decorate_request(request, backend): 43 | pass 44 | 45 | 46 | class BaseSocialAuthView(GenericAPIView): 47 | """ 48 | View will login or signin (create) the user from social oauth2.0 provider. 49 | 50 | **Input** (default serializer_class_in): 51 | 52 | { 53 | "provider": "facebook", 54 | "code": "AQBPBBTjbdnehj51" 55 | } 56 | 57 | + optional 58 | 59 | "redirect_uri": "/relative/or/absolute/redirect/uri" 60 | 61 | **Output**: 62 | 63 | user data in serializer_class format 64 | """ 65 | 66 | oauth1_serializer_class_in = OAuth1InputSerializer 67 | oauth2_serializer_class_in = OAuth2InputSerializer 68 | serializer_class = None 69 | 70 | def oauth_v1(self): 71 | assert hasattr(self.request, 'backend'), 'Don\'t call this method before decorate_request' 72 | return isinstance(self.request.backend, BaseOAuth1) 73 | 74 | def get_serializer_class_in(self): 75 | return self.oauth1_serializer_class_in if self.oauth_v1() else self.oauth2_serializer_class_in 76 | 77 | def get_serializer_in(self, *args, **kwargs): 78 | """ 79 | Return the serializer instance that should be used for validating and 80 | deserializing input, and for serializing output. 81 | """ 82 | serializer_class = self.get_serializer_class_in() 83 | kwargs['context'] = self.get_serializer_context() 84 | return serializer_class(*args, **kwargs) 85 | 86 | def get_serializer_in_data(self): 87 | """ 88 | Compile the incoming data into a form fit for the serializer_in class. 89 | :return: Data for serializer in the form of a dictionary with 'provider' and 'code' keys. 90 | """ 91 | return self.request.data.copy() 92 | 93 | @method_decorator(never_cache) 94 | def post(self, request, *args, **kwargs): 95 | input_data = self.get_serializer_in_data() 96 | provider_name = self.get_provider_name(input_data) 97 | if not provider_name: 98 | return self.respond_error("Provider is not specified") 99 | self.set_input_data(request, input_data) 100 | decorate_request(request, provider_name) 101 | serializer_in = self.get_serializer_in(data=input_data) 102 | if self.oauth_v1() and request.backend.OAUTH_TOKEN_PARAMETER_NAME not in input_data: 103 | # oauth1 first stage (1st is get request_token, 2nd is get access_token) 104 | request_token = parse_qs(request.backend.set_unauthorized_token()) 105 | return Response(request_token) 106 | serializer_in.is_valid(raise_exception=True) 107 | try: 108 | user = self.get_object() 109 | except (AuthException, HTTPError) as e: 110 | l.exception(e) 111 | return self.respond_error(e) 112 | resp_data = self.get_serializer(instance=user) 113 | self.do_login(request.backend, user) 114 | return Response(resp_data.data) 115 | 116 | def get_object(self): 117 | user = self.request.user 118 | manual_redirect_uri = self.request.auth_data.pop('redirect_uri', None) 119 | manual_redirect_uri = self.get_redirect_uri(manual_redirect_uri) 120 | if manual_redirect_uri: 121 | self.request.backend.redirect_uri = manual_redirect_uri 122 | elif DOMAIN_FROM_ORIGIN: 123 | origin = self.request.strategy.request.META.get('HTTP_ORIGIN') 124 | if origin: 125 | relative_path = urlparse(self.request.backend.redirect_uri).path 126 | url = urlparse(origin) 127 | origin_scheme_host = "%s://%s" % (url.scheme, url.netloc) 128 | location = urljoin(origin_scheme_host, relative_path) 129 | self.request.backend.redirect_uri = iri_to_uri(location) 130 | is_authenticated = user_is_authenticated(user) 131 | user = is_authenticated and user or None 132 | # skip checking state by setting following params to False 133 | # it is responsibility of front-end to check state 134 | # TODO: maybe create an additional resource, where front-end will 135 | # store the state before making a call to oauth provider 136 | # so server can save it in session and consequently check it before 137 | # sending request to acquire access token. 138 | # In case of token authentication we need a way to store an anonymous 139 | # session to do it. 140 | self.request.backend.REDIRECT_STATE = False 141 | self.request.backend.STATE_PARAMETER = False 142 | user = self.request.backend.complete(user=user) 143 | return user 144 | 145 | def do_login(self, backend, user): 146 | """ 147 | Do login action here. 148 | For example in case of session authentication store the session in 149 | cookies. 150 | """ 151 | 152 | def set_input_data(self, request, auth_data): 153 | """ 154 | auth_data will be used used as request_data in strategy 155 | """ 156 | request.auth_data = auth_data 157 | 158 | def get_redirect_uri(self, manual_redirect_uri): 159 | if not manual_redirect_uri: 160 | manual_redirect_uri = getattr(settings, 161 | 'REST_SOCIAL_OAUTH_ABSOLUTE_REDIRECT_URI', None) 162 | return manual_redirect_uri 163 | 164 | def get_provider_name(self, input_data): 165 | if 'provider' in input_data: 166 | return input_data['provider'] 167 | else: 168 | return self.kwargs.get('provider') 169 | 170 | def respond_error(self, error): 171 | return Response(status=status.HTTP_400_BAD_REQUEST) 172 | 173 | 174 | class SocialSessionAuthView(BaseSocialAuthView): 175 | serializer_class = UserSerializer 176 | 177 | def do_login(self, backend, user): 178 | social_auth_login(backend, user, user.social_user) 179 | 180 | @method_decorator(csrf_protect) # just to be sure csrf is not disabled 181 | def post(self, request, *args, **kwargs): 182 | return super(SocialSessionAuthView, self).post(request, *args, **kwargs) 183 | 184 | 185 | class SocialTokenOnlyAuthView(BaseSocialAuthView): 186 | serializer_class = TokenSerializer 187 | authentication_classes = (TokenAuthentication, ) 188 | 189 | 190 | class SocialTokenUserAuthView(BaseSocialAuthView): 191 | serializer_class = UserTokenSerializer 192 | authentication_classes = (TokenAuthentication, ) 193 | 194 | 195 | class JWTAuthMixin(object): 196 | def get_authenticators(self): 197 | try: 198 | from rest_framework_jwt.authentication import JSONWebTokenAuthentication 199 | except ImportError: 200 | warnings.warn('djangorestframework-jwt must be installed for JWT authentication', 201 | ImportWarning) 202 | raise 203 | 204 | return [JSONWebTokenAuthentication()] 205 | 206 | 207 | class SocialJWTOnlyAuthView(JWTAuthMixin, BaseSocialAuthView): 208 | serializer_class = JWTSerializer 209 | 210 | 211 | class SocialJWTUserAuthView(JWTAuthMixin, BaseSocialAuthView): 212 | serializer_class = UserJWTSerializer 213 | -------------------------------------------------------------------------------- /tests/test_social.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | try: 4 | from urlparse import parse_qsl, urlparse 5 | except ImportError: 6 | # python 3 7 | from urllib.parse import parse_qsl, urlparse 8 | 9 | from django.core.urlresolvers import reverse 10 | from django.contrib.auth import get_user_model 11 | from django.test.utils import override_settings 12 | from rest_framework.test import APITestCase 13 | from rest_framework.authtoken.models import Token 14 | from httpretty import HTTPretty 15 | from social.utils import module_member, parse_qs 16 | from social.backends.utils import load_backends 17 | from social.tests.backends.test_facebook import FacebookOAuth2Test 18 | from social.tests.backends.test_twitter import TwitterOAuth1Test 19 | 20 | from rest_social_auth.views import load_strategy 21 | from .utils import modify_settings 22 | 23 | 24 | l = logging.getLogger(__name__) 25 | 26 | 27 | # don't run third party tests 28 | for attr in (attr for attr in dir(FacebookOAuth2Test) if attr.startswith('test_')): 29 | delattr(FacebookOAuth2Test, attr) 30 | for attr in (attr for attr in dir(TwitterOAuth1Test) if attr.startswith('test_')): 31 | delattr(TwitterOAuth1Test, attr) 32 | 33 | 34 | session_modify_settings = dict( 35 | INSTALLED_APPS={ 36 | 'remove': [ 37 | 'rest_framework.authtoken', 38 | ] 39 | }, 40 | ) 41 | 42 | 43 | token_modify_settings = dict( 44 | INSTALLED_APPS={ 45 | 'remove': [ 46 | 'django.contrib.sessions' 47 | ] 48 | }, 49 | MIDDLEWARE_CLASSES={ 50 | 'remove': [ 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | ], 55 | } 56 | ) 57 | 58 | 59 | jwt_modify_settings = dict( 60 | INSTALLED_APPS={ 61 | 'remove': [ 62 | 'django.contrib.sessions', 63 | 'rest_framework.authtoken', 64 | ] 65 | }, 66 | MIDDLEWARE_CLASSES={ 67 | 'remove': [ 68 | 'django.contrib.sessions.middleware.SessionMiddleware', 69 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 70 | 'django.contrib.messages.middleware.MessageMiddleware', 71 | ], 72 | } 73 | ) 74 | 75 | class RestSocialMixin(object): 76 | def setUp(self): 77 | HTTPretty.enable() 78 | Backend = module_member(self.backend_path) 79 | self.strategy = load_strategy() 80 | self.backend = Backend(self.strategy, redirect_uri=self.complete_url) 81 | self.name = self.backend.name.upper().replace('-', '_') 82 | self.complete_url = self.strategy.build_absolute_uri( 83 | self.raw_complete_url.format(self.backend.name) 84 | ) 85 | backends = (self.backend_path, ) 86 | load_backends(backends, force_load=True) 87 | 88 | user_data_body = json.loads(self.user_data_body) 89 | self.email = 'example@mail.com' 90 | user_data_body['email'] = self.email 91 | self.user_data_body = json.dumps(user_data_body) 92 | 93 | self.do_rest_login() 94 | 95 | def tearDown(self): 96 | HTTPretty.disable() 97 | HTTPretty.reset() 98 | self.backend = None 99 | self.strategy = None 100 | self.name = None 101 | self.complete_url = None 102 | 103 | 104 | class BaseFacebookAPITestCase(RestSocialMixin, FacebookOAuth2Test): 105 | 106 | def do_rest_login(self): 107 | start_url = self.backend.start().url 108 | self.auth_handlers(start_url) 109 | 110 | 111 | class BaseTiwtterApiTestCase(RestSocialMixin, TwitterOAuth1Test): 112 | 113 | def do_rest_login(self): 114 | self.request_token_handler() 115 | start_url = self.backend.start().url 116 | self.auth_handlers(start_url) 117 | 118 | 119 | class TestSocialAuth1(APITestCase, BaseTiwtterApiTestCase): 120 | 121 | @modify_settings(**session_modify_settings) 122 | def test_login_social_oauth1_session(self): 123 | resp = self.client.post(reverse('login_social_session'), 124 | data={'provider': 'twitter'}) 125 | self.assertEqual(resp.status_code, 200) 126 | self.assertEqual(resp.data, parse_qs(self.request_token_body)) 127 | resp = self.client.post(reverse('login_social_session'), data={ 128 | 'provider': 'twitter', 129 | 'oauth_token': 'foobar', 130 | 'oauth_verifier': 'overifier' 131 | }) 132 | self.assertEqual(resp.status_code, 200) 133 | 134 | def test_login_social_oauth1_token(self): 135 | """ 136 | Currently oauth1 works only if session is enabled. 137 | Probably it is possible to make it work without session, but 138 | it will be needed to change the logic in python-social-auth. 139 | """ 140 | resp = self.client.post(reverse('login_social_token_user'), 141 | data={'provider': 'twitter'}) 142 | self.assertEqual(resp.status_code, 200) 143 | self.assertEqual(resp.data, parse_qs(self.request_token_body)) 144 | resp = self.client.post(reverse('login_social_token_user'), data={ 145 | 'provider': 'twitter', 146 | 'oauth_token': 'foobar', 147 | 'oauth_verifier': 'overifier' 148 | }) 149 | self.assertEqual(resp.status_code, 200) 150 | 151 | @modify_settings(INSTALLED_APPS={'remove': ['rest_framework.authtoken', ]}) 152 | def test_login_social_oauth1_jwt(self): 153 | """ 154 | Currently oauth1 works only if session is enabled. 155 | Probably it is possible to make it work without session, but 156 | it will be needed to change the logic in python-social-auth. 157 | """ 158 | try: 159 | import rest_framework_jwt 160 | except ImportError: 161 | return 162 | assert rest_framework_jwt is not None 163 | resp = self.client.post(reverse('login_social_jwt_user'), 164 | data={'provider': 'twitter'}) 165 | self.assertEqual(resp.status_code, 200) 166 | self.assertEqual(resp.data, parse_qs(self.request_token_body)) 167 | resp = self.client.post(reverse('login_social_token_user'), data={ 168 | 'provider': 'twitter', 169 | 'oauth_token': 'foobar', 170 | 'oauth_verifier': 'overifier' 171 | }) 172 | self.assertEqual(resp.status_code, 200) 173 | 174 | 175 | class TestSocialAuth2(APITestCase, BaseFacebookAPITestCase): 176 | 177 | @modify_settings(**session_modify_settings) 178 | def _check_login_social_session(self, url, data): 179 | resp = self.client.post(url, data) 180 | self.assertEqual(resp.status_code, 200) 181 | self.assertEqual(resp.data['email'], self.email) 182 | # check cookies are set 183 | self.assertTrue('sessionid' in resp.cookies) 184 | # check user is created 185 | self.assertTrue( 186 | get_user_model().objects.filter(email=self.email).exists()) 187 | 188 | @modify_settings(**token_modify_settings) 189 | def _check_login_social_token_user(self, url, data): 190 | resp = self.client.post(url, data) 191 | self.assertEqual(resp.status_code, 200) 192 | self.assertEqual(resp.data['email'], self.email) 193 | # check token exists 194 | token = Token.objects.get(key=resp.data['token']) 195 | # check user is created 196 | self.assertEqual(token.user.email, self.email) 197 | 198 | @modify_settings(**token_modify_settings) 199 | def _check_login_social_token_only(self, url, data): 200 | resp = self.client.post(url, data) 201 | self.assertEqual(resp.status_code, 200) 202 | # check token exists 203 | token = Token.objects.get(key=resp.data['token']) 204 | # check user is created 205 | self.assertEqual(token.user.email, self.email) 206 | 207 | @modify_settings(**jwt_modify_settings) 208 | def _check_login_social_jwt_only(self, url, data): 209 | try: 210 | from rest_framework_jwt.settings import api_settings 211 | except ImportError: 212 | return 213 | jwt_decode_handler = api_settings.JWT_DECODE_HANDLER 214 | resp = self.client.post(url, data) 215 | self.assertEqual(resp.status_code, 200) 216 | # check token valid 217 | jwt_data = jwt_decode_handler(resp.data['token']) 218 | self.assertEqual(jwt_data['email'], self.email) 219 | 220 | @modify_settings(**jwt_modify_settings) 221 | def _check_login_social_jwt_user(self, url, data): 222 | try: 223 | from rest_framework_jwt.settings import api_settings 224 | except ImportError: 225 | return 226 | jwt_decode_handler = api_settings.JWT_DECODE_HANDLER 227 | resp = self.client.post(url, data) 228 | self.assertEqual(resp.status_code, 200) 229 | self.assertEqual(resp.data['email'], self.email) 230 | # check token valid 231 | jwt_data = jwt_decode_handler(resp.data['token']) 232 | self.assertEqual(jwt_data['email'], self.email) 233 | 234 | def test_login_social_session(self): 235 | self._check_login_social_session( 236 | reverse('login_social_session'), 237 | {'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 238 | 239 | def test_login_social_session_provider_in_url(self): 240 | self._check_login_social_session( 241 | reverse('login_social_session', kwargs={'provider': 'facebook'}), 242 | {'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 243 | 244 | def test_login_social_token_user(self): 245 | self._check_login_social_token_user( 246 | reverse('login_social_token_user'), 247 | {'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 248 | 249 | def test_login_social_token_user_provider_in_url(self): 250 | self._check_login_social_token_user( 251 | reverse('login_social_token_user', kwargs={'provider': 'facebook'}), 252 | {'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 253 | 254 | def test_login_social_token_only(self): 255 | self._check_login_social_token_only( 256 | reverse('login_social_token'), 257 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 258 | 259 | def test_login_social_token_only_provider_in_url(self): 260 | self._check_login_social_token_only( 261 | reverse('login_social_token', kwargs={'provider': 'facebook'}), 262 | data={'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 263 | 264 | def test_login_social_jwt_only(self): 265 | self._check_login_social_jwt_only( 266 | reverse('login_social_jwt'), 267 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 268 | 269 | def test_login_social_jwt_only_provider_in_url(self): 270 | self._check_login_social_jwt_only( 271 | reverse('login_social_jwt', kwargs={'provider': 'facebook'}), 272 | data={'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 273 | 274 | def test_login_social_jwt_user(self): 275 | self._check_login_social_jwt_user( 276 | reverse('login_social_jwt_user'), 277 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 278 | 279 | def test_login_social_jwt_user_provider_in_url(self): 280 | self._check_login_social_jwt_user( 281 | reverse('login_social_jwt_user', kwargs={'provider': 'facebook'}), 282 | data={'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 283 | 284 | def test_no_provider_session(self): 285 | resp = self.client.post( 286 | reverse('login_social_session'), 287 | {'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 288 | self.assertEqual(resp.status_code, 400) 289 | 290 | def test_unknown_provider_session(self): 291 | resp = self.client.post( 292 | reverse('login_social_session', kwargs={'provider': 'unknown'}), 293 | {'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 294 | self.assertEqual(resp.status_code, 404) 295 | 296 | def test_login_social_http_origin(self): 297 | resp = self.client.post(reverse('login_social_session'), 298 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 299 | HTTP_ORIGIN="http://frontend.com") 300 | self.assertEqual(resp.status_code, 200) 301 | url_params = dict(parse_qsl(urlparse(HTTPretty.latest_requests[0].path).query)) 302 | self.assertEqual(url_params['redirect_uri'], "http://frontend.com/") 303 | 304 | @override_settings(REST_SOCIAL_OAUTH_ABSOLUTE_REDIRECT_URI='http://myproject.com/') 305 | def test_login_absolute_redirect(self): 306 | resp = self.client.post(reverse('login_social_session'), 307 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 308 | self.assertEqual(resp.status_code, 200) 309 | url_params = dict(parse_qsl(urlparse(HTTPretty.latest_requests[0].path).query)) 310 | self.assertEqual('http://myproject.com/', url_params['redirect_uri']) 311 | 312 | @override_settings(REST_SOCIAL_OAUTH_ABSOLUTE_REDIRECT_URI='http://myproject.com/') 313 | def test_login_manual_redirect(self): 314 | resp = self.client.post(reverse('login_social_session'), 315 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw', 316 | 'redirect_uri': 'http://manualdomain.com/'}) 317 | self.assertEqual(resp.status_code, 200) 318 | url_params = dict(parse_qsl(urlparse(HTTPretty.latest_requests[0].path).query)) 319 | self.assertEqual('http://manualdomain.com/', url_params['redirect_uri']) 320 | 321 | 322 | class TestSocialAuth2Error(APITestCase, BaseFacebookAPITestCase): 323 | access_token_status = 400 324 | 325 | def test_login_oauth_provider_error(self): 326 | resp = self.client.post(reverse('login_social_session'), 327 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 328 | self.assertEqual(resp.status_code, 400) 329 | 330 | 331 | class TestSocialAuth2HTTPError(APITestCase, BaseFacebookAPITestCase): 332 | access_token_status = 401 333 | 334 | def test_login_oauth_provider_http_error(self): 335 | resp = self.client.post(reverse('login_social_session'), 336 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 337 | self.assertEqual(resp.status_code, 400) 338 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django REST social auth 2 | ======================= 3 | 4 | [![Build Status](https://travis-ci.org/st4lk/django-rest-social-auth.svg?branch=master)](https://travis-ci.org/st4lk/django-rest-social-auth) 5 | [![Coverage Status](https://coveralls.io/repos/st4lk/django-rest-social-auth/badge.svg?branch=master)](https://coveralls.io/r/st4lk/django-rest-social-auth?branch=master) 6 | [![Pypi version](https://img.shields.io/pypi/v/rest_social_auth.svg)](https://pypi.python.org/pypi/rest_social_auth) 7 | 8 | 9 | OAuth signin with django rest framework. 10 | 11 | 12 | Requirements 13 | ----------- 14 | 15 | - python (2.7, 3.4, 3.5) 16 | - django (1.7, 1.8, 1.9) 17 | - djangorestframework (>= 3.1) 18 | - python-social-auth (>=0.2.9) 19 | - [optional] djangorestframework-jwt (>=1.7.2) 20 | 21 | Release notes 22 | ------------- 23 | 24 | [Here](https://github.com/st4lk/django-rest-social-auth/blob/master/RELEASE_NOTES.md) 25 | 26 | 27 | Motivation 28 | ---------- 29 | 30 | To have a resource, that will do very simple thing: 31 | take the oauth code from social provider (for example facebook) 32 | and return the authenticated user. 33 | That's it. 34 | 35 | I can't find such util for [django rest framework](http://www.django-rest-framework.org/). 36 | There are packages (for example [django-rest-auth](https://github.com/Tivix/django-rest-auth)), that take access_token, not the code. 37 | Also, i've used to work with awesome library [python-social-auth](https://github.com/omab/python-social-auth), 38 | so it will be nice to use it again. In fact, most of the work is done by this package. 39 | Current util brings a little help to integrate django-rest-framework and python-social-auth. 40 | 41 | Quick start 42 | ----------- 43 | 44 | 1. Install this package to your python distribution: 45 | 46 | pip install rest-social-auth 47 | 48 | 2. Do the settings 49 | 50 | Install apps 51 | 52 | INSTALLED_APPS = ( 53 | ... 54 | 'rest_framework', 55 | 'rest_framework.authtoken', # only if you use token authentication 56 | 'social.apps.django_app.default', # python social auth 57 | 'rest_social_auth', # this package 58 | ) 59 | 60 | python-social-auth settings, look [documentation](http://psa.matiasaguirre.net/docs/configuration/django.html) for more details 61 | 62 | SOCIAL_AUTH_FACEBOOK_KEY = 'your app client id' 63 | SOCIAL_AUTH_FACEBOOK_SECRET = 'your app client secret' 64 | SOCIAL_AUTH_FACEBOOK_SCOPE = ['email', ] # optional 65 | SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {'locale': 'ru_RU'} # optional 66 | 67 | 68 | AUTHENTICATION_BACKENDS = ( 69 | 'social.backends.facebook.FacebookOAuth2', 70 | # and maybe some others ... 71 | 'django.contrib.auth.backends.ModelBackend', 72 | ) 73 | 74 | Also look [optional settings](#settings) avaliable. 75 | 76 | 3. Make sure everything is up do date 77 | 78 | python manage.py syncdb 79 | 80 | 81 | 4. Include rest social urls (choose at least one) 82 | 83 | 4.1 [session authentication](http://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication) 84 | 85 | url(r'^api/login/', include('rest_social_auth.urls_session')), 86 | 87 | 4.2 [token authentication](http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication) 88 | 89 | url(r'^api/login/', include('rest_social_auth.urls_token')), 90 | 91 | 4.3 [jwt authentication](http://getblimp.github.io/django-rest-framework-jwt/) 92 | 93 | url(r'^api/login/', include('rest_social_auth.urls_jwt')), 94 | 95 | 5. You are ready to login users 96 | 97 | Following examples are for OAuth 2.0. 98 | 99 | 5.1 session authentication 100 | 101 | - POST /api/login/social/session/ 102 | 103 | input: 104 | 105 | { 106 | "provider": "facebook", 107 | "code": "AQBPBBTjbdnehj51" 108 | } 109 | 110 | output: 111 | 112 | { 113 | "username": "Alex", 114 | "email": "user@email.com", 115 | // other user data 116 | } 117 | 118 | + session id in cookies 119 | 120 | 5.2 token authentication 121 | 122 | - POST /api/login/social/token/ 123 | 124 | input: 125 | 126 | { 127 | "provider": "facebook", 128 | "code": "AQBPBBTjbdnehj51" 129 | } 130 | 131 | output: 132 | 133 | { 134 | "token": "68ded41d89f6a28da050f882998b2ea1decebbe0" 135 | } 136 | 137 | - POST /api/login/social/token_user/ 138 | 139 | input: 140 | 141 | { 142 | "provider": "facebook", 143 | "code": "AQBPBBTjbdnehj51" 144 | } 145 | 146 | output: 147 | 148 | { 149 | "username": "Alex", 150 | "email": "user@email.com", 151 | // other user data 152 | "token": "68ded41d89f6a28da050f882998b2ea1decebbe0" 153 | } 154 | 155 | 5.3 jwt authentication 156 | 157 | - POST /api/login/social/jwt/ 158 | - POST /api/login/social/jwt_user/ 159 | 160 | Similar to token authentication, but token is JSON Web Token. 161 | 162 | See [JWT.io](http://jwt.io/) for details. 163 | 164 | To use it, [django-rest-framework-jwt](https://github.com/GetBlimp/django-rest-framework-jwt) must be installed. 165 | 166 | User model is taken from [`settings.AUTH_USER_MODEL`](https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model). 167 | 168 | At input there is also non-required field `redirect_uri`. 169 | If given, server will use this redirect uri in requests, instead of uri 170 | got from settings. 171 | This redirect_uri must be equal in front-end request and in back-end request. 172 | Back-end will not do any redirect in fact. 173 | 174 | It is also possible to specify provider in url, not in request body. 175 | Just append it to the url: 176 | 177 | POST /api/login/social/session/facebook/ 178 | 179 | Don't need to specify it in body now: 180 | 181 | { 182 | "code": "AQBPBBTjbdnehj51" 183 | } 184 | 185 | 186 | OAuth 2.0 workflow with rest-social-auth 187 | ----------------------------------------- 188 | 1. Front-end need to know following params for each social provider: 189 | - client_id _# only in case of OAuth 2.0, id of registered application on social service provider_ 190 | - redirect_uri _# to this url social provider will redirect with code_ 191 | - scope=your_scope _# for example email_ 192 | - response_type=code _# same for all oauth2.0 providers_ 193 | 194 | 2. Front-end redirect user to social authorize url with params from previous point. 195 | 196 | 3. User confirms. 197 | 198 | 4. Social provider redirects back to `redirect_uri` with param `code`. 199 | 200 | 5. Front-end now ready to login the user. To do it, send POST request with provider name and code: 201 | 202 | POST /api/login/social/session/ 203 | 204 | with data (form data or json) 205 | 206 | provider=facebook&code=AQBPBBTjbdnehj51 207 | 208 | Backend will either signin the user, either signup, either return error. 209 | 210 | Sometimes it is more suitable to specify provider in url, not in request body. 211 | It is possible, rest-social-auth will understand that. 212 | Following request is the same as above: 213 | 214 | POST /api/login/social/session/facebook/ 215 | 216 | with data (form data or json) 217 | 218 | code=AQBPBBTjbdnehj51 219 | 220 | 221 | OAuth 1.0a workflow with rest-social-auth 222 | ----------------------------------------- 223 | 1. Front-end needs to make a POST request to your backend with the provider name ONLY: 224 | 225 | POST /api/login/social/ 226 | 227 | with data (form data or json): 228 | 229 | provider=twitter 230 | 231 | Or specify provider in url, in that case data will be empty: 232 | 233 | POST /api/login/social/twitter 234 | 235 | 2. The backend will return a short-lived `oauth_token` request token in the response. This can be used by the front-end to perform authentication with the provider. 236 | 237 | 3. User confirms. In the case of Twitter, they will then return the following data to your front-end: 238 | 239 | { 240 | "redirect_state": "...bHrz2x0wy43", 241 | "oauth_token" : "...AAAAAAAhD5u", 242 | "oauth_verifier": "...wDBdTR7CYdR" 243 | } 244 | 245 | 4. Front-end now ready to login the user. To do it, send POST request again with provider name and the `oauth_token` and `oauth_verifier` you got from the provider: 246 | 247 | POST /api/login/social/ 248 | 249 | with data (form data or json) 250 | 251 | provider=twitter&oauth_token=AQBPBBTjbdnehj51&oauth_verifier=wDBdTR7CYdR 252 | 253 | Backend will either signin the user, or signup, or return an error. 254 | Same as in OAuth 2.0, you can specify provider in url, not in body: 255 | 256 | POST /api/login/social/twitter 257 | 258 | This flow is the same as described in [satellizer](https://github.com/sahat/satellizer#-login-with-oauth-10). This angularjs module is used in example project. 259 | 260 | #### Note 261 | If you use token (or jwt) authentication and OAuth 1.0, then you still need 'django.contrib.sessions' app (it is not required for OAuth 2.0 and token authentication). 262 | This is because python-social-auth will store some data in session between requests to OAuth 1.0 provider. 263 | 264 | 265 | rest-social-auth purpose 266 | ------------------------ 267 | 268 | As we can see, our backend must implement resource for signin the user. 269 | 270 | Django REST social auth provides means to easily implement such resource. 271 | 272 | 273 | List of oauth providers 274 | ----------------------- 275 | 276 | OAuth 1.0 and OAuth 2.0 providers are supported. 277 | 278 | Look [python-social-auth](https://github.com/omab/python-social-auth#user-content-auth-providers) for full list. 279 | Name of provider is taken from corresponding `backend.name` property of 280 | particular backed class in python-social-auth. 281 | 282 | For example for [facebook backend](https://github.com/omab/python-social-auth/blob/master/social/backends/facebook.py#L19) 283 | we see: 284 | 285 | class FacebookOAuth2(BaseOAuth2): 286 | name = 'facebook' 287 | 288 | Here are some provider names: 289 | 290 | Provider | provider name 291 | --------- | ------------- 292 | Facebook | facebook 293 | Google | google-oauth2 294 | Vkontakte | vk-oauth2 295 | Instagram | instagram 296 | Github | github 297 | Yandex | yandex-oauth2 298 | Twitter | twitter 299 | [Others](https://github.com/omab/python-social-auth#user-content-auth-providers) | ... 300 | 301 | 302 | Settings 303 | -------- 304 | 305 | - `REST_SOCIAL_OAUTH_REDIRECT_URI` 306 | 307 | Default: `'/'` 308 | 309 | Defines redirect_uri. This redirect must be the same in both authorize request (made by front-end) and access token request (made by back-end) to OAuth provider. 310 | 311 | To override the relative path (url path or url name are both supported): 312 | 313 | REST_SOCIAL_OAUTH_REDIRECT_URI = '/oauth/redirect/path/' 314 | # or url name 315 | REST_SOCIAL_OAUTH_REDIRECT_URI = 'redirect_url_name' 316 | 317 | Note, in case of url name, backend name will be provided to url resolver as argument. 318 | 319 | - `REST_SOCIAL_DOMAIN_FROM_ORIGIN` 320 | 321 | Default: `True` 322 | 323 | Sometimes front-end and back-end are run on different domains. 324 | For example frontend at 'myproject.com', and backend at 'api.myproject.com'. 325 | 326 | If True, domain will be taken from request origin, if origin is defined. 327 | So in current example domain will be 'myproject.com', not 'api.myproject.com'. 328 | Next, this domain will be joined with path from `REST_SOCIAL_OAUTH_REDIRECT_URI` settings. 329 | 330 | To be clear, suppose we have following settings (defaults): 331 | 332 | REST_SOCIAL_OAUTH_REDIRECT_URI = '/' 333 | REST_SOCIAL_DOMAIN_FROM_ORIGIN = True 334 | 335 | Front-end is running on domain 'myproject.com', back-end - on 'api.myproject.com'. 336 | Back-end will use following redirect_uri: 337 | 338 | myproject.com/ 339 | 340 | And with following settings: 341 | 342 | REST_SOCIAL_OAUTH_REDIRECT_URI = '/' 343 | REST_SOCIAL_DOMAIN_FROM_ORIGIN = False 344 | 345 | redirect_uri will be: 346 | 347 | api.myproject.com/ 348 | 349 | Also look at [django-cors-headers](https://github.com/ottoyiu/django-cors-headers) if such architecture is your case. 350 | 351 | - `REST_SOCIAL_OAUTH_ABSOLUTE_REDIRECT_URI` 352 | 353 | Default: `None` 354 | 355 | Full redirect uri (domain and path) can be hardcoded 356 | 357 | REST_SOCIAL_OAUTH_ABSOLUTE_REDIRECT_URI = 'http://myproject.com/' 358 | 359 | This settings has higher priority than `REST_SOCIAL_OAUTH_REDIRECT_URI` and `REST_SOCIAL_DOMAIN_FROM_ORIGIN`. 360 | I.e. if this settings is defined, other will be ignored. 361 | But `redirect_uri` param from request has higher priority than any setting. 362 | 363 | 364 | 365 | Customization 366 | ------------- 367 | 368 | First of all, customization provided by python-social-auth is also avaliable. 369 | For example, use nice mechanism of [pipeline](http://psa.matiasaguirre.net/docs/pipeline.html) to do any action you need during login/signin. 370 | 371 | Second, you can override any method from current package. 372 | Specify serializer for each view by subclassing the view. 373 | 374 | To do it 375 | 376 | - define your own url: 377 | 378 | url(r'^api/login/social/$', MySocialView.as_view(), name='social_login'), 379 | 380 | - define your serializer 381 | 382 | from rest_framework import serializers 383 | from django.contrib.auth import get_user_model 384 | 385 | class MyUserSerializer(serializers.ModelSerializer): 386 | 387 | class Meta: 388 | model = get_user_model() 389 | exclude = ('password', 'user_permissions', 'groups') 390 | 391 | - define view 392 | 393 | from rest_social_auth.views import SocialSessionAuthView 394 | from .serializers import MyUserSerializer 395 | 396 | class MySocialView(SocialSessionAuthView): 397 | serializer_class = MyUserSerializer 398 | 399 | Check the code of the lib, there is not much of it. 400 | 401 | 402 | Example 403 | ------- 404 | 405 | There is an [example project](https://github.com/st4lk/django-rest-social-auth/tree/master/example_project). 406 | 407 | - make sure you have rest-social-auth installed 408 | 409 | pip install rest-social-auth 410 | 411 | - clone repo 412 | 413 | git clone https://github.com/st4lk/django-rest-social-auth.git 414 | 415 | - step in example_project/ 416 | 417 | cd django-rest-social-auth/example_project 418 | 419 | - create database (sqlite3) 420 | 421 | python manage.py syncdb 422 | 423 | - run development server 424 | 425 | python manage.py runserver 426 | 427 | Example project already contains facebook, google and twitter app ids and secrets. 428 | These apps are configured to work only with restsocialexample.com domain (localhost is not supported by some providers). 429 | So, to play with it, define in your [hosts](http://en.wikipedia.org/wiki/Hosts_\(file\)) file this domain as localhost: 430 | 431 | 127.0.0.1 restsocialexample.com 432 | 433 | And visit http://restsocialexample.com:8000/ 434 | 435 | Example project uses [satellizer](https://github.com/sahat/satellizer) angularjs module. 436 | 437 | 438 | Contributors 439 | ------------ 440 | 441 | - Alexey Evseev, [st4lk](https://github.com/st4lk) 442 | - James Keys, [skolsuper](https://github.com/skolsuper) 443 | - Aaron Abbott, [aabmass](https://github.com/aabmass) 444 | - Grigorii Eremeev, [Budulianin](https://github.com/Budulianin) 445 | - shubham, [shubh3794](https://github.com/shubh3794) 446 | - Deshraj Yadav, [DESHRAJ](https://github.com/DESHRAJ) 447 | --------------------------------------------------------------------------------