├── .github └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── RELEASE_NOTES.md ├── example_project ├── config │ ├── __init__.py │ ├── settings.py │ ├── settings_test.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── static │ └── anonymous.png ├── templates │ ├── base.html │ ├── home_jwt.html │ ├── home_knox.html │ ├── home_session.html │ └── home_token.html └── users │ ├── __init__.py │ ├── context_processors.py │ ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20180415_0235.py │ ├── 0003_auto_20190305_0001.py │ └── __init__.py │ ├── models.py │ ├── social_pipeline.py │ └── views.py ├── pytest.ini ├── requirements.txt ├── requirements_optional.txt ├── requirements_test.txt ├── rest_social_auth ├── __init__.py ├── serializers.py ├── strategy.py ├── urls_jwt_pair.py ├── urls_jwt_sliding.py ├── urls_knox.py ├── urls_session.py ├── urls_token.py └── views.py ├── scripts └── entrypoint.sh ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── base.py ├── test_knox.py ├── test_session.py ├── test_simple_jwt.py └── test_token.py └── tox.ini /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: [opened, reopened] 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Setup Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.9' 19 | cache: pip 20 | cache-dependency-path: requirements*.txt 21 | 22 | - name: Install pre-commit 23 | run: | 24 | python -m pip install --upgrade pip wheel 25 | make native-install-all 26 | 27 | - name: Run lint 28 | run: | 29 | make native-lint 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: [opened, reopened] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: 14 | - '3.9' 15 | - '3.10' 16 | - '3.11' 17 | - '3.12' 18 | django-version: 19 | - '42' 20 | - '50' 21 | exclude: 22 | - python-version: '3.9' 23 | django-version: '50' 24 | env: 25 | PYTHON_VERSION: ${{ matrix.python-version }} 26 | DJANGO_VERSION: ${{ matrix.django-version }} 27 | PYTHONUNBUFFERED: 1 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | cache: pip 37 | cache-dependency-path: requirements*.txt 38 | 39 | - name: Install Python dependencies 40 | run: | 41 | python -m pip install --upgrade pip wheel 42 | make native-install-all 43 | python -m pip install tox 44 | 45 | - name: Test with tox 46 | run: | 47 | echo "Running py${PYTHON_VERSION/\./}-django${DJANGO_VERSION}" 48 | tox -e "py${PYTHON_VERSION/\./}-django${DJANGO_VERSION}" 49 | 50 | - name: Coveralls Parallel 51 | uses: coverallsapp/github-action@master 52 | with: 53 | github-token: ${{ secrets.github_token }} 54 | flag-name: run-${{ matrix.python-version }}-${{ matrix.django-version }} 55 | parallel: true 56 | path-to-lcov: "coverage.lcov" 57 | 58 | finish: 59 | needs: test 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Coveralls Finished 63 | uses: coverallsapp/github-action@master 64 | with: 65 | github-token: ${{ secrets.github_token }} 66 | parallel-finished: true 67 | path-to-lcov: "coverage.lcov" 68 | -------------------------------------------------------------------------------- /.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 | .idea/ 18 | .DS_Store 19 | .pytest_cache/ 20 | settings_local.py 21 | .direnv/ 22 | .envrc 23 | .mypy_cache/ 24 | .build 25 | .install 26 | .install-test 27 | .install-optional 28 | .install-python-versions 29 | venv/ 30 | .pyenv/ 31 | lcov.info 32 | cert.* 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.4 2 | 3 | RUN mkdir -p /opt/runtime/ 4 | ADD scripts/* /opt/runtime/ 5 | 6 | RUN useradd -ms /bin/bash appuser 7 | USER appuser 8 | 9 | ENTRYPOINT ["/opt/runtime/entrypoint.sh"] 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include RELEASE_NOTES.md 3 | include requirements.txt 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: run all test test_tox shell run-example native-test native-test-tox 2 | 3 | PROJECT_PATH_DOCKER = /django_rest_social_auth 4 | PROJECT_PATH_NATIVE = "." 5 | PORT ?= 8000 6 | 7 | # --------------------------- 8 | # Commands for running docker 9 | # --------------------------- 10 | 11 | all: run-example 12 | 13 | build: .build 14 | .build: Dockerfile scripts/* 15 | docker build -t st4lk/django-rest-social-auth . 16 | touch $@ 17 | 18 | rebuild: 19 | rm .build 20 | $(MAKE) build 21 | 22 | run: build 23 | docker run -it --rm --name django-rest-social-auth \ 24 | -p $(PORT):$(PORT) \ 25 | -v $(PWD):$(PROJECT_PATH_DOCKER)/ \ 26 | st4lk/django-rest-social-auth "$(COMMAND)" 27 | 28 | run-example: 29 | @COMMAND='make native-run-example' $(MAKE) run 30 | 31 | test: 32 | @COMMAND='make native-test' $(MAKE) run 33 | 34 | test-tox: 35 | @COMMAND='make native-test-tox' $(MAKE) run 36 | 37 | shell: 38 | @COMMAND='bash' $(MAKE) run 39 | 40 | # ------------------------ 41 | # Commands to run natively 42 | # ------------------------ 43 | 44 | native-install: .install 45 | .install: requirements.txt 46 | pip install -r requirements.txt 47 | touch $@ 48 | 49 | native-install-test: .install-test 50 | .install-test: requirements_test.txt 51 | pip install -r requirements_test.txt 52 | touch $@ 53 | 54 | native-install-optional: .install-optional 55 | .install-optional: requirements_optional.txt 56 | pip install -r requirements_optional.txt 57 | touch $@ 58 | 59 | native-install-all: native-install native-install-test native-install-optional 60 | 61 | native-migrate: native-install-all 62 | PYTHONPATH='.' python example_project/manage.py migrate 63 | 64 | native-run-example: native-migrate 65 | PYTHONPATH='.' python example_project/manage.py runserver_plus --cert-file cert.crt 0.0.0.0:$(PORT) 66 | 67 | native-clean: 68 | find . -path ./venv -prune | grep -E "(__pycache__|\.pyc|\.pyo$$)" | xargs rm -rf 69 | 70 | native-test-only: native-install-all native-clean 71 | PYTHONPATH='example_project/' python -m pytest -Wignore $(TEST_ARGS) 72 | 73 | native-test: native-lint native-test-only 74 | 75 | native-lint: native-install-all 76 | flake8 . 77 | 78 | 79 | native-install-python-versions: .install-python-versions 80 | .install-python-versions: tox.ini 81 | curl https://pyenv.run | PYENV_ROOT=$(PROJECT_PATH_NATIVE)/.pyenv bash || echo '-- pyenv already setup, skipping --\n' 82 | PYENV_ROOT=$(PROJECT_PATH_NATIVE)/.pyenv $(PROJECT_PATH_NATIVE)/.pyenv/bin/pyenv install -s 3.7.7 83 | PYENV_ROOT=$(PROJECT_PATH_NATIVE)/.pyenv $(PROJECT_PATH_NATIVE)/.pyenv/bin/pyenv install -s 3.8.2 84 | PYENV_ROOT=$(PROJECT_PATH_NATIVE)/.pyenv $(PROJECT_PATH_NATIVE)/.pyenv/bin/pyenv install -s 3.9.7 85 | PYENV_ROOT=$(PROJECT_PATH_NATIVE)/.pyenv $(PROJECT_PATH_NATIVE)/.pyenv/bin/pyenv install -s 3.10.4 86 | $(PROJECT_PATH_NATIVE)/.pyenv/bin/pyenv global system 3.7.7 3.8.2 3.9.7 3.10.4 87 | touch $@ 88 | 89 | native-test-tox: native-install-python-versions native-clean 90 | pip install tox tox-pyenv 91 | tox 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django REST social auth 2 | ======================= 3 | 4 | [![Lint](https://github.com/st4lk/django-rest-social-auth/actions/workflows/lint.yml/badge.svg?branch=master)](https://github.com/st4lk/django-rest-social-auth/actions/workflows/lint.yml?query=branch%3Amaster) 5 | [![Tests](https://github.com/st4lk/django-rest-social-auth/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/st4lk/django-rest-social-auth/actions/workflows/test.yml?query=branch%3Amaster) 6 | [![Coverage Status](https://coveralls.io/repos/github/st4lk/django-rest-social-auth/badge.svg?branch=master)](https://coveralls.io/github/st4lk/django-rest-social-auth?branch=master) 7 | [![Pypi version](https://img.shields.io/pypi/v/rest_social_auth.svg)](https://pypi.python.org/pypi/rest_social_auth) 8 | [![Downloads](https://pepy.tech/badge/rest-social-auth)](https://pepy.tech/project/rest-social-auth) 9 | 10 | 11 | OAuth signin with django rest framework. 12 | 13 | 14 | Requirements 15 | ----------- 16 | 17 | - python (3.9, 3.10, 3.11, 3.12) 18 | - django (4.2, 5.0, 5.1, 5.2) 19 | - djangorestframework (>=3.9, <4.0) 20 | - social-auth-core (>=4.6.1, <5.0) 21 | - social-auth-app-django (>=5.4.3, <6.0) 22 | - [optional] djangorestframework-simplejwt (>=5.0.0) 23 | - [optional] django-rest-knox (>=4.0.0, <5.0.0) 24 | 25 | Release notes 26 | ------------- 27 | 28 | [Here](https://github.com/st4lk/django-rest-social-auth/blob/master/RELEASE_NOTES.md) 29 | 30 | 31 | Motivation 32 | ---------- 33 | 34 | To have a resource, that will do very simple thing: 35 | take the [oauth code](https://tools.ietf.org/html/rfc6749#section-1.3.1) from social provider (for example facebook) 36 | and return the authenticated user. 37 | That's it. 38 | 39 | I can't find such util for [django rest framework](http://www.django-rest-framework.org/). 40 | There are packages (for example [django-rest-auth](https://github.com/Tivix/django-rest-auth)), that take [access_token](https://tools.ietf.org/html/rfc6749#section-1.4), not the [code](https://tools.ietf.org/html/rfc6749#section-1.3.1). 41 | Also, i've used to work with awesome library [python-social-auth](https://github.com/omab/python-social-auth), 42 | so it will be nice to use it again (now it is split into [social-core](https://github.com/python-social-auth/social-core) and [social-app-django](https://github.com/python-social-auth/social-app-django)). In fact, most of the work is done by this package. 43 | Current util brings a little help to integrate django-rest-framework and python-social-auth. 44 | 45 | Quick start 46 | ----------- 47 | 48 | 1. Install this package to your python distribution: 49 | 50 | ```bash 51 | pip install rest-social-auth 52 | ``` 53 | 54 | 2. Do the settings 55 | 56 | 57 | Install apps 58 | 59 | ```python 60 | INSTALLED_APPS = ( 61 | ... 62 | 'rest_framework', 63 | 'rest_framework.authtoken', # only if you use token authentication 64 | 'social_django', # django social auth 65 | 'rest_social_auth', # this package 66 | 'knox', # Only if you use django-rest-knox 67 | ) 68 | ``` 69 | 70 | social auth settings, look [documentation](http://python-social-auth.readthedocs.io/en/latest/configuration/django.html) for more details 71 | 72 | ```python 73 | SOCIAL_AUTH_FACEBOOK_KEY = 'your app client id' 74 | SOCIAL_AUTH_FACEBOOK_SECRET = 'your app client secret' 75 | SOCIAL_AUTH_FACEBOOK_SCOPE = ['email', ] # optional 76 | SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {'locale': 'ru_RU'} # optional 77 | 78 | 79 | AUTHENTICATION_BACKENDS = ( 80 | 'social_core.backends.facebook.FacebookOAuth2', 81 | # and maybe some others ... 82 | 'django.contrib.auth.backends.ModelBackend', 83 | ) 84 | ``` 85 | 86 | Also look [optional settings](#settings) avaliable. 87 | 88 | 3. Make sure everything is up do date 89 | 90 | ```bash 91 | python manage.py migrate 92 | ``` 93 | 94 | 95 | 4. Include rest social urls (choose at least one) 96 | 97 | 4.1 [session authentication](http://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication) 98 | 99 | ```python 100 | path('api/login/', include('rest_social_auth.urls_session')), 101 | ``` 102 | 103 | 4.2 [token authentication](http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication) 104 | 105 | ```python 106 | path('api/login/', include('rest_social_auth.urls_token')), 107 | ``` 108 | 109 | 4.3 [jwt authentication](https://github.com/davesque/django-rest-framework-simplejwt) 110 | 111 | ```python 112 | path('api/login/', include('rest_social_auth.urls_jwt_pair')), 113 | ``` 114 | 115 | or / and 116 | 117 | ```python 118 | path('api/login/', include('rest_social_auth.urls_jwt_sliding')), 119 | ``` 120 | 121 | 4.4 [knox authentication](https://github.com/James1345/django-rest-knox/) 122 | 123 | ```python 124 | path('api/login/', include('rest_social_auth.urls_knox')), 125 | ``` 126 | 127 | 5. You are ready to login users 128 | 129 | Following examples are for OAuth 2.0. 130 | 131 | 5.1 session authentication 132 | 133 | - POST /api/login/social/session/ 134 | 135 | input: 136 | 137 | { 138 | "provider": "facebook", 139 | "code": "AQBPBBTjbdnehj51" 140 | } 141 | 142 | output: 143 | 144 | { 145 | "username": "Alex", 146 | "email": "user@email.com", 147 | // other user data 148 | } 149 | 150 | + session id in cookies 151 | 152 | 5.2 token authentication 153 | 154 | - POST /api/login/social/token/ 155 | 156 | input: 157 | 158 | { 159 | "provider": "facebook", 160 | "code": "AQBPBBTjbdnehj51" 161 | } 162 | 163 | output: 164 | 165 | { 166 | "token": "68ded41d89f6a28da050f882998b2ea1decebbe0" 167 | } 168 | 169 | - POST /api/login/social/token_user/ 170 | 171 | input: 172 | 173 | { 174 | "provider": "facebook", 175 | "code": "AQBPBBTjbdnehj51" 176 | } 177 | 178 | output: 179 | 180 | { 181 | "username": "Alex", 182 | "email": "user@email.com", 183 | // other user data 184 | "token": "68ded41d89f6a28da050f882998b2ea1decebbe0" 185 | } 186 | 187 | 5.3 jwt authentication (using [django-rest-framework-simplejwt](https://github.com/davesque/django-rest-framework-simplejwt)) 188 | 189 | - POST /api/login/social/jwt-pair/ 190 | - POST /api/login/social/jwt-pair-user/ 191 | 192 | Similar to token authentication, but token is JSON Web Token. 193 | 194 | See [JWT.io](http://jwt.io/) for details. 195 | 196 | To use it, django-rest-framework-simplejwt must be installed. 197 | 198 | For `jwt-pair`, the response will include additional "refresh" token: 199 | ```json 200 | { 201 | "token": "...", 202 | "refresh": "..." 203 | } 204 | ``` 205 | 206 | ##### Or sliding JWT token: 207 | 208 | - POST /api/login/social/jwt-sliding/ 209 | - POST /api/login/social/jwt-sliding-user/ 210 | 211 | Check [docs of simplejwt](https://github.com/davesque/django-rest-framework-simplejwt#token-types) for pair/sliding token difference. 212 | 213 | Note. `django-rest-framework-simplejwt` doesn't work on python3.6 214 | 215 | 5.4 knox authentication 216 | 217 | - POST /api/login/social/knox/ 218 | - POST /api/login/social/knox_user/ 219 | 220 | Similar to token authentication, but token is Django Rest Knox Token. 221 | 222 | To use it, [django-rest-knox](https://github.com/James1345/django-rest-knox/) must be installed. 223 | 224 | 225 | User model is taken from [`settings.AUTH_USER_MODEL`](https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model). 226 | 227 | At input there is also non-required field `redirect_uri`. 228 | If given, server will use this redirect uri in requests, instead of uri 229 | got from settings. 230 | This redirect_uri must be equal in front-end request and in back-end request. 231 | Back-end will not do any redirect in fact. 232 | 233 | It is also possible to specify provider in URL, not in request body. 234 | Just append it to the url: 235 | 236 | POST /api/login/social/session/facebook/ 237 | 238 | Don't need to specify it in body now: 239 | 240 | { 241 | "code": "AQBPBBTjbdnehj51" 242 | } 243 | 244 | Provider defined in URL has higher priority than provider in body. 245 | If both are specified - provider will be taken from URL. 246 | 247 | 248 | OAuth 2.0 workflow with rest-social-auth 249 | ----------------------------------------- 250 | 1. Front-end need to know following params for each social provider: 251 | - client_id _# only in case of OAuth 2.0, id of registered application on social service provider_ 252 | - redirect_uri _# to this url social provider will redirect with code_ 253 | - scope=your_scope _# for example email_ 254 | - response_type=code _# same for all oauth2.0 providers_ 255 | 256 | 2. Front-end redirect user to social authorize url with params from previous point. 257 | 258 | 3. User confirms. 259 | 260 | 4. Social provider redirects back to `redirect_uri` with param `code`. 261 | 262 | 5. Front-end now ready to login the user. To do it, send POST request with provider name and code: 263 | 264 | POST /api/login/social/session/ 265 | 266 | with data (form data or json) 267 | 268 | provider=facebook&code=AQBPBBTjbdnehj51 269 | 270 | Backend will either signin the user, either signup, either return error. 271 | 272 | Sometimes it is more suitable to specify provider in url, not in request body. 273 | It is possible, rest-social-auth will understand that. 274 | Following request is the same as above: 275 | 276 | POST /api/login/social/session/facebook/ 277 | 278 | with data (form data or json) 279 | 280 | code=AQBPBBTjbdnehj51 281 | 282 | 283 | OAuth 1.0a workflow with rest-social-auth 284 | ----------------------------------------- 285 | 1. Front-end needs to make a POST request to your backend with the provider name ONLY: 286 | 287 | POST /api/login/social/ 288 | 289 | with data (form data or json): 290 | 291 | provider=twitter 292 | 293 | Or specify provider in url, in that case data will be empty: 294 | 295 | POST /api/login/social/twitter 296 | 297 | 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. 298 | 299 | 3. User confirms. In the case of Twitter, they will then return the following data to your front-end: 300 | 301 | { 302 | "redirect_state": "...bHrz2x0wy43", 303 | "oauth_token" : "...AAAAAAAhD5u", 304 | "oauth_verifier": "...wDBdTR7CYdR" 305 | } 306 | 307 | 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: 308 | 309 | POST /api/login/social/ 310 | 311 | with data (form data or json) 312 | 313 | provider=twitter&oauth_token=AQBPBBTjbdnehj51&oauth_verifier=wDBdTR7CYdR 314 | 315 | Backend will either signin the user, or signup, or return an error. 316 | Same as in OAuth 2.0, you can specify provider in url, not in body: 317 | 318 | POST /api/login/social/twitter 319 | 320 | 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. 321 | 322 | 323 | rest-social-auth purpose 324 | ------------------------ 325 | 326 | As we can see, our backend must implement resource for signin the user. 327 | 328 | Django REST social auth provides means to easily implement such resource. 329 | 330 | 331 | List of oauth providers 332 | ----------------------- 333 | 334 | OAuth 1.0 and OAuth 2.0 providers are supported. 335 | 336 | Look [python-social-auth](http://python-social-auth.readthedocs.io/en/latest/backends/index.html#social-backends) for full list. 337 | Name of provider is taken from corresponding `backend.name` property of 338 | particular backed class in python-social-auth. 339 | 340 | For example for [facebook backend](https://github.com/python-social-auth/social-core/blob/master/social_core/backends/facebook.py#L22) 341 | we see: 342 | 343 | class FacebookOAuth2(BaseOAuth2): 344 | name = 'facebook' 345 | 346 | Here are some provider names: 347 | 348 | Provider | provider name 349 | --------- | ------------- 350 | Facebook | facebook 351 | Google | google-oauth2 352 | Vkontakte | vk-oauth2 353 | Instagram | instagram 354 | Github | github 355 | Yandex | yandex-oauth2 356 | Twitter | twitter 357 | [Others](http://python-social-auth.readthedocs.io/en/latest/backends/index.html#social-backends) | [...](http://python-social-auth.readthedocs.io/en/latest/backends/index.html#social-backends) 358 | 359 | 360 | Settings 361 | -------- 362 | 363 | - `REST_SOCIAL_OAUTH_REDIRECT_URI` 364 | 365 | Default: `'/'` 366 | 367 | 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. 368 | 369 | To override the relative path (url path or url name are both supported): 370 | 371 | REST_SOCIAL_OAUTH_REDIRECT_URI = '/oauth/redirect/path/' 372 | # or url name 373 | REST_SOCIAL_OAUTH_REDIRECT_URI = 'redirect_url_name' 374 | 375 | Note, in case of url name, backend name will be provided to url resolver as argument. 376 | 377 | - `REST_SOCIAL_DOMAIN_FROM_ORIGIN` 378 | 379 | Default: `True` 380 | 381 | Sometimes front-end and back-end are run on different domains. 382 | For example frontend at 'myproject.com', and backend at 'api.myproject.com'. 383 | 384 | If True, domain will be taken from request origin, if origin is defined. 385 | So in current example domain will be 'myproject.com', not 'api.myproject.com'. 386 | Next, this domain will be joined with path from `REST_SOCIAL_OAUTH_REDIRECT_URI` settings. 387 | 388 | To be clear, suppose we have following settings (defaults): 389 | 390 | REST_SOCIAL_OAUTH_REDIRECT_URI = '/' 391 | REST_SOCIAL_DOMAIN_FROM_ORIGIN = True 392 | 393 | Front-end is running on domain 'myproject.com', back-end - on 'api.myproject.com'. 394 | Back-end will use following redirect_uri: 395 | 396 | myproject.com/ 397 | 398 | And with following settings: 399 | 400 | REST_SOCIAL_OAUTH_REDIRECT_URI = '/' 401 | REST_SOCIAL_DOMAIN_FROM_ORIGIN = False 402 | 403 | redirect_uri will be: 404 | 405 | api.myproject.com/ 406 | 407 | Also look at [django-cors-headers](https://github.com/ottoyiu/django-cors-headers) if such architecture is your case. 408 | 409 | - `REST_SOCIAL_OAUTH_ABSOLUTE_REDIRECT_URI` 410 | 411 | Default: `None` 412 | 413 | Full redirect uri (domain and path) can be hardcoded 414 | 415 | REST_SOCIAL_OAUTH_ABSOLUTE_REDIRECT_URI = 'http://myproject.com/' 416 | 417 | This settings has higher priority than `REST_SOCIAL_OAUTH_REDIRECT_URI` and `REST_SOCIAL_DOMAIN_FROM_ORIGIN`. 418 | I.e. if this settings is defined, other will be ignored. 419 | But `redirect_uri` param from request has higher priority than any setting. 420 | 421 | - `REST_SOCIAL_LOG_AUTH_EXCEPTIONS` 422 | 423 | Default: `True` 424 | 425 | When `False` will not log social auth authentication exceptions. 426 | 427 | - `REST_SOCIAL_VERBOSE_ERRORS` 428 | 429 | Default: `False` 430 | 431 | When `True` the API will return error message received from server. Can be potentially unsecure to turn it ON. 432 | 433 | - `SOCIAL_AUTH_STRATEGY` 434 | 435 | Default: `'rest_social_auth.strategy.DRFStrategy'` 436 | 437 | Override [strategy](https://python-social-auth.readthedocs.io/en/latest/strategies.html) for python-social-auth. 438 | 439 | 440 | Customization 441 | ------------- 442 | 443 | First of all, customization provided by python-social-auth is also avaliable. 444 | For example, use nice mechanism of [pipeline](http://python-social-auth.readthedocs.io/en/latest/pipeline.html) to do any action you need during login/signin. 445 | 446 | Second, you can override any method from current package. 447 | Specify serializer for each view by subclassing the view. 448 | 449 | To do it 450 | 451 | - define your own url: 452 | 453 | path('api/login/social/$', MySocialView.as_view(), name='social_login'), 454 | 455 | - define your serializer 456 | 457 | from rest_framework import serializers 458 | from django.contrib.auth import get_user_model 459 | 460 | class MyUserSerializer(serializers.ModelSerializer): 461 | 462 | class Meta: 463 | model = get_user_model() 464 | exclude = ('password', 'user_permissions', 'groups') 465 | 466 | - define view 467 | 468 | from rest_social_auth.views import SocialSessionAuthView 469 | from .serializers import MyUserSerializer 470 | 471 | class MySocialView(SocialSessionAuthView): 472 | serializer_class = MyUserSerializer 473 | 474 | Check the code of the lib, there is not much of it. 475 | 476 | 477 | Example 478 | ------- 479 | 480 | There is an [example project](https://github.com/st4lk/django-rest-social-auth/tree/master/example_project). 481 | 482 | - clone repo 483 | 484 | ```bash 485 | git clone https://github.com/st4lk/django-rest-social-auth.git 486 | ``` 487 | 488 | - run example project 489 | 490 | ```bash 491 | make 492 | ``` 493 | 494 | It is assumed, that you have: 495 | - [make](https://www.gnu.org/software/make/) 496 | - [docker](https://www.docker.com/) 497 | 498 | - open [https://127.0.0.1:8000/](https://127.0.0.1:8000/) in your browser 499 | 500 | Note: `runserver_plus` is used instead of built-in `runserver` to serve the project with TLS (aka SSL) certificate. 501 | HTTPS is required by some social providers (facebook), without it they won't work. 502 | The certificate will not be trusted by your system - that is expected. 503 | Just tell your browser to proceed with this untrusted certificate - it is acceptable for development purposes. 504 | 505 | In Chrome browser it can look like this: 506 | - step 1: [image](https://user-images.githubusercontent.com/1771042/71641969-90ab7280-2cd6-11ea-95ed-de4c6e123345.png) 507 | - step 2: [image](https://user-images.githubusercontent.com/1771042/71641970-91440900-2cd6-11ea-9275-f6c351ddc543.png) 508 | 509 | More details [here](https://github.com/teddziuba/django-sslserver#browser-certificate-errors). 510 | 511 | Facebook, Google and Twitter auth should work, all secret keys are already set. 512 | 513 | Example project uses [satellizer](https://github.com/sahat/satellizer) angularjs module. 514 | 515 | Development 516 | ----------- 517 | 518 | Run tests locally 519 | 520 | ```bash 521 | make test 522 | ``` 523 | 524 | Run tests in all enviroments (can take some time) 525 | 526 | ```bash 527 | make test-tox 528 | ``` 529 | 530 | Contributors 531 | ------------ 532 | 533 | Thanks to all contributors! 534 | 535 | 536 | 537 | 538 | 539 | - Alexey Evseev, [st4lk](https://github.com/st4lk) 540 | - James Keys, [skolsuper](https://github.com/skolsuper) 541 | - Aaron Abbott, [aabmass](https://github.com/aabmass) 542 | - Grigorii Eremeev, [Budulianin](https://github.com/Budulianin) 543 | - shubham, [shubh3794](https://github.com/shubh3794) 544 | - Deshraj Yadav, [DESHRAJ](https://github.com/DESHRAJ) 545 | - georgewhewell, [georgewhewell](https://github.com/georgewhewell) 546 | - Ahmed Sa3d, [zee93](https://github.com/zee93) 547 | - Olle Vidner, [ovidner](https://github.com/ovidner) 548 | - MounirMesselmeni, [MounirMesselmeni](https://github.com/MounirMesselmeni) 549 | - Tuomas Virtanen, [katajakasa](https://github.com/katajakasa) 550 | - Jeremy Storer, [storerjeremy](https://github.com/storerjeremy) 551 | - Jeffrey de Lange, [jgadelange](https://github.com/jgadelange) 552 | - John Vandenberg, [jayvdb](https://github.com/jayvdb) 553 | - Anton_Datsik, [AntonDatsik](https://github.com/AntonDatsik) 554 | - Netizen29, [Netizen29](https://github.com/Netizen29) 555 | - Dipendra Raj Panta, [Mr-DRP](https://github.com/Mr-DRP) 556 | - JD, [jd-0001](https://github.com/jd-0001) 557 | - Harsh Patel, [eelectronn](https://github.com/eelectronn) 558 | - Devid, [sevdog](https://github.com/sevdog) 559 | - Anton Yakovlev, [sputnik5459](https://github.com/sputnik5459) 560 | - Olivér Kecskeméty, [KOliver94](https://github.com/KOliver94) 561 | - vainu-arto, [vainu-arto](https://github.com/vainu-arto) 562 | - Maxim De Clercq, [maximdeclercq](https://github.com/maximdeclercq) 563 | - alicegilli, [alicegilli](https://github.com/alicegilli) 564 | - Jourdan Rodrigues, [jourdanrodrigues](https://github.com/jourdanrodrigues) 565 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | rest_social_auth release notes 2 | ============================== 3 | 4 | v9.0.0 5 | ------ 6 | - Add support of Django 5.2 7 | - Drop support of Django 3.2, 4.1 8 | - Drop support of Python 3.7, 3.8 9 | - Updated minimal required social-auth versions: 10 | * social-auth-core (>=4.6.1, <5.0) 11 | * social-auth-app-django (>=5.4.3, <6.0) 12 | 13 | Issues: #180 14 | 15 | v8.4.0 16 | ------ 17 | - Add support of Django 5.1 18 | 19 | v8.3.0 20 | ------ 21 | - Pass request to authentication backend https://github.com/st4lk/django-rest-social-auth/pull/175 22 | 23 | v8.2.0 24 | ------ 25 | - Add support of Django 5.0 26 | - Add support of Python 3.12 27 | 28 | v8.1.0 29 | ------ 30 | - Add support of Django 4.2 31 | - Add support of Python 3.11 32 | 33 | v8.0.0 34 | ------ 35 | - Drop support of Python 3.6 36 | - Drop support of Django 3.0, 3.1 37 | - Add support of Django 4.1 38 | - Add support of Python 3.10 39 | - Allow customized implementations of djangorestframework-simplejwt token class 40 | - Add `REST_SOCIAL_VERBOSE_ERRORS` setting 41 | 42 | v7.0.0 43 | ------ 44 | - Add support of social-auth-app-django==5.x 45 | - Add support of Django 3.2 46 | - Drop support of social-auth-app-django<5.0 47 | 48 | v6.0.1 49 | ------ 50 | - Fix processing error from social provider #144 51 | 52 | v6.0.0 53 | ------ 54 | - Add support of social-auth-core==4.0 55 | - Add support of social-auth-app-django==4.0 56 | - Drop support of Python 3.5 57 | - Drop support of social-auth-core<4.0 58 | - Drop support of social-auth-app-django<4.0 59 | 60 | v5.0.1 61 | ------ 62 | - Expect error without response during error handling 63 | 64 | v5.0.0 65 | ------ 66 | - Update user serializer: exclude field only if it was defined in customer user model 67 | - Include error message from social provider into HTTP response of API 68 | - Use `path()` django function instead of deprecated `url()` 69 | - Drop support of Django 1.11 70 | - Drop support of Python 2.7 71 | - Drop support of deprecated `djangorestframework-jwt` dependency 72 | - Add support of Django 3.1 73 | 74 | Issues: #137 75 | 76 | v4.2.0 77 | ------ 78 | - Take provider name from URL first 79 | 80 | Issues: #121 81 | 82 | v4.1.0 83 | ------ 84 | - Allow to specify custom strategy for python-social-auth 85 | 86 | v4.0.0 87 | ------ 88 | - Update supported version of django-rest-knox (>=4.0.0, <5.0.0). v4 has breaking changes. 89 | - Allow to use token auth with OAuth1 without django session enabled 90 | 91 | Issues: #110 92 | 93 | v3.0.0 94 | ------ 95 | - Add Django 3.0 support 96 | - Add Python 3.8 support 97 | - Drop Django 2.0, 2.1 support 98 | - Fix facebook integration in example project: serve with fake TLS certificate 99 | 100 | Issues: #104, #106 101 | 102 | v2.2.0 103 | ------ 104 | - Update license, use MIT 105 | - Add Django 2.2 support 106 | - Fix customer redirect URI for OAuth1 107 | - Cleanup knox integration 108 | 109 | v2.1.0 110 | ------ 111 | - Use djangorestframework-simplejwt for JWT implementation 112 | - Deprecate djangorestframework-jwt 113 | - Add python3.7 support 114 | 115 | Issues: #74 116 | 117 | v2.0.2 118 | ------ 119 | - Make social-auth-core >=3.0 as mandatory dependency 120 | 121 | v2.0.1 122 | ------ 123 | - Minor update of pypi deployment process 124 | 125 | v2.0.0 126 | ------ 127 | - Update social-auth-core dependency to at least 3.0.0 128 | 129 | Issues: #73 130 | 131 | v1.5.0 132 | ------ 133 | - Update minimal required version of social-auth-app-django to 3.1.0 134 | - Minor updates in readme 135 | - Add Django 2.1 support 136 | - Drop Django 1.10 support 137 | - Drop Python 3.4 support 138 | 139 | Issues: #70 140 | 141 | v1.4.0 142 | ------ 143 | - Add django-rest-knox support 144 | 145 | v1.3.1 146 | ------ 147 | - Fix Django 2.0 support 148 | 149 | v1.3.0 150 | ------ 151 | - Add Django 2.0 support 152 | - Drop Django 1.8, 1.9 support 153 | 154 | Issues: #58 155 | 156 | v1.2.0 157 | ------ 158 | - Add Python 3.6 and Django 1.11 support 159 | 160 | Issues #54 161 | 162 | v1.1.0 163 | ------ 164 | - Update docs 165 | - Add new setting `REST_SOCIAL_LOG_AUTH_EXCEPTIONS` 166 | 167 | Issues #42 168 | 169 | v1.0.0 170 | ------ 171 | - Add Django 1.10 support 172 | - Drop Django 1.7 support 173 | - Add social-auth-core, social-auth-app-django dependencies 174 | - Drop python-social-auth dependency 175 | 176 | Issues: #33, #35, #37, #38 177 | 178 | v0.5.0 179 | ------ 180 | - Handle HttpResponses returned by the pipeline 181 | 182 | Issues: #28 183 | 184 | v0.4.4 185 | ------ 186 | - Log exceptions from python-social-auth 187 | - Don't use find_packages from setuptools 188 | 189 | Issues: #22, #25 190 | 191 | v0.4.3 192 | ------ 193 | - Fix queryset assert error 194 | - minor typo fixes 195 | 196 | Issues: #20 197 | 198 | v0.4.2 199 | ------ 200 | - Remove django.conf.urls.patterns from code 201 | - Exclude modifing immutable data 202 | - refactor tests 203 | - minor typo fixes 204 | 205 | Issues: #11, #17, #14 206 | 207 | v0.4.1 208 | ------ 209 | - Fix requirements.txt: allow django==1.9 210 | 211 | v0.4.0 212 | ------ 213 | - Add [JSON Web Tokens](http://jwt.io/) using [djangorestframework-jwt](https://github.com/GetBlimp/django-rest-framework-jwt) 214 | - Add Python 3.5 and Django 1.9 support 215 | 216 | Issues: #6 217 | 218 | v0.3.1 219 | ------ 220 | - Explicitly set token authentication for token views 221 | 222 | v0.3.0 223 | ------ 224 | - Add support for Oauth1 225 | - Add ability to override request parsing 226 | - Allow to specify provider in url 227 | - Drop Python 2.6 and Django 1.6 support 228 | 229 | Issues: #2, #3, #5 230 | 231 | v0.2.0 232 | ------ 233 | - Get domain from HTTP Origin 234 | - Add example of Google OAuth2.0 235 | - Add manual redirect uri (front-end can specify it) 236 | - Use GenericAPIView instead of APIView 237 | - Main serializer is output serializer, not input 238 | - Update docs 239 | - Minor code fixes 240 | 241 | v0.1.0 242 | ------ 243 | 244 | First version in pypi 245 | -------------------------------------------------------------------------------- /example_project/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st4lk/django-rest-social-auth/9a9bfd0279ed4b7515da1ad086e5972c5d42de69/example_project/config/__init__.py -------------------------------------------------------------------------------- /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 | import os 14 | import django 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = '5i830%k0$u*+@0290eq&lb!c%h3cxknj01ygyck-@el-5__y4y' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ['*'] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = ( 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 42 | 'rest_framework', 43 | 'rest_framework.authtoken', 44 | 'social_django', 45 | 'rest_social_auth', 46 | 'knox', # For django-rest-knox 47 | 'django_extensions', # some social providers require https 48 | 49 | 'users', 50 | ) 51 | 52 | MIDDLEWARE = ( 53 | 'django.contrib.sessions.middleware.SessionMiddleware', 54 | 'django.middleware.common.CommonMiddleware', 55 | 'django.middleware.csrf.CsrfViewMiddleware', 56 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 57 | 'django.contrib.messages.middleware.MessageMiddleware', 58 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 59 | ) 60 | 61 | ROOT_URLCONF = 'config.urls' 62 | 63 | 64 | TEMPLATES = [ 65 | { 66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 67 | 'DIRS': [ 68 | os.path.join(BASE_DIR, 'templates'), 69 | # insert your TEMPLATE_DIRS here 70 | ], 71 | 'APP_DIRS': True, 72 | 'OPTIONS': { 73 | 'context_processors': [ 74 | # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this 75 | # list if you haven't customized them: 76 | 'django.contrib.auth.context_processors.auth', 77 | 'django.template.context_processors.debug', 78 | 'django.template.context_processors.i18n', 79 | 'django.template.context_processors.media', 80 | 'django.template.context_processors.static', 81 | 'django.template.context_processors.tz', 82 | 'django.contrib.messages.context_processors.messages', 83 | 'users.context_processors.social_app_keys', 84 | ], 85 | }, 86 | }, 87 | ] 88 | WSGI_APPLICATION = 'config.wsgi.application' 89 | 90 | 91 | # Database 92 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 93 | 94 | DATABASES = { 95 | 'default': { 96 | 'ENGINE': 'django.db.backends.sqlite3', 97 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 98 | } 99 | } 100 | 101 | 102 | # Internationalization 103 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 104 | 105 | LANGUAGE_CODE = 'en-us' 106 | 107 | TIME_ZONE = 'UTC' 108 | 109 | USE_I18N = True 110 | 111 | if django.VERSION < (4, 0): 112 | USE_L10N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 119 | 120 | STATIC_URL = '/static/' 121 | 122 | STATICFILES_DIRS = ( 123 | os.path.join(BASE_DIR, 'static'), 124 | ) 125 | 126 | AUTH_USER_MODEL = 'users.CustomUser' 127 | 128 | # DRF settings 129 | 130 | SIMPLE_JWT = { 131 | 'AUTH_TOKEN_CLASSES': ( 132 | 'rest_framework_simplejwt.tokens.AccessToken', 133 | 'rest_framework_simplejwt.tokens.SlidingToken', 134 | ), 135 | } 136 | 137 | # social auth settings 138 | # valid redirect domain for all apps: http://restsocialexample.com:8000/ 139 | SOCIAL_AUTH_FACEBOOK_KEY = '295137440610143' 140 | SOCIAL_AUTH_FACEBOOK_SECRET = '4b4aef291799a7b9aaf016689339e97f' 141 | SOCIAL_AUTH_FACEBOOK_SCOPE = ['email', ] 142 | SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { 143 | 'fields': ','.join([ 144 | # public_profile 145 | 'id', 'cover', 'name', 'first_name', 'last_name', 'age_range', 'link', 146 | 'gender', 'locale', 'picture', 'timezone', 'updated_time', 'verified', 147 | # extra fields 148 | 'email', 149 | ]), 150 | } 151 | 152 | SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ( 153 | '976099811367-ihbmg1pfnniln9qgfacleiu41bhl3fqn.apps.googleusercontent.com' 154 | ) 155 | SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'JaiLLvY1BK97TSy5_xcGWDhp' 156 | SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['email', ] 157 | 158 | SOCIAL_AUTH_TWITTER_KEY = 'gCrpEpNxpWkAE6Cul98OzAWTk' 159 | SOCIAL_AUTH_TWITTER_SECRET = '7SYSRpYY4amW5kiNXUAxUDdWS7G3nHytRIGHbDVTByzBfsqDJl' 160 | 161 | 162 | AUTHENTICATION_BACKENDS = ( 163 | 'social_core.backends.facebook.FacebookOAuth2', 164 | 'social_core.backends.google.GoogleOAuth2', 165 | 'social_core.backends.twitter.TwitterOAuth', # OAuth1.0 166 | 'django.contrib.auth.backends.ModelBackend', 167 | ) 168 | 169 | SOCIAL_AUTH_PIPELINE = ( 170 | 'users.social_pipeline.auto_logout', # custom action 171 | 'social_core.pipeline.social_auth.social_details', 172 | 'social_core.pipeline.social_auth.social_uid', 173 | 'social_core.pipeline.social_auth.auth_allowed', 174 | 'users.social_pipeline.check_for_email', # custom action 175 | 'social_core.pipeline.social_auth.social_user', 176 | 'social_core.pipeline.user.get_username', 177 | 'social_core.pipeline.user.create_user', 178 | 'social_core.pipeline.social_auth.associate_user', 179 | 'social_core.pipeline.social_auth.load_extra_data', 180 | 'social_core.pipeline.user.user_details', 181 | 'users.social_pipeline.save_avatar', # custom action 182 | ) 183 | 184 | LOGGING = { 185 | 'version': 1, 186 | 'disable_existing_loggers': False, 187 | 'formatters': { 188 | 'verbose': { 189 | 'format': '%(levelname)s:%(name)s: %(message)s ' 190 | '(%(asctime)s; %(filename)s:%(lineno)d)', 191 | 'datefmt': "%Y-%m-%d %H:%M:%S", 192 | }, 193 | }, 194 | 'handlers': { 195 | 'console': { 196 | 'level': 'DEBUG', 197 | 'class': 'logging.StreamHandler', 198 | 'formatter': 'verbose', 199 | }, 200 | }, 201 | 'loggers': { 202 | 'rest_social_auth': { 203 | 'handlers': ['console', ], 204 | 'level': "DEBUG", 205 | }, 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /example_project/config/settings_test.py: -------------------------------------------------------------------------------- 1 | " Settings for tests. " 2 | INSTALLED_APPS = [] 3 | 4 | from .settings import * # NOQA: E402, F401, F403 5 | 6 | # Databases 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.sqlite3', 10 | 'NAME': ':memory:', 11 | } 12 | } 13 | 14 | if 'knox' not in INSTALLED_APPS: 15 | INSTALLED_APPS += ('knox', ) 16 | -------------------------------------------------------------------------------- /example_project/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, re_path 3 | 4 | from users import views 5 | 6 | urlpatterns = [ 7 | re_path(r'^$', views.HomeSessionView.as_view(), name='home'), 8 | re_path(r'^session/$', views.HomeSessionView.as_view(), name='home_session'), 9 | re_path(r'^token/$', views.HomeTokenView.as_view(), name='home_token'), 10 | re_path(r'^jwt/$', views.HomeJWTView.as_view(), name='home_jwt'), 11 | re_path(r'^knox/$', views.HomeKnoxView.as_view(), name='home_knox'), 12 | 13 | re_path(r'^api/login/', include('rest_social_auth.urls_session')), 14 | re_path(r'^api/login/', include('rest_social_auth.urls_token')), 15 | re_path(r'^api/login/', include('rest_social_auth.urls_jwt_pair')), 16 | re_path(r'^api/login/', include('rest_social_auth.urls_jwt_sliding')), 17 | re_path(r'^api/login/', include('rest_social_auth.urls_knox')), 18 | 19 | re_path(r'^api/logout/session/$', views.LogoutSessionView.as_view(), name='logout_session'), 20 | re_path(r'^api/user/session/', 21 | views.UserSessionDetailView.as_view(), 22 | name="current_user_session"), 23 | re_path(r'^api/user/token/', views.UserTokenDetailView.as_view(), name="current_user_token"), 24 | re_path(r'^api/user/jwt/', views.UserJWTDetailView.as_view(), name="current_user_jwt"), 25 | re_path(r'^api/user/knox/', views.UserKnoxDetailView.as_view(), name="current_user_knox"), 26 | 27 | re_path(r'^admin/', admin.site.urls), 28 | ] 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/static/anonymous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st4lk/django-rest-social-auth/9a9bfd0279ed4b7515da1ad086e5972c5d42de69/example_project/static/anonymous.png -------------------------------------------------------------------------------- /example_project/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Django-rest-framework and OAuth example

8 | 14 | 15 | {% block content %} 16 | 17 | {% endblock content %} 18 | 19 | {% block scripts %} 20 | 21 | 22 | 23 | {% endblock scripts %} 24 | 25 | 26 | -------------------------------------------------------------------------------- /example_project/templates/home_jwt.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |

Note! django-rest-framework-simplejwt must be installed for this method

6 |
7 |
8 |
9 |

rest_framework_simplejwt.authentication.JWTAuthentication

10 |
11 |
12 |
13 |
14 |
15 |
16 | (**) In this example pair token is used. With simplejwt you can use sliding token as well. 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 | -------------------------------------------------------------------------------- /example_project/templates/home_knox.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |

Note! django-rest-knox must be installed for this method

6 |
7 |
8 |
9 |

knox.auth.TokenAuthentication

10 |
11 |
12 |
13 |
14 |
15 |
16 |

Knox user data

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

Raw knox payload

35 |
36 |
{% verbatim %}{{ ctrl.knoxPayload | json }}{% endverbatim %}
37 |
38 |
39 |
40 |
41 | 42 | {% endblock content %} 43 | 44 | {% block scripts %} 45 | {{ block.super }} 46 | 118 | {% endblock scripts %} 119 | -------------------------------------------------------------------------------- /example_project/templates/home_session.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 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/templates/home_token.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

rest_framework.authentication.TokenAuthentication

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

Token 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 | 37 | {% endblock content %} 38 | 39 | {% block scripts %} 40 | {{ block.super }} 41 | 103 | {% endblock scripts %} 104 | -------------------------------------------------------------------------------- /example_project/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st4lk/django-rest-social-auth/9a9bfd0279ed4b7515da1ad086e5972c5d42de69/example_project/users/__init__.py -------------------------------------------------------------------------------- /example_project/users/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def social_app_keys(request): 5 | return { 6 | 'facebook_key': settings.SOCIAL_AUTH_FACEBOOK_KEY, 7 | 'googleoauth2_key': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, 8 | } 9 | -------------------------------------------------------------------------------- /example_project/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import models, migrations 2 | import django.utils.timezone 3 | import django.core.validators 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('auth', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='CustomUser', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('password', models.CharField(max_length=128, verbose_name='password')), 18 | ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login', null=True, blank=True)), 19 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 20 | ('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')])), 21 | ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)), 22 | ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)), 23 | ('email', models.EmailField(max_length=75, verbose_name='email address', blank=True)), 24 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 25 | ('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')), 26 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 27 | ('social_thumb', models.URLField(null=True, blank=True)), 28 | ('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')), 29 | ('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')), 30 | ], 31 | options={ 32 | 'abstract': False, 33 | 'verbose_name': 'user', 34 | 'verbose_name_plural': 'users', 35 | }, 36 | bases=(models.Model,), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /example_project/users/migrations/0002_auto_20180415_0235.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.4 on 2018-04-15 02:35 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('users', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelManagers( 16 | name='customuser', 17 | managers=[ 18 | ('objects', django.contrib.auth.models.UserManager()), 19 | ], 20 | ), 21 | migrations.AlterField( 22 | model_name='customuser', 23 | name='email', 24 | field=models.EmailField(blank=True, max_length=254, verbose_name='email address'), 25 | ), 26 | migrations.AlterField( 27 | model_name='customuser', 28 | name='groups', 29 | field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), 30 | ), 31 | migrations.AlterField( 32 | model_name='customuser', 33 | name='last_login', 34 | field=models.DateTimeField(blank=True, null=True, verbose_name='last login'), 35 | ), 36 | migrations.AlterField( 37 | model_name='customuser', 38 | name='username', 39 | field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username'), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /example_project/users/migrations/0003_auto_20190305_0001.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2019-03-05 00:01 2 | 3 | import django.contrib.auth.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('users', '0002_auto_20180415_0235'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='customuser', 16 | name='last_name', 17 | field=models.CharField(blank=True, max_length=150, verbose_name='last name'), 18 | ), 19 | migrations.AlterField( 20 | model_name='customuser', 21 | name='username', 22 | field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /example_project/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st4lk/django-rest-social-auth/9a9bfd0279ed4b7515da1ad086e5972c5d42de69/example_project/users/migrations/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example_project/users/social_pipeline.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from rest_framework.response import Response 3 | 4 | 5 | def auto_logout(*args, **kwargs): 6 | """Do not compare current user with new one""" 7 | return {'user': None} 8 | 9 | 10 | def save_avatar(strategy, details, user=None, *args, **kwargs): 11 | """Get user avatar from social provider.""" 12 | if user: 13 | backend_name = kwargs['backend'].__class__.__name__.lower() 14 | response = kwargs.get('response', {}) 15 | social_thumb = None 16 | if 'facebook' in backend_name: 17 | if 'id' in response: 18 | social_thumb = ( 19 | 'http://graph.facebook.com/{0}/picture?type=normal' 20 | ).format(response['id']) 21 | elif 'twitter' in backend_name and response.get('profile_image_url'): 22 | social_thumb = response['profile_image_url'] 23 | elif 'googleoauth2' in backend_name and response.get('image', {}).get('url'): 24 | social_thumb = response['image']['url'].split('?')[0] 25 | else: 26 | social_thumb = 'http://www.gravatar.com/avatar/' 27 | social_thumb += hashlib.md5(user.email.lower().encode('utf8')).hexdigest() 28 | social_thumb += '?size=100' 29 | if social_thumb and user.social_thumb != social_thumb: 30 | user.social_thumb = social_thumb 31 | strategy.storage.user.changed(user) 32 | 33 | 34 | def check_for_email(backend, uid, user=None, *args, **kwargs): 35 | if not kwargs['details'].get('email'): 36 | return Response({'error': "Email wasn't provided by oauth provider"}, status=400) 37 | -------------------------------------------------------------------------------- /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 KnoxAuthMixin, SimpleJWTAuthMixin 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().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 HomeKnoxView(TemplateView): 31 | template_name = 'home_knox.html' 32 | 33 | 34 | class LogoutSessionView(APIView): 35 | 36 | def post(self, request, *args, **kwargs): 37 | logout(request) 38 | return Response(status=status.HTTP_204_NO_CONTENT) 39 | 40 | 41 | class BaseDetailView(generics.RetrieveAPIView): 42 | permission_classes = IsAuthenticated, 43 | serializer_class = UserSerializer 44 | model = get_user_model() 45 | 46 | def get_object(self, queryset=None): 47 | return self.request.user 48 | 49 | 50 | class UserSessionDetailView(BaseDetailView): 51 | authentication_classes = (SessionAuthentication, ) 52 | 53 | 54 | class UserTokenDetailView(BaseDetailView): 55 | authentication_classes = (TokenAuthentication, ) 56 | 57 | 58 | class UserJWTDetailView(SimpleJWTAuthMixin, BaseDetailView): 59 | pass 60 | 61 | 62 | class UserKnoxDetailView(KnoxAuthMixin, BaseDetailView): 63 | pass 64 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = config.settings_test 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=4.2 2 | djangorestframework<4.0 3 | social-auth-core>=4.6.1,<5.0 4 | social-auth-app-django>=5.4.3,<6.0 5 | -------------------------------------------------------------------------------- /requirements_optional.txt: -------------------------------------------------------------------------------- 1 | django-rest-knox>=4.0.0,<5.0.0 2 | djangorestframework-simplejwt>=5.0.0 3 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest-django==4.5.2 2 | unittest2==1.1.0 3 | django-extensions==4.1 4 | flake8==7.2.0 5 | responses==0.25.7 6 | typing_extensions==4.13.2 7 | Werkzeug==3.1.3 8 | pyOpenSSL==25.1.0 9 | -------------------------------------------------------------------------------- /rest_social_auth/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'st4lk' 2 | __version__ = '9.0.0' 3 | -------------------------------------------------------------------------------- /rest_social_auth/serializers.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from django.utils.module_loading import import_string 4 | from django.contrib.auth import get_user_model 5 | from rest_framework import serializers 6 | from rest_framework.authtoken.models import Token 7 | 8 | 9 | class OAuth2InputSerializer(serializers.Serializer): 10 | 11 | provider = serializers.CharField(required=False) 12 | code = serializers.CharField() 13 | redirect_uri = serializers.CharField(required=False) 14 | 15 | 16 | class OAuth1InputSerializer(serializers.Serializer): 17 | 18 | provider = serializers.CharField(required=False) 19 | oauth_token = serializers.CharField() 20 | oauth_verifier = serializers.CharField() 21 | 22 | 23 | class UserSerializer(serializers.ModelSerializer): 24 | 25 | class Meta: 26 | model = get_user_model() 27 | # Custom user model may not have some fields from the list below, 28 | # excluding them based on actual fields 29 | exclude = [ 30 | field for field in ( 31 | 'is_staff', 'is_active', 'date_joined', 'password', 'last_login', 32 | 'user_permissions', 'groups', 'is_superuser', 33 | ) if field in [mfield.name for mfield in get_user_model()._meta.get_fields()] 34 | ] 35 | 36 | 37 | class TokenSerializer(serializers.Serializer): 38 | 39 | token = serializers.SerializerMethodField() 40 | 41 | def get_token(self, obj): 42 | token, created = Token.objects.get_or_create(user=obj) 43 | return token.key 44 | 45 | 46 | class UserTokenSerializer(TokenSerializer, UserSerializer): 47 | pass 48 | 49 | 50 | class KnoxSerializer(TokenSerializer): 51 | def get_token(self, obj): 52 | try: 53 | from knox.models import AuthToken 54 | except ImportError: 55 | warnings.warn( 56 | 'django-rest-knox must be installed for Knox authentication', 57 | ImportWarning, 58 | ) 59 | raise 60 | 61 | token_instance, token_key = AuthToken.objects.create(obj) 62 | return token_key 63 | 64 | 65 | class UserKnoxSerializer(KnoxSerializer, UserSerializer): 66 | pass 67 | 68 | 69 | class JWTBaseSerializer(serializers.Serializer): 70 | 71 | jwt_token_class_name = None 72 | 73 | def get_token_instance(self): 74 | if not hasattr(self, '_jwt_token_instance'): 75 | if self.jwt_token_class_name is None: 76 | raise NotImplementedError('Must specify `jwt_token_class_name` property') 77 | if '.' not in self.jwt_token_class_name: 78 | # Maintain compatibility with class name without module path 79 | module_path = 'rest_framework_simplejwt.tokens' 80 | self.jwt_token_class_name = f'{module_path}.{self.jwt_token_class_name}' 81 | try: 82 | token_class = import_string(self.jwt_token_class_name) 83 | except ImportError: 84 | warnings.warn( 85 | 'djangorestframework_simplejwt must be installed for JWT authentication', 86 | ImportWarning, 87 | ) 88 | raise 89 | user = self.instance 90 | self._jwt_token_instance = token_class.for_user(user) 91 | for key, value in self.get_token_payload(user).items(): 92 | self._jwt_token_instance[key] = value 93 | return self._jwt_token_instance 94 | 95 | def get_token_payload(self, user): 96 | """ 97 | Payload defined here will be added to default mandatory payload. 98 | Receive User instance in argument, returns dict. 99 | """ 100 | return {} 101 | 102 | 103 | class JWTPairSerializer(JWTBaseSerializer): 104 | token = serializers.SerializerMethodField() 105 | refresh = serializers.SerializerMethodField() 106 | 107 | jwt_token_class_name = 'rest_framework_simplejwt.tokens.RefreshToken' 108 | 109 | def get_token(self, obj): 110 | return str(self.get_token_instance().access_token) 111 | 112 | def get_refresh(self, obj): 113 | return str(self.get_token_instance()) 114 | 115 | 116 | class UserJWTPairSerializer(JWTPairSerializer, UserSerializer): 117 | 118 | def get_token_payload(self, user): 119 | payload = dict(UserSerializer(user).data) 120 | payload.pop('id', None) 121 | return payload 122 | 123 | 124 | class JWTSlidingSerializer(JWTBaseSerializer): 125 | token = serializers.SerializerMethodField() 126 | 127 | jwt_token_class_name = 'rest_framework_simplejwt.tokens.SlidingToken' 128 | 129 | def get_token(self, obj): 130 | return str(self.get_token_instance()) 131 | 132 | 133 | class UserJWTSlidingSerializer(JWTSlidingSerializer, UserSerializer): 134 | 135 | def get_token_payload(self, user): 136 | payload = dict(UserSerializer(user).data) 137 | payload.pop('id', None) 138 | return payload 139 | -------------------------------------------------------------------------------- /rest_social_auth/strategy.py: -------------------------------------------------------------------------------- 1 | from social_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 | -------------------------------------------------------------------------------- /rest_social_auth/urls_jwt_pair.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = ( 7 | # returns token only 8 | re_path(r'^social/jwt-pair/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 9 | views.SocialJWTPairOnlyAuthView.as_view(), 10 | name='login_social_jwt_pair'), 11 | # returns token + user_data 12 | re_path(r'^social/jwt-pair-user/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 13 | views.SocialJWTPairUserAuthView.as_view(), 14 | name='login_social_jwt_pair_user'), 15 | ) 16 | -------------------------------------------------------------------------------- /rest_social_auth/urls_jwt_sliding.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = ( 7 | # returns token only 8 | re_path(r'^social/jwt-sliding/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 9 | views.SocialJWTSlidingOnlyAuthView.as_view(), 10 | name='login_social_jwt_sliding'), 11 | # returns token + user_data 12 | re_path(r'^social/jwt-sliding-user/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 13 | views.SocialJWTSlidingUserAuthView.as_view(), 14 | name='login_social_jwt_sliding_user'), 15 | ) 16 | -------------------------------------------------------------------------------- /rest_social_auth/urls_knox.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = ( 7 | # returns knox token + user_data 8 | re_path(r'^social/knox_user/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 9 | views.SocialKnoxUserAuthView.as_view(), 10 | name='login_social_knox_user'), 11 | 12 | # returns knox token only 13 | re_path(r'^social/knox/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 14 | views.SocialKnoxOnlyAuthView.as_view(), 15 | name='login_social_knox'), 16 | ) 17 | -------------------------------------------------------------------------------- /rest_social_auth/urls_session.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = ( 7 | re_path(r'^social/session/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 8 | views.SocialSessionAuthView.as_view(), 9 | name='login_social_session'),) 10 | -------------------------------------------------------------------------------- /rest_social_auth/urls_token.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = ( 7 | # returns token + user_data 8 | re_path(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 | re_path(r'^social/token/(?:(?P[a-zA-Z0-9_-]+)/?)?$', 14 | views.SocialTokenOnlyAuthView.as_view(), 15 | name='login_social_token'),) 16 | -------------------------------------------------------------------------------- /rest_social_auth/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import warnings 3 | 4 | from django.conf import settings 5 | from django.http import HttpResponse 6 | from django.utils.decorators import method_decorator 7 | from django.utils.encoding import iri_to_uri 8 | from django.views.decorators.cache import never_cache 9 | from django.views.decorators.csrf import csrf_protect 10 | from urllib.parse import urljoin, urlencode, urlparse 11 | from social_django.utils import psa, STORAGE 12 | from social_django.views import _do_login as social_auth_login 13 | from social_core.backends.oauth import BaseOAuth1 14 | from social_core.utils import get_strategy, parse_qs, user_is_authenticated, setting_name 15 | from social_core.exceptions import AuthException, SocialAuthBaseException 16 | from rest_framework.generics import GenericAPIView 17 | from rest_framework.response import Response 18 | from rest_framework import status 19 | from rest_framework.authentication import TokenAuthentication 20 | from rest_framework.permissions import AllowAny 21 | from requests.exceptions import HTTPError 22 | 23 | from .serializers import ( 24 | JWTPairSerializer, 25 | JWTSlidingSerializer, 26 | KnoxSerializer, 27 | OAuth1InputSerializer, 28 | OAuth2InputSerializer, 29 | TokenSerializer, 30 | UserJWTSlidingSerializer, 31 | UserKnoxSerializer, 32 | UserJWTPairSerializer, 33 | UserSerializer, 34 | UserTokenSerializer, 35 | ) 36 | 37 | 38 | logger = logging.getLogger(__name__) 39 | 40 | 41 | REDIRECT_URI = getattr(settings, 'REST_SOCIAL_OAUTH_REDIRECT_URI', '/') 42 | DOMAIN_FROM_ORIGIN = getattr(settings, 'REST_SOCIAL_DOMAIN_FROM_ORIGIN', True) 43 | LOG_AUTH_EXCEPTIONS = getattr(settings, 'REST_SOCIAL_LOG_AUTH_EXCEPTIONS', True) 44 | VERBOSE_ERRORS = getattr(settings, 'REST_SOCIAL_VERBOSE_ERRORS', False) 45 | STRATEGY = getattr(settings, setting_name('STRATEGY'), 'rest_social_auth.strategy.DRFStrategy') 46 | 47 | 48 | def load_strategy(request=None): 49 | return get_strategy(STRATEGY, STORAGE, request) 50 | 51 | 52 | @psa(REDIRECT_URI, load_strategy=load_strategy) 53 | def decorate_request(request, backend): 54 | pass 55 | 56 | 57 | class BaseSocialAuthView(GenericAPIView): 58 | """ 59 | View will login or signin (create) the user from social oauth2.0 provider. 60 | 61 | **Input** (default serializer_class_in): 62 | 63 | { 64 | "provider": "facebook", 65 | "code": "AQBPBBTjbdnehj51" 66 | } 67 | 68 | + optional 69 | 70 | "redirect_uri": "/relative/or/absolute/redirect/uri" 71 | 72 | **Output**: 73 | 74 | user data in serializer_class format 75 | """ 76 | 77 | oauth1_serializer_class_in = OAuth1InputSerializer 78 | oauth2_serializer_class_in = OAuth2InputSerializer 79 | serializer_class = None 80 | permission_classes = (AllowAny, ) 81 | 82 | def oauth_v1(self): 83 | assert hasattr(self.request, 'backend'), 'Don\'t call this method before decorate_request' 84 | return isinstance(self.request.backend, BaseOAuth1) 85 | 86 | def get_serializer_class_in(self): 87 | if self.oauth_v1(): 88 | return self.oauth1_serializer_class_in 89 | return self.oauth2_serializer_class_in 90 | 91 | def get_serializer_in(self, *args, **kwargs): 92 | """ 93 | Return the serializer instance that should be used for validating and 94 | deserializing input, and for serializing output. 95 | """ 96 | serializer_class = self.get_serializer_class_in() 97 | kwargs['context'] = self.get_serializer_context() 98 | return serializer_class(*args, **kwargs) 99 | 100 | def get_serializer_in_data(self): 101 | """ 102 | Compile the incoming data into a form fit for the serializer_in class. 103 | :return: Data for serializer in the form of a dictionary with 'provider' and 'code' keys. 104 | """ 105 | return self.request.data.copy() 106 | 107 | @method_decorator(never_cache) 108 | def post(self, request, *args, **kwargs): 109 | input_data = self.get_serializer_in_data() 110 | provider_name = self.get_provider_name(input_data) 111 | if not provider_name: 112 | return self.respond_error("Provider is not specified") 113 | self.set_input_data(request, input_data) 114 | decorate_request(request, provider_name) 115 | serializer_in = self.get_serializer_in(data=input_data) 116 | if self.oauth_v1() and request.backend.OAUTH_TOKEN_PARAMETER_NAME not in input_data: 117 | # oauth1 first stage (1st is get request_token, 2nd is get access_token) 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 | request_token = parse_qs(request.backend.set_unauthorized_token()) 123 | return Response(request_token) 124 | serializer_in.is_valid(raise_exception=True) 125 | try: 126 | user = self.get_object() 127 | except (AuthException, HTTPError) as e: 128 | return self.respond_error(e) 129 | if isinstance(user, HttpResponse): 130 | # error happened and pipeline returned HttpResponse instead of user 131 | return user 132 | resp_data = self.get_serializer(instance=user) 133 | self.do_login(request.backend, user) 134 | return Response(resp_data.data) 135 | 136 | def get_object(self): 137 | user = self.request.user 138 | manual_redirect_uri = self.request.auth_data.pop('redirect_uri', None) 139 | manual_redirect_uri = self.get_redirect_uri(manual_redirect_uri) 140 | if manual_redirect_uri: 141 | self.request.backend.redirect_uri = manual_redirect_uri 142 | elif DOMAIN_FROM_ORIGIN: 143 | origin = self.request.strategy.request.META.get('HTTP_ORIGIN') 144 | if origin: 145 | relative_path = urlparse(self.request.backend.redirect_uri).path 146 | url = urlparse(origin) 147 | origin_scheme_host = f"{url.scheme}://{url.netloc}" 148 | location = urljoin(origin_scheme_host, relative_path) 149 | self.request.backend.redirect_uri = iri_to_uri(location) 150 | is_authenticated = user_is_authenticated(user) 151 | user = is_authenticated and user or None 152 | # skip checking state by setting following params to False 153 | # it is responsibility of front-end to check state 154 | # TODO: maybe create an additional resource, where front-end will 155 | # store the state before making a call to oauth provider 156 | # so server can save it in session and consequently check it before 157 | # sending request to acquire access token. 158 | # In case of token authentication we need a way to store an anonymous 159 | # session to do it. 160 | self.request.backend.REDIRECT_STATE = False 161 | self.request.backend.STATE_PARAMETER = False 162 | 163 | if self.oauth_v1(): 164 | self.save_token_param_in_session() 165 | 166 | user = self.request.backend.complete(user=user, request=self.request) 167 | return user 168 | 169 | def save_token_param_in_session(self): 170 | """ 171 | Save token param in strategy's session. 172 | This method will allow to use token auth with OAuth1 even if session is not enabled in 173 | django settings (social_core expects that session is enabled). 174 | """ 175 | backend = self.request.backend 176 | session_token_name = backend.name + backend.UNATHORIZED_TOKEN_SUFIX 177 | session = self.request.strategy.session 178 | if ( 179 | (isinstance(session, dict) and session_token_name not in session) or 180 | not session.exists(session_token_name) 181 | ): 182 | oauth1_token_param = backend.data.get(backend.OAUTH_TOKEN_PARAMETER_NAME) 183 | session[session_token_name] = [ 184 | urlencode({ 185 | backend.OAUTH_TOKEN_PARAMETER_NAME: oauth1_token_param, 186 | 'oauth_token_secret': backend.data.get('oauth_token_secret') 187 | }) 188 | ] 189 | 190 | def do_login(self, backend, user): 191 | """ 192 | Do login action here. 193 | For example in case of session authentication store the session in 194 | cookies. 195 | """ 196 | 197 | def set_input_data(self, request, auth_data): 198 | """ 199 | auth_data will be used used as request_data in strategy 200 | """ 201 | request.auth_data = auth_data 202 | 203 | def get_redirect_uri(self, manual_redirect_uri): 204 | if not manual_redirect_uri: 205 | manual_redirect_uri = getattr( 206 | settings, 'REST_SOCIAL_OAUTH_ABSOLUTE_REDIRECT_URI', None) 207 | return manual_redirect_uri 208 | 209 | def get_provider_name(self, input_data): 210 | if self.kwargs.get('provider'): 211 | return self.kwargs['provider'] 212 | return input_data.get('provider') 213 | 214 | def respond_error(self, error): 215 | message = error if isinstance(error, str) else '' 216 | if isinstance(error, Exception): 217 | if not isinstance(error, AuthException) or LOG_AUTH_EXCEPTIONS: 218 | self.log_exception(error) 219 | if VERBOSE_ERRORS: 220 | if hasattr(error, 'response'): 221 | try: 222 | message = error.response.json()['error'] 223 | if isinstance(message, dict) and 'message' in message: 224 | message = message['message'] 225 | elif isinstance(message, list) and len(message): 226 | message = message[0] 227 | except (KeyError, TypeError): 228 | pass 229 | # As a fallback, if no valid message was captured, covert the exception to string 230 | # because most of the social-core exceptions implement a valid conversion. 231 | if isinstance(error, SocialAuthBaseException) and not message: 232 | message = str(error) 233 | else: 234 | logger.error(error) 235 | return Response(data=message, status=status.HTTP_400_BAD_REQUEST) 236 | 237 | def log_exception(self, error): 238 | err_msg = error.args[0] if error.args else '' 239 | if getattr(error, 'response', None) is not None: 240 | try: 241 | err_data = error.response.json() 242 | except (ValueError, AttributeError): 243 | logger.error('%s; %s', error, err_msg) 244 | else: 245 | logger.error('%s; %s; %s', error, err_msg, err_data) 246 | else: 247 | logger.exception('{%s}; {%s}', error, err_msg) 248 | 249 | 250 | class SocialSessionAuthView(BaseSocialAuthView): 251 | serializer_class = UserSerializer 252 | 253 | def do_login(self, backend, user): 254 | social_auth_login(backend, user, user.social_user) 255 | 256 | @method_decorator(csrf_protect) # just to be sure csrf is not disabled 257 | def post(self, request, *args, **kwargs): 258 | return super().post(request, *args, **kwargs) 259 | 260 | 261 | class SocialTokenOnlyAuthView(BaseSocialAuthView): 262 | serializer_class = TokenSerializer 263 | authentication_classes = (TokenAuthentication, ) 264 | 265 | 266 | class SocialTokenUserAuthView(BaseSocialAuthView): 267 | serializer_class = UserTokenSerializer 268 | authentication_classes = (TokenAuthentication, ) 269 | 270 | 271 | class KnoxAuthMixin: 272 | def get_authenticators(self): 273 | try: 274 | from knox.auth import TokenAuthentication 275 | except ImportError: 276 | warnings.warn( 277 | 'django-rest-knox must be installed for Knox authentication', 278 | ImportWarning, 279 | ) 280 | raise 281 | 282 | return [TokenAuthentication()] 283 | 284 | 285 | class SocialKnoxOnlyAuthView(KnoxAuthMixin, BaseSocialAuthView): 286 | serializer_class = KnoxSerializer 287 | 288 | 289 | class SocialKnoxUserAuthView(KnoxAuthMixin, BaseSocialAuthView): 290 | serializer_class = UserKnoxSerializer 291 | 292 | 293 | class SimpleJWTAuthMixin: 294 | def get_authenticators(self): 295 | try: 296 | from rest_framework_simplejwt.authentication import JWTAuthentication 297 | except ImportError: 298 | warnings.warn( 299 | 'django-rest-framework-simplejwt must be installed for JWT authentication', 300 | ImportWarning, 301 | ) 302 | raise 303 | 304 | return [JWTAuthentication()] 305 | 306 | 307 | class SocialJWTPairOnlyAuthView(SimpleJWTAuthMixin, BaseSocialAuthView): 308 | serializer_class = JWTPairSerializer 309 | 310 | 311 | class SocialJWTPairUserAuthView(SimpleJWTAuthMixin, BaseSocialAuthView): 312 | serializer_class = UserJWTPairSerializer 313 | 314 | 315 | class SocialJWTSlidingOnlyAuthView(SimpleJWTAuthMixin, BaseSocialAuthView): 316 | serializer_class = JWTSlidingSerializer 317 | 318 | 319 | class SocialJWTSlidingUserAuthView(SimpleJWTAuthMixin, BaseSocialAuthView): 320 | serializer_class = UserJWTSlidingSerializer 321 | -------------------------------------------------------------------------------- /scripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PROJECT_PATH=/django_rest_social_auth 4 | 5 | # create virtualenv if it doesn't exist 6 | python -m venv /${PROJECT_PATH}/venv 7 | . ${PROJECT_PATH}/venv/bin/activate 8 | python -m pip install -U pip 9 | 10 | export PYENV_ROOT="${PROJECT_PATH}/.pyenv" 11 | export PATH="${PROJECT_PATH}/.pyenv/bin:${PATH}" 12 | 13 | cd ${PROJECT_PATH} 14 | $1 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | exclude = 6 | venv/ 7 | __pycache__ 8 | migrations 9 | build/ 10 | dist/ 11 | .tox 12 | .pyenv 13 | import-order-style = pycharm 14 | max-line-length = 99 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | from setuptools import setup 5 | from rest_social_auth import __author__, __version__ 6 | 7 | 8 | def __read(fname): 9 | try: 10 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 11 | except OSError: 12 | return '' 13 | 14 | 15 | if sys.argv[-1] == 'build': 16 | shutil.rmtree('build', ignore_errors=True) 17 | shutil.rmtree('dist', ignore_errors=True) 18 | 19 | # build both sdist and wheel in one go, using the PEP 517 frontend 20 | os.system(f"{sys.executable} -m build --sdist --wheel") 21 | sys.exit(0) 22 | 23 | 24 | if sys.argv[-1] == 'publish': 25 | # TODO: Need python 3.4.6+, 3.5.3+, Python 3.6+ here, add a check 26 | # https://packaging.python.org/guides/migrating-to-pypi-org/#uploading 27 | dists_to_upload = [ 28 | # f'dist/rest_social_auth-{__version__}.tar.gz', 29 | f'dist/rest_social_auth-{__version__}-py2.py3-none-any.whl', 30 | ] 31 | for dist in dists_to_upload: 32 | print(f'Uploading {dist}') 33 | os.system(f'twine upload -r pypi {dist}') 34 | sys.exit() 35 | 36 | 37 | if sys.argv[-1] == 'tag': 38 | print("Tagging the version on github:") 39 | os.system(f"git tag -a v{__version__} -m 'version {__version__}'") 40 | os.system("git push --tags") 41 | sys.exit() 42 | 43 | 44 | install_requires = __read('requirements.txt').split() 45 | 46 | setup( 47 | name='rest_social_auth', 48 | author=__author__, 49 | author_email='alexevseev@gmail.com', 50 | version=__version__, 51 | description='Django rest framework resources for social auth', 52 | long_description=__read('README.md') + '\n\n' + __read('RELEASE_NOTES.md'), 53 | long_description_content_type='text/markdown; charset=UTF-8', 54 | platforms=('Any'), 55 | packages=['rest_social_auth'], 56 | install_requires=install_requires, 57 | keywords='django social auth rest login signin signup oauth'.split(), 58 | include_package_data=True, 59 | license='MIT license', 60 | package_dir={'rest_social_auth': 'rest_social_auth'}, 61 | url='https://github.com/st4lk/django-rest-social-auth', 62 | classifiers=[ 63 | 'Environment :: Web Environment', 64 | 'Framework :: Django', 65 | 'Intended Audience :: Developers', 66 | 'License :: OSI Approved :: MIT License', 67 | 'Operating System :: OS Independent', 68 | 'Programming Language :: Python', 69 | 'Programming Language :: Python :: 3.7', 70 | 'Programming Language :: Python :: 3.8', 71 | 'Programming Language :: Python :: 3.9', 72 | 'Programming Language :: Python :: 3.10', 73 | 'Programming Language :: Python :: 3.11', 74 | 'Topic :: Utilities', 75 | ], 76 | ) 77 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st4lk/django-rest-social-auth/9a9bfd0279ed4b7515da1ad086e5972c5d42de69/tests/__init__.py -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import responses 4 | from social_core.backends.utils import load_backends 5 | from social_core.tests.backends.test_facebook import FacebookOAuth2Test 6 | from social_core.tests.backends.test_twitter import TwitterOAuth1Test 7 | from social_core.utils import module_member 8 | 9 | from social_core.tests.models import ( 10 | TestAssociation, 11 | TestCode, 12 | TestNonce, 13 | TestUserSocialAuth, 14 | User, 15 | ) 16 | 17 | from rest_social_auth import views 18 | 19 | 20 | # don't run third party tests 21 | for attr in (attr for attr in dir(FacebookOAuth2Test) if attr.startswith('test_')): 22 | try: 23 | delattr(FacebookOAuth2Test, attr) 24 | except AttributeError: 25 | pass 26 | for attr in (attr for attr in dir(TwitterOAuth1Test) if attr.startswith('test_')): 27 | try: 28 | delattr(TwitterOAuth1Test, attr) 29 | except AttributeError: 30 | pass 31 | 32 | 33 | class RestSocialMixin: 34 | def setUp(self): 35 | responses.start() 36 | Backend = module_member(self.backend_path) 37 | self.strategy = views.load_strategy() 38 | self.backend = Backend(self.strategy, redirect_uri=self.complete_url) 39 | self.name = self.backend.name.upper().replace("-", "_") 40 | self.complete_url = self.strategy.build_absolute_uri( 41 | self.raw_complete_url.format(self.backend.name) 42 | ) 43 | backends = (self.backend_path, ) 44 | load_backends(backends, force_load=True) 45 | User.reset_cache() 46 | TestUserSocialAuth.reset_cache() 47 | TestNonce.reset_cache() 48 | TestAssociation.reset_cache() 49 | TestCode.reset_cache() 50 | 51 | user_data_body = json.loads(self.user_data_body) 52 | self.email = 'example@mail.com' 53 | user_data_body['email'] = self.email 54 | self.user_data_body = json.dumps(user_data_body) 55 | 56 | self.do_rest_login() 57 | 58 | def tearDown(self): 59 | responses.stop() 60 | responses.reset() 61 | self.backend = None 62 | self.strategy = None 63 | self.name = None 64 | self.complete_url = None 65 | User.reset_cache() 66 | TestUserSocialAuth.reset_cache() 67 | TestNonce.reset_cache() 68 | TestAssociation.reset_cache() 69 | TestCode.reset_cache() 70 | 71 | 72 | class BaseFacebookAPITestCase(RestSocialMixin, FacebookOAuth2Test): 73 | 74 | def do_rest_login(self): 75 | start_url = self.backend.start().url 76 | self.auth_handlers(start_url) 77 | self.pre_complete_callback(start_url) 78 | 79 | def setup_api_mocks(self): 80 | # Add missing mocks for access token and user data endpoints 81 | responses.reset() 82 | responses.add( 83 | responses.GET, 84 | self.backend.access_token_url(), 85 | body=self.access_token_body, 86 | status=self.access_token_status 87 | ) 88 | responses.add( 89 | responses.GET, 90 | self.user_data_url, 91 | body=self.user_data_body, 92 | status=200 93 | ) 94 | 95 | 96 | class BaseTwitterApiTestCase(RestSocialMixin, TwitterOAuth1Test): 97 | 98 | def do_rest_login(self): 99 | self.request_token_handler() 100 | start_url = self.backend.start().url 101 | self.auth_handlers(start_url) 102 | self.pre_complete_callback(start_url) 103 | -------------------------------------------------------------------------------- /tests/test_knox.py: -------------------------------------------------------------------------------- 1 | from django.test import override_settings 2 | from django.urls import reverse 3 | from knox.auth import TokenAuthentication as KnoxTokenAuthentication 4 | from rest_framework.test import APITestCase 5 | from social_core.utils import parse_qs 6 | 7 | from .base import BaseFacebookAPITestCase, BaseTwitterApiTestCase 8 | 9 | 10 | knox_override_settings = dict( 11 | INSTALLED_APPS=[ 12 | 'django.contrib.contenttypes', 13 | 'rest_framework', 14 | 'social_django', 15 | 'rest_social_auth', 16 | 'knox', # For django-rest-knox 17 | 'users', 18 | ], 19 | MIDDLEWARE=[ 20 | ], 21 | ) 22 | 23 | 24 | @override_settings(**knox_override_settings) 25 | class TestSocialAuth1Knox(APITestCase, BaseTwitterApiTestCase): 26 | 27 | def test_login_social_oauth1_knox(self): 28 | resp = self.client.post( 29 | reverse('login_social_knox_user'), data={'provider': 'twitter'}) 30 | self.assertEqual(resp.status_code, 200) 31 | self.assertEqual(resp.data, parse_qs(self.request_token_body)) 32 | resp = self.client.post(reverse('login_social_knox_user'), data={ 33 | 'provider': 'twitter', 34 | 'oauth_token': 'foobar', 35 | 'oauth_verifier': 'overifier' 36 | }) 37 | self.assertEqual(resp.status_code, 200) 38 | 39 | 40 | @override_settings(**knox_override_settings) 41 | class TestSocialAuth2Knox(APITestCase, BaseFacebookAPITestCase): 42 | 43 | def _check_login_social_knox_only(self, url, data): 44 | resp = self.client.post(url, data) 45 | self.assertEqual(resp.status_code, 200) 46 | # check token valid 47 | knox_auth = KnoxTokenAuthentication() 48 | user, auth_data = knox_auth.authenticate_credentials(resp.data['token'].encode('utf8')) 49 | self.assertEqual(user.email, self.email) 50 | 51 | def _check_login_social_knox_user(self, url, data): 52 | resp = self.client.post(url, data) 53 | self.assertEqual(resp.status_code, 200) 54 | self.assertEqual(resp.data['email'], self.email) 55 | # check token valid 56 | knox_auth = KnoxTokenAuthentication() 57 | user, auth_data = knox_auth.authenticate_credentials(resp.data['token'].encode('utf8')) 58 | self.assertEqual(user.email, self.email) 59 | 60 | def test_login_social_knox_only(self): 61 | self._check_login_social_knox_only( 62 | reverse('login_social_knox'), 63 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 64 | 65 | def test_login_social_knox_only_provider_in_url(self): 66 | self._check_login_social_knox_only( 67 | reverse('login_social_knox', kwargs={'provider': 'facebook'}), 68 | data={'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 69 | 70 | def test_login_social_knox_user(self): 71 | self._check_login_social_knox_user( 72 | reverse('login_social_knox_user'), 73 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 74 | 75 | def test_login_social_knox_user_provider_in_url(self): 76 | self._check_login_social_knox_user( 77 | reverse('login_social_knox_user', kwargs={'provider': 'facebook'}), 78 | data={'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 79 | -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from urllib.parse import parse_qsl, urlparse 3 | 4 | from django.urls import reverse 5 | from django.contrib.auth import get_user_model 6 | from django.test import modify_settings 7 | from django.test.utils import override_settings 8 | from rest_framework.test import APITestCase 9 | from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly 10 | import responses 11 | from social_core.utils import parse_qs 12 | 13 | from .base import BaseFacebookAPITestCase, BaseTwitterApiTestCase 14 | 15 | 16 | session_modify_settings = dict( 17 | INSTALLED_APPS={ 18 | 'remove': [ 19 | 'rest_framework.authtoken', 20 | 'knox', 21 | ] 22 | }, 23 | ) 24 | 25 | 26 | class TestSocialAuth1(APITestCase, BaseTwitterApiTestCase): 27 | 28 | @modify_settings(**session_modify_settings) 29 | def test_login_social_oauth1_session(self): 30 | resp = self.client.post( 31 | reverse('login_social_session'), data={'provider': 'twitter'}) 32 | self.assertEqual(resp.status_code, 200) 33 | self.assertEqual(resp.data, parse_qs(self.request_token_body)) 34 | resp = self.client.post(reverse('login_social_session'), data={ 35 | 'provider': 'twitter', 36 | 'oauth_token': 'foobar', 37 | 'oauth_verifier': 'overifier' 38 | }) 39 | self.assertEqual(resp.status_code, 200) 40 | 41 | 42 | class TestSocialAuth2(APITestCase, BaseFacebookAPITestCase): 43 | 44 | @modify_settings(**session_modify_settings) 45 | def _check_login_social_session(self, url, data): 46 | self.setup_api_mocks() 47 | resp = self.client.post(url, data) 48 | self.assertEqual(resp.status_code, 200) 49 | self.assertEqual(resp.data['email'], self.email) 50 | # check cookies are set 51 | self.assertTrue('sessionid' in resp.cookies) 52 | # check user is created 53 | self.assertTrue( 54 | get_user_model().objects.filter(email=self.email).exists()) 55 | 56 | def test_login_social_session(self): 57 | self._check_login_social_session( 58 | reverse('login_social_session'), 59 | {'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 60 | 61 | def test_login_social_session_provider_in_url(self): 62 | self._check_login_social_session( 63 | reverse('login_social_session', kwargs={'provider': 'facebook'}), 64 | {'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 65 | 66 | def test_no_provider_session(self): 67 | resp = self.client.post( 68 | reverse('login_social_session'), 69 | {'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 70 | self.assertEqual(resp.status_code, 400) 71 | 72 | def test_unknown_provider_session(self): 73 | resp = self.client.post( 74 | reverse('login_social_session', kwargs={'provider': 'unknown'}), 75 | {'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 76 | self.assertEqual(resp.status_code, 404) 77 | 78 | def test_login_social_http_origin(self): 79 | self.setup_api_mocks() 80 | resp = self.client.post( 81 | reverse('login_social_session'), 82 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 83 | HTTP_ORIGIN="http://frontend.com") 84 | self.assertEqual(resp.status_code, 200) 85 | url_params = dict(parse_qsl(urlparse(responses.calls[0].request.path_url).query)) 86 | self.assertEqual(url_params['redirect_uri'], "http://frontend.com/") 87 | 88 | @override_settings(REST_SOCIAL_OAUTH_ABSOLUTE_REDIRECT_URI='http://myproject.com/') 89 | def test_login_absolute_redirect(self): 90 | self.setup_api_mocks() 91 | resp = self.client.post( 92 | reverse('login_social_session'), 93 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 94 | self.assertEqual(resp.status_code, 200) 95 | url_params = dict(parse_qsl(urlparse(responses.calls[0].request.path_url).query)) 96 | self.assertEqual('http://myproject.com/', url_params['redirect_uri']) 97 | 98 | @override_settings(REST_SOCIAL_OAUTH_ABSOLUTE_REDIRECT_URI='http://myproject.com/') 99 | def test_login_manual_redirect(self): 100 | self.setup_api_mocks() 101 | resp = self.client.post( 102 | reverse('login_social_session'), 103 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw', 104 | 'redirect_uri': 'http://manualdomain.com/'}) 105 | self.assertEqual(resp.status_code, 200) 106 | url_params = dict(parse_qsl(urlparse(responses.calls[0].request.path_url).query)) 107 | self.assertEqual('http://manualdomain.com/', url_params['redirect_uri']) 108 | 109 | @mock.patch('rest_framework.views.APIView.permission_classes') 110 | def test_login_social_session_model_permission(self, m_permission_classes): 111 | setattr( 112 | m_permission_classes, 113 | '__get__', 114 | lambda *args, **kwargs: (DjangoModelPermissionsOrAnonReadOnly,), 115 | ) 116 | self._check_login_social_session( 117 | reverse('login_social_session'), 118 | {'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 119 | 120 | 121 | class TestSocialAuth2Error(APITestCase, BaseFacebookAPITestCase): 122 | access_token_status = 400 123 | 124 | def test_login_oauth_provider_error(self): 125 | resp = self.client.post( 126 | reverse('login_social_session'), 127 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 128 | self.assertEqual(resp.status_code, 400) 129 | 130 | 131 | class TestSocialAuth2HTTPError(APITestCase, BaseFacebookAPITestCase): 132 | access_token_status = 401 133 | 134 | def test_login_oauth_provider_http_error(self): 135 | resp = self.client.post( 136 | reverse('login_social_session'), 137 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 138 | self.assertEqual(resp.status_code, 400) 139 | -------------------------------------------------------------------------------- /tests/test_simple_jwt.py: -------------------------------------------------------------------------------- 1 | from django.test import override_settings 2 | from django.urls import reverse 3 | from rest_framework.test import APITestCase 4 | from rest_framework_simplejwt.authentication import JWTAuthentication 5 | from social_core.utils import parse_qs 6 | 7 | from .base import BaseFacebookAPITestCase, BaseTwitterApiTestCase 8 | 9 | 10 | jwt_simple_override_settings = dict( 11 | INSTALLED_APPS=[ 12 | 'django.contrib.contenttypes', 13 | 'rest_framework', 14 | 'social_django', 15 | 'rest_social_auth', 16 | 'users', 17 | ], 18 | MIDDLEWARE=[], 19 | ) 20 | 21 | 22 | @override_settings(**jwt_simple_override_settings) 23 | class TestSocialAuth1SimpleJWT(APITestCase, BaseTwitterApiTestCase): 24 | 25 | def test_login_social_oauth1_jwt(self): 26 | resp = self.client.post( 27 | reverse('login_social_jwt_pair'), data={'provider': 'twitter'}) 28 | self.assertEqual(resp.status_code, 200) 29 | self.assertEqual(resp.data, parse_qs(self.request_token_body)) 30 | resp = self.client.post(reverse('login_social_jwt_pair'), data={ 31 | 'provider': 'twitter', 32 | 'oauth_token': 'foobar', 33 | 'oauth_verifier': 'overifier' 34 | }) 35 | self.assertEqual(resp.status_code, 200) 36 | 37 | 38 | @override_settings(**jwt_simple_override_settings) 39 | class TestSocialAuth2SimpleJWT(APITestCase, BaseFacebookAPITestCase): 40 | 41 | def _check_login_social_simple_jwt_only(self, url, data, token_type): 42 | resp = self.client.post(url, data) 43 | self.assertEqual(resp.status_code, 200) 44 | # check token valid 45 | jwt_auth = JWTAuthentication() 46 | token_instance = jwt_auth.get_validated_token(resp.data['token']) 47 | self.assertEqual(token_instance['token_type'], token_type) 48 | 49 | def _check_login_social_simple_jwt_user(self, url, data, token_type): 50 | resp = self.client.post(url, data) 51 | self.assertEqual(resp.status_code, 200) 52 | self.assertEqual(resp.data['email'], self.email) 53 | # check token valid 54 | jwt_auth = JWTAuthentication() 55 | token_instance = jwt_auth.get_validated_token(resp.data['token']) 56 | self.assertEqual(token_instance['token_type'], token_type) 57 | self.assertEqual(token_instance['email'], self.email) 58 | 59 | def test_login_social_simple_jwt_pair_only(self): 60 | self._check_login_social_simple_jwt_only( 61 | reverse('login_social_jwt_pair'), 62 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 63 | token_type='access', 64 | ) 65 | 66 | def test_login_social_simple_jwt_pair_only_provider_in_url(self): 67 | self._check_login_social_simple_jwt_only( 68 | reverse('login_social_jwt_pair', kwargs={'provider': 'facebook'}), 69 | data={'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 70 | token_type='access', 71 | ) 72 | 73 | def test_login_social_simple_jwt_pair_user(self): 74 | self._check_login_social_simple_jwt_user( 75 | reverse('login_social_jwt_pair_user'), 76 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 77 | token_type='access', 78 | ) 79 | 80 | def test_login_social_simple_jwt_pair_user_provider_in_url(self): 81 | self._check_login_social_simple_jwt_user( 82 | reverse('login_social_jwt_pair_user', kwargs={'provider': 'facebook'}), 83 | data={'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 84 | token_type='access', 85 | ) 86 | 87 | def test_login_social_simple_jwt_sliding_only(self): 88 | self._check_login_social_simple_jwt_only( 89 | reverse('login_social_jwt_sliding'), 90 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 91 | token_type='sliding', 92 | ) 93 | 94 | def test_login_social_simple_jwt_sliding_only_provider_in_url(self): 95 | self._check_login_social_simple_jwt_only( 96 | reverse('login_social_jwt_sliding', kwargs={'provider': 'facebook'}), 97 | data={'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 98 | token_type='sliding', 99 | ) 100 | 101 | def test_login_social_simple_jwt_sliding_user(self): 102 | self._check_login_social_simple_jwt_user( 103 | reverse('login_social_jwt_sliding_user'), 104 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 105 | token_type='sliding', 106 | ) 107 | 108 | def test_login_social_simple_jwt_sliding_user_provider_in_url(self): 109 | self._check_login_social_simple_jwt_user( 110 | reverse('login_social_jwt_sliding_user', kwargs={'provider': 'facebook'}), 111 | data={'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 112 | token_type='sliding', 113 | ) 114 | -------------------------------------------------------------------------------- /tests/test_token.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.test import override_settings 4 | from django.urls import reverse 5 | from rest_framework.authtoken.models import Token 6 | from rest_framework.test import APITestCase 7 | from social_core.utils import parse_qs 8 | 9 | from .base import BaseFacebookAPITestCase, BaseTwitterApiTestCase 10 | 11 | 12 | token_override_settings = dict( 13 | INSTALLED_APPS=[ 14 | 'django.contrib.contenttypes', 15 | 'rest_framework', 16 | 'rest_framework.authtoken', 17 | 'social_django', 18 | 'rest_social_auth', 19 | 'users', 20 | ], 21 | MIDDLEWARE=[], 22 | SOCIAL_AUTH_PIPELINE=( 23 | 'users.social_pipeline.auto_logout', # custom action 24 | 'social_core.pipeline.social_auth.social_details', 25 | 'social_core.pipeline.social_auth.social_uid', 26 | 'social_core.pipeline.social_auth.auth_allowed', 27 | 'users.social_pipeline.check_for_email', # custom action 28 | 'social_core.pipeline.social_auth.social_user', 29 | 'social_core.pipeline.user.get_username', 30 | 'social_core.pipeline.user.create_user', 31 | 'social_core.pipeline.social_auth.associate_user', 32 | 'social_core.pipeline.social_auth.load_extra_data', 33 | 'social_core.pipeline.user.user_details', 34 | 'users.social_pipeline.save_avatar', # custom action 35 | ), 36 | ) 37 | 38 | 39 | @override_settings(**token_override_settings) 40 | class TestSocialAuth1Token(APITestCase, BaseTwitterApiTestCase): 41 | 42 | def test_login_social_oauth1_token(self): 43 | url = reverse('login_social_token_user') 44 | resp = self.client.post(url, data={'provider': 'twitter'}) 45 | self.assertEqual(resp.status_code, 200) 46 | self.assertEqual(resp.data, parse_qs(self.request_token_body)) 47 | resp = self.client.post(reverse('login_social_token_user'), data={ 48 | 'provider': 'twitter', 49 | 'oauth_token': 'foobar', 50 | 'oauth_verifier': 'overifier' 51 | }) 52 | self.assertEqual(resp.status_code, 200) 53 | 54 | 55 | @override_settings(**token_override_settings) 56 | class TestSocialAuth2Token(APITestCase, BaseFacebookAPITestCase): 57 | 58 | def _check_login_social_token_user(self, url, data): 59 | self.setup_api_mocks() 60 | resp = self.client.post(url, data) 61 | self.assertEqual(resp.status_code, 200) 62 | self.assertEqual(resp.data['email'], self.email) 63 | # check token exists 64 | token = Token.objects.get(key=resp.data['token']) 65 | # check user is created 66 | self.assertEqual(token.user.email, self.email) 67 | 68 | def _check_login_social_token_only(self, url, data): 69 | self.setup_api_mocks() 70 | resp = self.client.post(url, data) 71 | self.assertEqual(resp.status_code, 200) 72 | # check token exists 73 | token = Token.objects.get(key=resp.data['token']) 74 | # check user is created 75 | self.assertEqual(token.user.email, self.email) 76 | 77 | def test_login_social_token_user(self): 78 | self._check_login_social_token_user( 79 | reverse('login_social_token_user'), 80 | {'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 81 | 82 | def test_login_social_token_user_provider_in_url(self): 83 | self._check_login_social_token_user( 84 | reverse('login_social_token_user', kwargs={'provider': 'facebook'}), 85 | {'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 86 | 87 | def test_login_social_token_only(self): 88 | self._check_login_social_token_only( 89 | reverse('login_social_token'), 90 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 91 | 92 | def test_login_social_token_only_provider_in_url(self): 93 | self._check_login_social_token_only( 94 | reverse('login_social_token', kwargs={'provider': 'facebook'}), 95 | data={'code': '3D52VoM1uiw94a1ETnGvYlCw'}) 96 | 97 | def test_user_login_with_no_email(self): 98 | # Modify user data to have no email BEFORE setting up the test 99 | user_data_body = json.loads(self.user_data_body) 100 | user_data_body['email'] = '' 101 | self.user_data_body = json.dumps(user_data_body) 102 | 103 | # Set up mocks and do the rest login 104 | self.do_rest_login() 105 | self.setup_api_mocks() 106 | 107 | resp = self.client.post( 108 | reverse('login_social_token'), 109 | data={'provider': 'facebook', 'code': '3D52VoM1uiw94a1ETnGvYlCw'}, 110 | ) 111 | self.assertEqual(resp.status_code, 400) 112 | self.assertIn('error', resp.data) 113 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{38}-django{42} 4 | py{39}-django{42} 5 | py{310}-django{42, 50, 51, 52} 6 | py{311}-django{42, 50, 51, 52} 7 | py{312}-django{42, 50, 51, 52} 8 | 9 | [testenv] 10 | setenv = 11 | PYTHONPATH = {toxinidir}/example_project 12 | LC_ALL = en_US.utf-8 13 | basepython = 14 | py39: python3.9 15 | py310: python3.10 16 | py311: python3.11 17 | py312: python3.12 18 | deps = 19 | djangorestframework<4.0 20 | social-auth-core==4.6.1 21 | social-auth-app-django==5.4.3 22 | djangorestframework-jwt 23 | djangorestframework_simplejwt 24 | django-rest-knox<5.0.0 25 | coverage 26 | django42: Django>=4.2,<4.3 27 | django50: Django>=5.0,<5.1 28 | django51: Django>=5.1,<5.2 29 | django52: Django>=5.2,<5.3 30 | -rrequirements_test.txt 31 | commands = 32 | coverage run --source=rest_social_auth -m pytest {posargs} 33 | coverage report 34 | coverage lcov 35 | --------------------------------------------------------------------------------