├── .gitignore ├── CHANGELOG.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── contributors.txt ├── rest_framework_social_oauth2 ├── __init__.py ├── authentication.py ├── backends.py ├── management │ └── commands │ │ └── createapp.py ├── oauth2_backends.py ├── oauth2_endpoints.py ├── oauth2_grants.py ├── settings.py ├── urls.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | *.DS_Store 57 | *.pyc 58 | 59 | ### Sublime Files ### 60 | *.sublime-project 61 | *.sublime-workspace 62 | 63 | ### OSX ### 64 | .DS_Store 65 | 66 | # Thumbnails 67 | ._* 68 | 69 | # Files that might appear on external disk 70 | .Spotlight-V100 71 | .Trashes 72 | 73 | ### Linux ### 74 | .* 75 | !.gitignore 76 | *~ 77 | 78 | # KDE 79 | .directory 80 | 81 | ### Windows ### 82 | Thumbs.db 83 | 84 | # Folder config file 85 | Desktop.ini 86 | 87 | # Recycle Bin used on file shares 88 | $RECYCLE.BIN/ 89 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Change log 2 | ========== 3 | 4 | 1.2.0 - 2024-01-12 5 | ------------------ 6 | 7 | - Add support for Django 4.0 8 | - Drop support for python 2, enforce python >=3.5 9 | - Reference the User model with get_user_model() 10 | - Fix ACCESS_TOKEN_URL namespace 11 | - Refactor README 12 | - Bump dependencies 13 | - Remove django-braces dependency 14 | - Set Django request object through a server method call 15 | - Added a new Django Rest Framework view to disconnect backend 16 | - Update documentation with Google example 17 | - Create manage.py command to create an application 18 | - Remove harcoded oauthlibcore 19 | - Fix NoRerverseMatch Error with custom namespace 20 | - Updated invalidate_sessions to accept all POST content types 21 | - Restore compatibility with Django<2.0 22 | - Keep request.data mutable 23 | - Added compatibility with Django 2.0 24 | 25 | 1.1.0 - 2018-01-25 26 | ------------------ 27 | 28 | - 29 | 30 | 1.0.8 - 2017-06-18 31 | ------------------ 32 | 33 | - Added `django-braces` as a dependency 34 | 35 | 1.0.7 - 2017-06-17 36 | ------------------ 37 | 38 | - Added support for `django-oauth-toolkit` 1.0.0 39 | 40 | 1.0.6 - 2017-05-22 41 | ------------------ 42 | 43 | - Fix a bug where inactive users could still get tokens 44 | 45 | 46 | 1.0.5 - 2017-01-03 47 | ------------------ 48 | 49 | - Updated python-social-auth to social (`Migrating guide `_) 50 | - Wrapped token view and revoke token view in a rest framework APIView 51 | - Added url namespace 52 | - Renamed PROPRIETARY_BACKEND_NAME to DRFSO2_PROPRIETARY_BACKEND_NAME 53 | 54 | 55 | 1.0.2 - 2015-08-11 56 | ------------------ 57 | 58 | - Fix a bug where the hack to keep the django request was not working due to oauthlib encoding the object 59 | 60 | 1.0.1 - 2015-08-09 61 | ------------------ 62 | 63 | - Forgot to update django-oauth-toolkit version in setup.py (version 0.9.0 needed because of `this change `_) 64 | 65 | 1.0.0 - 2015-07-30 66 | ------------------ 67 | 68 | - Convert token view api changed and is now more conform to the oauth2 api. 69 | - Removed PROPRIETARY_BACKEND_NAME setting 70 | - Invalidate sessions view now takes a client_id as a parameter 71 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Philip Garnero 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. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include LICENSE.txt 3 | include README.rst 4 | include CHANGELOG.rst 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django REST Framework Social OAuth2 2 | =================================== 3 | 4 | .. image:: https://badge.fury.io/py/django-rest-framework-social-oauth2.svg 5 | :target: http://badge.fury.io/py/django-rest-framework-social-oauth2 6 | 7 | This module provides OAuth2 social authentication support for applications in Django REST Framework. 8 | 9 | The aim of this package is to help set up social authentication for your REST API. It also helps setting up your OAuth2 provider. 10 | 11 | This package relies on `python-social-auth `_ and `django-oauth-toolkit `_. 12 | You should probably read their docs if you were to go further than what is done here. 13 | If you have some hard time understanding OAuth2, you can read a simple explanation `here `_. 14 | 15 | 16 | Installation 17 | ------------ 18 | 19 | Install with pip:: 20 | 21 | pip install django-rest-framework-social-oauth2 22 | 23 | 24 | Add the following to your ``INSTALLED_APPS``: 25 | 26 | .. code-block:: python 27 | 28 | INSTALLED_APPS = ( 29 | ... 30 | 'oauth2_provider', 31 | 'social_django', 32 | 'rest_framework_social_oauth2', 33 | ) 34 | 35 | 36 | Include social auth urls to your urls.py: 37 | 38 | .. code-block:: python 39 | 40 | urlpatterns = patterns( 41 | ... 42 | (r'^auth/', include('rest_framework_social_oauth2.urls')), 43 | ) 44 | 45 | 46 | Add these context processors to your ``TEMPLATE_CONTEXT_PROCESSORS``: 47 | 48 | .. code-block:: python 49 | 50 | TEMPLATE_CONTEXT_PROCESSORS = ( 51 | ... 52 | 'social_django.context_processors.backends', 53 | 'social_django.context_processors.login_redirect', 54 | ) 55 | 56 | NB: since Django version 1.8, the ``TEMPLATE_CONTEXT_PROCESSORS`` is deprecated, set the ``'context_processors'`` option in the ``'OPTIONS'`` of a DjangoTemplates backend instead: 57 | 58 | .. code-block:: python 59 | 60 | TEMPLATES = [ 61 | { 62 | ... 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | ... 66 | 'social_django.context_processors.backends', 67 | 'social_django.context_processors.login_redirect', 68 | ], 69 | }, 70 | } 71 | ] 72 | 73 | 74 | You can then enable the authentication classes for Django REST Framework by default or per view (add or update the ``REST_FRAMEWORK`` and ``AUTHENTICATION_BACKENDS`` entries in your settings.py) 75 | 76 | .. code-block:: python 77 | 78 | REST_FRAMEWORK = { 79 | ... 80 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 81 | ... 82 | # 'oauth2_provider.ext.rest_framework.OAuth2Authentication', # django-oauth-toolkit < 1.0.0 83 | 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', # django-oauth-toolkit >= 1.0.0 84 | 'rest_framework_social_oauth2.authentication.SocialAuthentication', 85 | ), 86 | } 87 | 88 | .. code-block:: python 89 | 90 | AUTHENTICATION_BACKENDS = ( 91 | ... 92 | 'rest_framework_social_oauth2.backends.DjangoOAuth2', 93 | 'django.contrib.auth.backends.ModelBackend', 94 | ) 95 | 96 | 97 | The settings of this app are: 98 | 99 | - ``DRFSO2_PROPRIETARY_BACKEND_NAME``: name of your OAuth2 social backend (e.g ``"Facebook"``), defaults to ``"Django"`` 100 | - ``DRFSO2_URL_NAMESPACE``: namespace for reversing URLs 101 | 102 | Setting Up a New Application 103 | ---------------------------- 104 | 105 | Go to Django admin and add a new Application with the following configuration: 106 | 107 | - ``client_id`` and ``client_secret`` should be left unchanged 108 | - ``user`` should be your superuser 109 | - ``redirect_uris`` should be left blank 110 | - ``client_type`` should be set to ``confidential`` 111 | - ``authorization_grant_type`` should be set to ``'Resource owner password-based'`` 112 | - ``name`` can be set to whatever you'd like 113 | 114 | The installation is done, you can now test the newly configured application. 115 | 116 | It is recommended that you read the docs from `python-social-auth` and `django-oauth-toolkit` if you would like to go further. If you want to enable a social backend (e.g. Facebook), check the docs of `python-social-auth` on `supported backends `_ and `django-social-auth` on `backend configuration `_. 117 | 118 | 119 | Testing the Setup 120 | ----------------- 121 | 122 | Now that the installation is done, let's try out the various functionality. 123 | We will assume for the following examples that the REST API is reachable on ``http://localhost:8000``. 124 | 125 | - Retrieve a token for a user using ``curl``:: 126 | 127 | curl -X POST -d "client_id=&client_secret=&grant_type=password&username=&password=" http://localhost:8000/auth/token 128 | 129 | ```` and ```` are the keys generated automatically. you can find in the model Application you created. 130 | 131 | - Refresh token:: 132 | 133 | curl -X POST -d "grant_type=refresh_token&client_id=&client_secret=&refresh_token=" http://localhost:8000/auth/token 134 | 135 | - Exchange an external token for a token linked to your app:: 136 | 137 | curl -X POST -d "grant_type=convert_token&client_id=&client_secret=&backend=&token=" http://localhost:8000/auth/convert-token 138 | 139 | ```` here needs to be replaced by the name of an enabled backend (e.g. "Facebook"). Note that ``PROPRIETARY_BACKEND_NAME`` is a valid backend name, but there is no use to do that here. 140 | ```` is for the token you got from the service utilizing an iOS app for example. 141 | 142 | - Revoke tokens: 143 | 144 | Revoke a single token:: 145 | 146 | curl -X POST -d "client_id=&client_secret=&token=" http://localhost:8000/auth/revoke-token 147 | 148 | Revoke all tokens for a user:: 149 | 150 | curl -H "Authorization: Bearer " -X POST -d "client_id=" http://localhost:8000/auth/invalidate-sessions 151 | 152 | 153 | Authenticating Requests 154 | ----------------------- 155 | 156 | As you have probably noticed, we enabled a default authentication backend called ``SocialAuthentication``. 157 | This backend lets you register and authenticate your users seamlessly with your REST API. 158 | 159 | The class simply retrieves the backend name and token from the Authorization header and tries to authenticate the user using the corresponding external provider. If the user was not yet registered on your app, it will automatically create a new user for this purpose. 160 | 161 | Example authenticated request:: 162 | 163 | curl -H "Authorization: Bearer " http://localhost:8000/route/to/your/view 164 | 165 | 166 | Integration Examples 167 | -------------------- 168 | 169 | For each authentication provider, the top portion of your REST API settings.py file should look like this: 170 | 171 | .. code-block:: python 172 | 173 | INSTALLED_APPS = ( 174 | ... 175 | # OAuth 176 | 'oauth2_provider', 177 | 'social_django', 178 | 'rest_framework_social_oauth2', 179 | ) 180 | 181 | TEMPLATES = [ 182 | { 183 | ... 184 | 'OPTIONS': { 185 | 'context_processors': [ 186 | ... 187 | # OAuth 188 | 'social_django.context_processors.backends', 189 | 'social_django.context_processors.login_redirect', 190 | ], 191 | }, 192 | } 193 | ] 194 | 195 | REST_FRAMEWORK = { 196 | ... 197 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 198 | ... 199 | # OAuth 200 | # 'oauth2_provider.ext.rest_framework.OAuth2Authentication', # django-oauth-toolkit < 1.0.0 201 | 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', # django-oauth-toolkit >= 1.0.0 202 | 'rest_framework_social_oauth2.authentication.SocialAuthentication', 203 | ) 204 | } 205 | 206 | Listed below are a few examples of supported backends that can be used for social authentication. 207 | 208 | 209 | Facebook Example 210 | ^^^^^^^^^^^^^^^^ 211 | 212 | To use Facebook as the authorization backend of your REST API, your settings.py file should look like this: 213 | 214 | .. code-block:: python 215 | 216 | AUTHENTICATION_BACKENDS = ( 217 | # Others auth providers (e.g. Google, OpenId, etc) 218 | ... 219 | 220 | # Facebook OAuth2 221 | 'social_core.backends.facebook.FacebookAppOAuth2', 222 | 'social_core.backends.facebook.FacebookOAuth2', 223 | 224 | # django-rest-framework-social-oauth2 225 | 'rest_framework_social_oauth2.backends.DjangoOAuth2', 226 | 227 | # Django 228 | 'django.contrib.auth.backends.ModelBackend', 229 | ) 230 | 231 | # Facebook configuration 232 | SOCIAL_AUTH_FACEBOOK_KEY = '' 233 | SOCIAL_AUTH_FACEBOOK_SECRET = '' 234 | 235 | # Define SOCIAL_AUTH_FACEBOOK_SCOPE to get extra permissions from Facebook. 236 | # Email is not sent by default, to get it, you must request the email permission. 237 | SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] 238 | SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { 239 | 'fields': 'id, name, email' 240 | } 241 | 242 | Remember to add this new Application in your Django admin (see section "Setting up Application"). 243 | 244 | You can test these settings by running the following command:: 245 | 246 | curl -X POST -d "grant_type=convert_token&client_id=&client_secret=&backend=facebook&token=" http://localhost:8000/auth/convert-token 247 | 248 | This request returns the "access_token" that you should use with every HTTP request to your REST API. What is happening here is that we are converting a third-party access token (````) to an access token to use with your API and its clients ("access_token"). You should use this token on each and further communications between your system/application and your api to authenticate each request and avoid authenticating with Facebook every time. 249 | 250 | You can get the ID (``SOCIAL_AUTH_FACEBOOK_KEY``) and secret (``SOCIAL_AUTH_FACEBOOK_SECRET``) of your app at https://developers.facebook.com/apps/. 251 | 252 | For testing purposes, you can use the access token ```` from https://developers.facebook.com/tools/accesstoken/. 253 | 254 | For more information on how to configure python-social-auth with Facebook visit http://python-social-auth.readthedocs.io/en/latest/backends/facebook.html. 255 | 256 | 257 | Google Example 258 | ^^^^^^^^^^^^^^ 259 | 260 | To use Google OAuth2 as the authorization backend of your REST API, your settings.py file should look like this: 261 | 262 | .. code-block:: python 263 | 264 | AUTHENTICATION_BACKENDS = ( 265 | # Others auth providers (e.g. Facebook, OpenId, etc) 266 | ... 267 | 268 | # Google OAuth2 269 | 'social_core.backends.google.GoogleOAuth2', 270 | 271 | # django-rest-framework-social-oauth2 272 | 'rest_framework_social_oauth2.backends.DjangoOAuth2', 273 | 274 | # Django 275 | 'django.contrib.auth.backends.ModelBackend', 276 | ) 277 | 278 | # Google configuration 279 | SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 280 | SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 281 | 282 | # Define SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE to get extra permissions from Google. 283 | SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [ 284 | 'https://www.googleapis.com/auth/userinfo.email', 285 | 'https://www.googleapis.com/auth/userinfo.profile', 286 | ] 287 | 288 | Remember to add the new Application in your Django admin (see section "Setting up Application"). 289 | 290 | You can test these settings by running the following command:: 291 | 292 | curl -X POST -d "grant_type=convert_token&client_id=&client_secret=&backend=google-oauth2&token=" http://localhost:8000/auth/convert-token 293 | 294 | This request returns an "access_token" that you should use with every HTTP requests to your REST API. What is happening here is that we are converting a third-party access token (````) to an access token to use with your API and its clients ("access_token"). You should use this token on each and further communications between your system/application and your API to authenticate each request and avoid authenticating with Google every time. 295 | 296 | You can get the ID (``SOCIAL_AUTH_GOOGLE_OAUTH2_KEY``) and secret (``SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET``) of your app at https://console.developers.google.com/apis/credentials 297 | and more information on how to create one on https://developers.google.com/identity/protocols/OAuth2. 298 | 299 | For testing purposes, you can use the access token ```` from https://developers.google.com/oauthplayground/. 300 | 301 | For more information on how to configure python-social-auth with Google visit https://python-social-auth.readthedocs.io/en/latest/backends/google.html#google-oauth2. 302 | -------------------------------------------------------------------------------- /contributors.txt: -------------------------------------------------------------------------------- 1 | Hugo Sequeira @hugocore 2 | Ryan Blunden @ryan_blunden 3 | Edward Romano @oudeismetis 4 | Vitaly Babiy @vbabiy 5 | Deshraj Yadav @DESHRAJ 6 | Alex @alexpilot11 7 | Trent Holliday @trumpet2012 8 | Aljaž Košir @aljazkosir 9 | Jakub Boukal @SukiCZ 10 | -------------------------------------------------------------------------------- /rest_framework_social_oauth2/__init__.py: -------------------------------------------------------------------------------- 1 | """python-social-auth and oauth2 support for django-rest-framework""" 2 | __version__ = "1.2.0" 3 | -------------------------------------------------------------------------------- /rest_framework_social_oauth2/authentication.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.urls import reverse 3 | except ImportError: # Will be removed in Django 2.0 4 | from django.core.urlresolvers import reverse 5 | 6 | from rest_framework.authentication import BaseAuthentication, get_authorization_header 7 | from rest_framework import exceptions, HTTP_HEADER_ENCODING 8 | 9 | from social_django.views import NAMESPACE 10 | from social_django.utils import load_backend, load_strategy 11 | from social_core.exceptions import MissingBackend 12 | from social_core.utils import requests 13 | 14 | from .settings import DRFSO2_URL_NAMESPACE 15 | 16 | 17 | class SocialAuthentication(BaseAuthentication): 18 | """ 19 | Authentication backend using `python-social-auth` 20 | 21 | Clients should authenticate by passing the token key in the "Authorization" 22 | HTTP header with the backend used, prepended with the string "Bearer ". 23 | 24 | For example: 25 | 26 | Authorization: Bearer facebook 401f7ac837da42b97f613d789819ff93537bee6a 27 | """ 28 | www_authenticate_realm = 'api' 29 | 30 | def authenticate(self, request): 31 | """ 32 | Returns two-tuple of (user, token) if authentication succeeds, 33 | or None otherwise. 34 | """ 35 | auth_header = get_authorization_header(request).decode(HTTP_HEADER_ENCODING) 36 | auth = auth_header.split() 37 | 38 | if not auth or auth[0].lower() != 'bearer': 39 | return None 40 | 41 | if len(auth) == 1: 42 | msg = 'Invalid token header. No backend provided.' 43 | raise exceptions.AuthenticationFailed(msg) 44 | elif len(auth) == 2: 45 | msg = 'Invalid token header. No credentials provided.' 46 | raise exceptions.AuthenticationFailed(msg) 47 | elif len(auth) > 3: 48 | msg = 'Invalid token header. Token string should not contain spaces.' 49 | raise exceptions.AuthenticationFailed(msg) 50 | 51 | token = auth[2] 52 | backend = auth[1] 53 | 54 | strategy = load_strategy(request=request) 55 | 56 | try: 57 | backend = load_backend(strategy, backend, reverse("%s:%s:complete" % (DRFSO2_URL_NAMESPACE, NAMESPACE), args=(backend,))) 58 | except MissingBackend: 59 | msg = 'Invalid token header. Invalid backend.' 60 | raise exceptions.AuthenticationFailed(msg) 61 | 62 | try: 63 | user = backend.do_auth(access_token=token) 64 | except requests.HTTPError as e: 65 | msg = e.response.text 66 | raise exceptions.AuthenticationFailed(msg) 67 | 68 | if not user: 69 | msg = 'Bad credentials.' 70 | raise exceptions.AuthenticationFailed(msg) 71 | return user, token 72 | 73 | def authenticate_header(self, request): 74 | """ 75 | Bearer is the only finalized type currently 76 | """ 77 | return 'Bearer backend realm="%s"' % self.www_authenticate_realm 78 | -------------------------------------------------------------------------------- /rest_framework_social_oauth2/backends.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.urls import reverse 3 | except ImportError: # Will be removed in Django 2.0 4 | from django.core.urlresolvers import reverse 5 | 6 | from social_core.backends.oauth import BaseOAuth2 7 | from .settings import DRFSO2_PROPRIETARY_BACKEND_NAME, DRFSO2_URL_NAMESPACE 8 | 9 | class DjangoOAuth2(BaseOAuth2): 10 | """Default OAuth2 authentication backend used by this package""" 11 | name = DRFSO2_PROPRIETARY_BACKEND_NAME 12 | AUTHORIZATION_URL = reverse(DRFSO2_URL_NAMESPACE + ':authorize' 13 | if DRFSO2_URL_NAMESPACE else 'authorize') 14 | ACCESS_TOKEN_URL = reverse(DRFSO2_URL_NAMESPACE + ':token' 15 | if DRFSO2_URL_NAMESPACE else 'token') 16 | -------------------------------------------------------------------------------- /rest_framework_social_oauth2/management/commands/createapp.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.core.management.base import BaseCommand, CommandError 3 | from django.contrib.auth import get_user_model 4 | from oauth2_provider.models import Application 5 | from oauth2_provider.generators import generate_client_id, generate_client_secret 6 | 7 | 8 | User = get_user_model() 9 | 10 | 11 | class Command(BaseCommand): 12 | help = "Create a Django OAuth Toolkit application (an existing admin is required)" 13 | 14 | def add_arguments(self, parser): 15 | parser.add_argument( 16 | "-ci", "--client_id", 17 | help="Client ID (recommeded 40 characters long)" 18 | ) 19 | parser.add_argument( 20 | "-cs", "--client_secret", 21 | help="Client Secret (recommeded 128 characters long)" 22 | ) 23 | parser.add_argument( 24 | "-n", "--name", 25 | help="Name for the application" 26 | ) 27 | 28 | def handle(self, *args, **options): 29 | new_application = Application( 30 | user=User.objects.filter(is_superuser=True)[0], 31 | client_type="confidential", 32 | authorization_grant_type="password", 33 | name=options["name"] or "socialauth_application", 34 | client_id=options["client_id"] or generate_client_id(), 35 | client_secret=options["client_secret"] or generate_client_secret(), 36 | ) 37 | new_application.save() 38 | -------------------------------------------------------------------------------- /rest_framework_social_oauth2/oauth2_backends.py: -------------------------------------------------------------------------------- 1 | from oauth2_provider.oauth2_backends import OAuthLibCore 2 | from oauth2_provider.settings import oauth2_settings 3 | 4 | from .oauth2_endpoints import SocialTokenServer 5 | 6 | 7 | class KeepRequestCore(oauth2_settings.OAUTH2_BACKEND_CLASS): 8 | """ 9 | Subclass of `oauth2_settings.OAUTH2_BACKEND_CLASS`, used for the sake of 10 | keeping the Django request object by passing it through to the 11 | `server_class` instance. 12 | 13 | This backend should only be used in views with SocialTokenServer 14 | as the `server_class`. 15 | """ 16 | 17 | def __init__(self, *args, **kwargs): 18 | super(KeepRequestCore, self).__init__(*args, **kwargs) 19 | if not isinstance(self.server, SocialTokenServer): 20 | raise TypeError( 21 | "server_class must be an instance of 'SocialTokenServer'" 22 | ) 23 | 24 | def create_token_response(self, request): 25 | """ 26 | A wrapper method that calls create_token_response on `server_class` instance. 27 | This method is modified to also pass the `django.http.HttpRequest` 28 | request object. 29 | 30 | :param request: The current django.http.HttpRequest object 31 | """ 32 | self.server.set_request_object(request) 33 | return super(KeepRequestCore, self).create_token_response(request) 34 | -------------------------------------------------------------------------------- /rest_framework_social_oauth2/oauth2_endpoints.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.http import HttpRequest 4 | 5 | from oauthlib.common import Request 6 | from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint 7 | from oauthlib.oauth2.rfc6749.tokens import BearerToken 8 | from oauthlib.oauth2.rfc6749.endpoints.base import catch_errors_and_unavailability 9 | 10 | from .oauth2_grants import SocialTokenGrant 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class SocialTokenServer(TokenEndpoint): 16 | """An endpoint used only for token generation. 17 | 18 | Use this with the KeepRequestCore backend class. 19 | """ 20 | 21 | def __init__(self, request_validator, token_generator=None, 22 | token_expires_in=None, refresh_token_generator=None, **kwargs): 23 | """Construct a client credentials grant server. 24 | :param request_validator: An implementation of 25 | oauthlib.oauth2.RequestValidator. 26 | :param token_expires_in: An int or a function to generate a token 27 | expiration offset (in seconds) given a 28 | oauthlib.common.Request object. 29 | :param token_generator: A function to generate a token from a request. 30 | :param refresh_token_generator: A function to generate a token from a 31 | request for the refresh token. 32 | :param kwargs: Extra parameters to pass to authorization-, 33 | token-, resource-, and revocation-endpoint constructors. 34 | """ 35 | self._params = {} 36 | refresh_grant = SocialTokenGrant(request_validator) 37 | bearer = BearerToken(request_validator, token_generator, 38 | token_expires_in, refresh_token_generator) 39 | TokenEndpoint.__init__(self, default_grant_type='convert_token', 40 | grant_types={ 41 | 'convert_token': refresh_grant, 42 | }, 43 | default_token_type=bearer) 44 | 45 | def set_request_object(self, request): 46 | """This should be called by the KeepRequestCore backend class before 47 | calling `create_token_response` to store the Django request object. 48 | """ 49 | if not isinstance(request, HttpRequest): 50 | raise TypeError( 51 | "request must be an instance of 'django.http.HttpRequest'" 52 | ) 53 | self._params['http_request'] = request 54 | 55 | def pop_request_object(self): 56 | """This is called internaly by `create_token_response` 57 | to fetch the Django request object and cleanup class instance. 58 | """ 59 | return self._params.pop('http_request', None) 60 | 61 | # We override this method just so we can pass the django request object 62 | @catch_errors_and_unavailability 63 | def create_token_response(self, uri, http_method='GET', body=None, 64 | headers=None, credentials=None): 65 | """Extract grant_type and route to the designated handler.""" 66 | request = Request( 67 | uri, http_method=http_method, body=body, headers=headers) 68 | request.scopes = None 69 | request.extra_credentials = credentials 70 | 71 | # Make sure we consume the django request object 72 | request.django_request = self.pop_request_object() 73 | 74 | grant_type_handler = self.grant_types.get(request.grant_type, 75 | self.default_grant_type_handler) 76 | log.debug('Dispatching grant_type %s request to %r.', 77 | request.grant_type, grant_type_handler) 78 | return grant_type_handler.create_token_response( 79 | request, self.default_token_type) 80 | -------------------------------------------------------------------------------- /rest_framework_social_oauth2/oauth2_grants.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | try: 4 | from django.urls import reverse 5 | except ImportError: # Will be removed in Django 2.0 6 | from django.core.urlresolvers import reverse 7 | 8 | from oauthlib.oauth2.rfc6749 import errors 9 | from oauthlib.oauth2.rfc6749.grant_types.refresh_token import RefreshTokenGrant 10 | 11 | from social_django.views import NAMESPACE 12 | from social_django.utils import load_backend, load_strategy 13 | from social_core.exceptions import MissingBackend, SocialAuthBaseException 14 | from social_core.utils import requests 15 | 16 | from .settings import DRFSO2_URL_NAMESPACE 17 | 18 | 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | class SocialTokenGrant(RefreshTokenGrant): 23 | 24 | """`Refresh token grant`_ 25 | .. _`Refresh token grant`: http://tools.ietf.org/html/rfc6749#section-6 26 | """ 27 | 28 | def validate_token_request(self, request): 29 | # This method's code is based on the parent method's code 30 | # We removed the original comments to replace with ours 31 | # explaining our modifications. 32 | 33 | # We need to set these at None by default otherwise 34 | # we are going to get some AttributeError later 35 | request._params.setdefault("backend", None) 36 | request._params.setdefault("client_secret", None) 37 | 38 | if request.grant_type != 'convert_token': 39 | raise errors.UnsupportedGrantTypeError(request=request) 40 | 41 | # We check that a token parameter is present. 42 | # It should contain the social token to be used with the backend 43 | if request.token is None: 44 | raise errors.InvalidRequestError( 45 | description='Missing token parameter.', 46 | request=request) 47 | 48 | # We check that a backend parameter is present. 49 | # It should contain the name of the social backend to be used 50 | if request.backend is None: 51 | raise errors.InvalidRequestError( 52 | description='Missing backend parameter.', 53 | request=request) 54 | 55 | if not request.client_id: 56 | raise errors.MissingClientIdError(request=request) 57 | 58 | if not self.request_validator.validate_client_id(request.client_id, request): 59 | raise errors.InvalidClientIdError(request=request) 60 | 61 | # Existing code to retrieve the application instance from the client id 62 | if self.request_validator.client_authentication_required(request): 63 | log.debug('Authenticating client, %r.', request) 64 | if not self.request_validator.authenticate_client(request): 65 | log.debug('Invalid client (%r), denying access.', request) 66 | raise errors.InvalidClientError(request=request) 67 | elif not self.request_validator.authenticate_client_id(request.client_id, request): 68 | log.debug('Client authentication failed, %r.', request) 69 | raise errors.InvalidClientError(request=request) 70 | 71 | # Ensure client is authorized use of this grant type 72 | # We chose refresh_token as a grant_type 73 | # as we don't want to modify all the codebase. 74 | # It is also the most permissive and logical grant for our needs. 75 | request.grant_type = "refresh_token" 76 | self.validate_grant_type(request) 77 | 78 | self.validate_scopes(request) 79 | 80 | # TODO: Find a better way to pass the django request object 81 | strategy = load_strategy(request=request.django_request) 82 | 83 | try: 84 | backend = load_backend(strategy, request.backend, 85 | reverse("%s:%s:complete" % (DRFSO2_URL_NAMESPACE, NAMESPACE) , args=(request.backend,))) 86 | except MissingBackend: 87 | raise errors.InvalidRequestError( 88 | description='Invalid backend parameter.', 89 | request=request) 90 | 91 | try: 92 | user = backend.do_auth(access_token=request.token) 93 | except requests.HTTPError as e: 94 | raise errors.InvalidRequestError( 95 | description="Backend responded with HTTP{0}: {1}.".format(e.response.status_code, 96 | e.response.text), 97 | request=request) 98 | except SocialAuthBaseException as e: 99 | raise errors.AccessDeniedError(description=str(e), request=request) 100 | 101 | if not user: 102 | raise errors.InvalidGrantError('Invalid credentials given.', request=request) 103 | 104 | if not user.is_active: 105 | raise errors.InvalidGrantError('User inactive or deleted.', request=request) 106 | 107 | request.user = user 108 | log.debug('Authorizing access to user %r.', request.user) 109 | -------------------------------------------------------------------------------- /rest_framework_social_oauth2/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | DRFSO2_PROPRIETARY_BACKEND_NAME = getattr(settings, 'DRFSO2_PROPRIETARY_BACKEND_NAME', "Django") 4 | DRFSO2_URL_NAMESPACE = getattr(settings, 'DRFSO2_URL_NAMESPACE', "") 5 | -------------------------------------------------------------------------------- /rest_framework_social_oauth2/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include 2 | try: 3 | from django.conf.urls import url 4 | except ImportError: 5 | from django.urls import re_path as url 6 | 7 | from oauth2_provider.views import AuthorizationView 8 | 9 | from .views import ConvertTokenView, TokenView, RevokeTokenView, invalidate_sessions, DisconnectBackendView 10 | 11 | app_name = 'drfso2' 12 | 13 | urlpatterns = [ 14 | url(r'^authorize/?$', AuthorizationView.as_view(), name="authorize"), 15 | url(r'^token/?$', TokenView.as_view(), name="token"), 16 | url('', include('social_django.urls', namespace="social")), 17 | url(r'^convert-token/?$', ConvertTokenView.as_view(), name="convert_token"), 18 | url(r'^revoke-token/?$', RevokeTokenView.as_view(), name="revoke_token"), 19 | url(r'^invalidate-sessions/?$', invalidate_sessions, name="invalidate_sessions"), 20 | url(r'^disconnect-backend/?$', DisconnectBackendView.as_view(), name="disconnect_backend") 21 | ] 22 | -------------------------------------------------------------------------------- /rest_framework_social_oauth2/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.urls import reverse 4 | from django.utils.decorators import method_decorator 5 | from django.views.decorators.csrf import csrf_exempt 6 | from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint 7 | from social_core.exceptions import MissingBackend 8 | from social_django.utils import load_strategy, load_backend 9 | from social_django.views import NAMESPACE 10 | 11 | from oauth2_provider.contrib.rest_framework import OAuth2Authentication 12 | from oauth2_provider.models import Application, AccessToken 13 | from oauth2_provider.settings import oauth2_settings 14 | from oauth2_provider.views.mixins import OAuthLibMixin 15 | from rest_framework import permissions 16 | from rest_framework import status 17 | from rest_framework.decorators import api_view, authentication_classes, permission_classes 18 | from rest_framework.response import Response 19 | from rest_framework.views import APIView 20 | 21 | from .oauth2_backends import KeepRequestCore 22 | from .oauth2_endpoints import SocialTokenServer 23 | 24 | 25 | class CsrfExemptMixin(object): 26 | """ 27 | Exempts the view from CSRF requirements. 28 | NOTE: 29 | This should be the left-most mixin of a view. 30 | """ 31 | 32 | @method_decorator(csrf_exempt) 33 | def dispatch(self, *args, **kwargs): 34 | return super(CsrfExemptMixin, self).dispatch(*args, **kwargs) 35 | 36 | 37 | class TokenView(CsrfExemptMixin, OAuthLibMixin, APIView): 38 | """ 39 | Implements an endpoint to provide access tokens 40 | 41 | The endpoint is used in the following flows: 42 | 43 | * Authorization code 44 | * Password 45 | * Client credentials 46 | """ 47 | server_class = oauth2_settings.OAUTH2_SERVER_CLASS 48 | validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS 49 | oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS 50 | permission_classes = (permissions.AllowAny,) 51 | 52 | def post(self, request, *args, **kwargs): 53 | # Use the rest framework `.data` to fake the post body of the django request. 54 | mutable_data = request.data.copy() 55 | request._request.POST = request._request.POST.copy() 56 | for key, value in mutable_data.items(): 57 | request._request.POST[key] = value 58 | 59 | url, headers, body, status = self.create_token_response(request._request) 60 | response = Response(data=json.loads(body), status=status) 61 | 62 | for k, v in headers.items(): 63 | response[k] = v 64 | return response 65 | 66 | 67 | class ConvertTokenView(CsrfExemptMixin, OAuthLibMixin, APIView): 68 | """ 69 | Implements an endpoint to convert a provider token to an access token 70 | 71 | The endpoint is used in the following flows: 72 | 73 | * Authorization code 74 | * Client credentials 75 | """ 76 | server_class = SocialTokenServer 77 | validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS 78 | oauthlib_backend_class = KeepRequestCore 79 | permission_classes = (permissions.AllowAny,) 80 | 81 | def post(self, request, *args, **kwargs): 82 | # Use the rest framework `.data` to fake the post body of the django request. 83 | mutable_data = request.data.copy() 84 | request._request.POST = request._request.POST.copy() 85 | for key, value in mutable_data.items(): 86 | request._request.POST[key] = value 87 | 88 | url, headers, body, status = self.create_token_response(request._request) 89 | response = Response(data=json.loads(body), status=status) 90 | 91 | for k, v in headers.items(): 92 | response[k] = v 93 | return response 94 | 95 | 96 | class RevokeTokenView(CsrfExemptMixin, OAuthLibMixin, APIView): 97 | """ 98 | Implements an endpoint to revoke access or refresh tokens 99 | """ 100 | server_class = oauth2_settings.OAUTH2_SERVER_CLASS 101 | validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS 102 | oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS 103 | permission_classes = (permissions.AllowAny,) 104 | 105 | def post(self, request, *args, **kwargs): 106 | # Use the rest framework `.data` to fake the post body of the django request. 107 | mutable_data = request.data.copy() 108 | request._request.POST = request._request.POST.copy() 109 | for key, value in mutable_data.items(): 110 | request._request.POST[key] = value 111 | 112 | url, headers, body, status = self.create_revocation_response(request._request) 113 | response = Response(data=json.loads(body) if body else '', status=status if body else 204) 114 | 115 | for k, v in headers.items(): 116 | response[k] = v 117 | return response 118 | 119 | 120 | @api_view(['POST']) 121 | @authentication_classes([OAuth2Authentication]) 122 | @permission_classes([permissions.IsAuthenticated]) 123 | def invalidate_sessions(request): 124 | client_id = request.data.get("client_id", None) 125 | if client_id is None: 126 | return Response({ 127 | "client_id": ["This field is required."] 128 | }, status=status.HTTP_400_BAD_REQUEST) 129 | 130 | try: 131 | app = Application.objects.get(client_id=client_id) 132 | except Application.DoesNotExist: 133 | return Response({ 134 | "detail": "The application linked to the provided client_id could not be found." 135 | }, status=status.HTTP_400_BAD_REQUEST) 136 | 137 | tokens = AccessToken.objects.filter(user=request.user, application=app) 138 | tokens.delete() 139 | return Response({}, status=status.HTTP_204_NO_CONTENT) 140 | 141 | 142 | class DisconnectBackendView(APIView): 143 | """ 144 | An endpoint for disconnect social auth backend providers such as Facebook. 145 | """ 146 | permission_classes = (permissions.IsAuthenticated, ) 147 | 148 | def get_object(self, queryset=None): 149 | return self.request.user 150 | 151 | def post(self, request, *args, **kwargs): 152 | backend = request.data.get("backend", None) 153 | if backend is None: 154 | return Response({ 155 | "backend": ["This field is required."] 156 | }, status=status.HTTP_400_BAD_REQUEST) 157 | 158 | association_id = request.data.get("association_id", None) 159 | if association_id is None: 160 | return Response({ 161 | "association_id": ["This field is required."] 162 | }, status=status.HTTP_400_BAD_REQUEST) 163 | 164 | strategy = load_strategy(request=request) 165 | try: 166 | backend = load_backend(strategy, backend, reverse(NAMESPACE + ":complete", args=(backend,))) 167 | except MissingBackend: 168 | return Response({"backend": ["Invalid backend."]}, status=status.HTTP_400_BAD_REQUEST) 169 | 170 | backend.disconnect(user=self.get_object(), association_id=association_id, *args, **kwargs) 171 | return Response(status=status.HTTP_204_NO_CONTENT) 172 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import sys 3 | 4 | if sys.version_info < (3, 5): 5 | raise SystemError("This package is for Python 3.5 and above.") 6 | 7 | setup( 8 | name='django-rest-framework-social-oauth2', 9 | version=__import__('rest_framework_social_oauth2').__version__, 10 | description=__import__('rest_framework_social_oauth2').__doc__, 11 | long_description=open('README.rst').read(), 12 | author='Philip Garnero', 13 | author_email='philip.garnero@gmail.com', 14 | url='https://github.com/PhilipGarnero/django-rest-framework-social-oauth2', 15 | license='MIT', 16 | packages=find_packages(), 17 | classifiers=[ 18 | "Development Status :: 4 - Beta", 19 | "Environment :: Web Environment", 20 | "Framework :: Django", 21 | "License :: OSI Approved :: MIT License", 22 | "Programming Language :: Python :: 3", 23 | "Topic :: Internet :: WWW/HTTP", 24 | "Topic :: Software Development :: Libraries :: Python Modules", 25 | ], 26 | install_requires=[ 27 | 'djangorestframework>=3.10.3', 28 | 'django-oauth-toolkit>=0.12.0', 29 | 'social-auth-app-django>=3.1.0', 30 | ], 31 | include_package_data=True, 32 | zip_safe=False, 33 | ) 34 | --------------------------------------------------------------------------------