├── .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 | [](https://github.com/st4lk/django-rest-social-auth/actions/workflows/lint.yml?query=branch%3Amaster)
5 | [](https://github.com/st4lk/django-rest-social-auth/actions/workflows/test.yml?query=branch%3Amaster)
6 | [](https://coveralls.io/github/st4lk/django-rest-social-auth?branch=master)
7 | [](https://pypi.python.org/pypi/rest_social_auth)
8 | [](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 |
6 |
7 |
8 |
9 |
rest_framework_simplejwt.authentication.JWTAuthentication
10 |
11 |
12 |
13 |
14 |
15 |
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 |
--------------------------------------------------------------------------------