├── example ├── example │ ├── __init__.py │ ├── settings │ │ ├── __init__.py │ │ ├── production.py │ │ ├── dev.py │ │ └── base.py │ ├── templates │ │ ├── oauth2_provider │ │ │ └── base.html │ │ └── example │ │ │ ├── login.html │ │ │ ├── consumer.html │ │ │ ├── home.html │ │ │ ├── consumer-client.html │ │ │ ├── consumer-done.html │ │ │ ├── base.html │ │ │ ├── consumer-exchange.html │ │ │ └── api-client.html │ ├── models.py │ ├── forms.py │ ├── fixtures │ │ └── oauth2_provider_fixtures.json │ ├── wsgi.py │ ├── middleware.py │ ├── urls.py │ ├── api_v1.py │ └── views.py ├── Procfile ├── manage.py └── requirements.txt ├── oauth2_provider ├── ext │ ├── __init__.py │ └── rest_framework │ │ ├── __init__.py │ │ ├── authentication.py │ │ └── permissions.py ├── migrations │ └── __init__.py ├── __init__.py ├── tests │ ├── models.py │ ├── urls.py │ ├── __init__.py │ ├── test_utils.py │ ├── test_oauth2_backends.py │ ├── test_validators.py │ ├── test_generator.py │ ├── test_mixins.py │ ├── test_oauth2_validators.py │ ├── settings.py │ ├── test_application_views.py │ ├── test_decorators.py │ ├── test_models.py │ ├── test_password.py │ ├── test_auth_backends.py │ ├── test_client_credential.py │ └── test_rest_framework.py ├── templates │ ├── 404.html │ └── oauth2_provider │ │ ├── application_registration_form.html │ │ ├── application_confirm_delete.html │ │ ├── application_list.html │ │ ├── authorize.html │ │ ├── base.html │ │ ├── application_detail.html │ │ └── application_form.html ├── validators.py ├── views │ ├── __init__.py │ ├── generic.py │ ├── application.py │ └── base.py ├── admin.py ├── exceptions.py ├── backends.py ├── urls.py ├── compat.py ├── forms.py ├── middleware.py ├── generators.py ├── decorators.py ├── settings.py └── oauth2_backends.py ├── requirements ├── project.txt ├── optional.txt ├── testing.txt └── base.txt ├── .coveragerc ├── .pep8 ├── docs ├── views │ ├── class_based.rst │ ├── views.rst │ ├── application.rst │ ├── details.rst │ └── function_based.rst ├── models.rst ├── tutorial │ ├── tutorial.rst │ ├── tutorial_02.rst │ ├── tutorial_03.rst │ └── tutorial_01.rst ├── rest-framework │ ├── rest-framework.rst │ ├── permissions.rst │ └── getting_started.rst ├── install.rst ├── settings.rst ├── rfc.py ├── index.rst ├── glossary.rst ├── advanced_topics.rst ├── changelog.rst ├── contributing.rst └── Makefile ├── MANIFEST.in ├── AUTHORS ├── CONTRIBUTING.rst ├── runtests.py ├── .travis.yml ├── .gitignore ├── tox.ini ├── LICENSE ├── setup.py └── README.rst /example/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oauth2_provider/ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/example/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oauth2_provider/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn example.wsgi:application -------------------------------------------------------------------------------- /requirements/project.txt: -------------------------------------------------------------------------------- 1 | -r optional.txt 2 | Django>=1.4 -------------------------------------------------------------------------------- /requirements/optional.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | djangorestframework>=2.3 -------------------------------------------------------------------------------- /requirements/testing.txt: -------------------------------------------------------------------------------- 1 | -r optional.txt 2 | coverage==3.6 3 | mock 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = oauth2_provider 3 | omit = */migrations/* 4 | -------------------------------------------------------------------------------- /.pep8: -------------------------------------------------------------------------------- 1 | [pep8] 2 | max-line-length = 120 3 | exclude = docs,migrations,.tox 4 | -------------------------------------------------------------------------------- /docs/views/class_based.rst: -------------------------------------------------------------------------------- 1 | Class-based Views 2 | ================= 3 | 4 | -------------------------------------------------------------------------------- /example/example/templates/oauth2_provider/base.html: -------------------------------------------------------------------------------- 1 | {% extends "example/base.html" %} -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE 2 | recursive-include oauth2_provider/templates *.html 3 | -------------------------------------------------------------------------------- /docs/models.rst: -------------------------------------------------------------------------------- 1 | `Models` 2 | ======== 3 | 4 | .. automodule:: oauth2_provider.models 5 | :members: 6 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | Sphinx==1.2b1 2 | South==0.8.2 3 | oauthlib==0.6.1 4 | six==1.3.0 5 | django-braces==1.2.2 6 | -------------------------------------------------------------------------------- /oauth2_provider/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.7.1' 2 | 3 | __author__ = "Massimiliano Pippi & Federico Frenguelli" 4 | 5 | VERSION = __version__ # synonym 6 | -------------------------------------------------------------------------------- /docs/tutorial/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorials 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | tutorial_01 8 | tutorial_02 9 | tutorial_03 10 | -------------------------------------------------------------------------------- /oauth2_provider/ext/rest_framework/__init__.py: -------------------------------------------------------------------------------- 1 | from .authentication import OAuth2Authentication 2 | from .permissions import TokenHasScope, TokenHasReadWriteScope 3 | -------------------------------------------------------------------------------- /docs/rest-framework/rest-framework.rst: -------------------------------------------------------------------------------- 1 | Django Rest Framework 2 | --------------------- 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | getting_started 8 | permissions 9 | -------------------------------------------------------------------------------- /example/example/settings/production.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | # Parse database configuration from $DATABASE_URL 4 | import dj_database_url 5 | DATABASES['default'] = dj_database_url.config() 6 | -------------------------------------------------------------------------------- /example/example/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'ENGINE': 'django.db.backends.sqlite3', 6 | 'NAME': 'example.sqlite', 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | Massimiliano Pippi 5 | Federico Frenguelli 6 | 7 | Contributors 8 | ============ 9 | 10 | Stéphane Raimbault 11 | Emanuele Palazzetti 12 | David Fischer 13 | Ash Christopher 14 | -------------------------------------------------------------------------------- /oauth2_provider/tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from oauth2_provider.models import AbstractApplication 3 | 4 | 5 | class TestApplication(AbstractApplication): 6 | custom_field = models.CharField(max_length=255) 7 | -------------------------------------------------------------------------------- /oauth2_provider/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'oauth2_provider/base.html' %} 2 | 3 | {% block content %} 4 |

Not Found

5 |

The requested URL {{ request_path }} was not found on this server.

6 | {% endblock content %} -------------------------------------------------------------------------------- /docs/views/views.rst: -------------------------------------------------------------------------------- 1 | Using the views 2 | =============== 3 | 4 | Django OAuth Toolkit provides a set of pre-defined views for different purposes: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | function_based 10 | class_based 11 | application 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Thanks for your interest! We love contributions, so please feel free to fix bugs, improve things, provide documentation. Just `follow the 5 | guidelines `_ and submit a PR. 6 | -------------------------------------------------------------------------------- /example/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", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /oauth2_provider/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.validators import URLValidator 2 | 3 | 4 | def validate_uris(value): 5 | """ 6 | This validator ensures that `value` contains valid blank-separated urls" 7 | """ 8 | v = URLValidator() 9 | for uri in value.split(): 10 | v(uri) 11 | -------------------------------------------------------------------------------- /example/example/models.py: -------------------------------------------------------------------------------- 1 | from oauth2_provider.models import AbstractApplication 2 | 3 | from django.db import models 4 | 5 | 6 | class MyApplication(AbstractApplication): 7 | """ 8 | Custom Application model which adds description field 9 | """ 10 | description = models.TextField(blank=True) 11 | -------------------------------------------------------------------------------- /oauth2_provider/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import AuthorizationView, TokenView 2 | from .application import ApplicationRegistration, ApplicationDetail, ApplicationList, \ 3 | ApplicationDelete, ApplicationUpdate 4 | from .generic import ProtectedResourceView, ScopedProtectedResourceView, ReadWriteScopedResourceView 5 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | Django>1.4,<1.6 2 | South==0.8.1 3 | -e git+https://github.com/idan/oauthlib.git#egg=oauthlib 4 | https://github.com/evonove/django-oauth-toolkit/archive/master.zip 5 | six==1.3.0 6 | django-braces==1.0.0 7 | gunicorn==0.17.4 8 | dj-database-url==0.2.1 9 | requests==1.2.3 10 | psycopg2==2.5.1 11 | -------------------------------------------------------------------------------- /oauth2_provider/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | admin.autodiscover() 5 | 6 | 7 | urlpatterns = patterns( 8 | '', 9 | url(r'^admin/', include(admin.site.urls)), 10 | url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), 11 | ) 12 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | app_to_test = "oauth2_provider" 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oauth2_provider.tests.settings") 8 | 9 | from django.core.management import execute_from_command_line 10 | execute_from_command_line([sys.argv[0], "test", app_to_test]) 11 | -------------------------------------------------------------------------------- /oauth2_provider/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Grant, AccessToken, RefreshToken, get_application_model 4 | 5 | class RawIDAdmin(admin.ModelAdmin): 6 | raw_id_fields = ('user',) 7 | 8 | Application = get_application_model() 9 | 10 | admin.site.register(Application, RawIDAdmin) 11 | admin.site.register(Grant, RawIDAdmin) 12 | admin.site.register(AccessToken, RawIDAdmin) 13 | admin.site.register(RefreshToken, RawIDAdmin) 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: "2.7" 3 | 4 | env: 5 | - TOX_ENV=py26-django14 6 | - TOX_ENV=py26-django15 7 | - TOX_ENV=py26-django16 8 | - TOX_ENV=py27-django14 9 | - TOX_ENV=py27-django15 10 | - TOX_ENV=py27-django16 11 | - TOX_ENV=py33-django15 12 | - TOX_ENV=py33-django16 13 | - TOX_ENV=docs 14 | 15 | install: 16 | - pip install tox 17 | - pip install coveralls 18 | 19 | script: 20 | - tox -e $TOX_ENV 21 | 22 | after_success: 23 | - coveralls 24 | -------------------------------------------------------------------------------- /oauth2_provider/templates/oauth2_provider/application_registration_form.html: -------------------------------------------------------------------------------- 1 | {% extends "oauth2_provider/application_form.html" %} 2 | 3 | {% load i18n %} 4 | {% load url from future %} 5 | 6 | {% block app-form-title %}{% trans "Register a new application" %}{% endblock app-form-title %} 7 | 8 | {% block app-form-action-url %}{% url 'oauth2_provider:register' %}{% endblock app-form-action-url %} 9 | 10 | {% block app-form-back-url %}{% url "oauth2_provider:list" %}"{% endblock app-form-back-url %} -------------------------------------------------------------------------------- /oauth2_provider/exceptions.py: -------------------------------------------------------------------------------- 1 | class OAuthToolkitError(Exception): 2 | """ 3 | Base class for exceptions 4 | """ 5 | def __init__(self, error=None, redirect_uri=None, *args, **kwargs): 6 | super(OAuthToolkitError, self).__init__(*args, **kwargs) 7 | self.oauthlib_error = error 8 | 9 | if redirect_uri: 10 | self.oauthlib_error.redirect_uri = redirect_uri 11 | 12 | 13 | class FatalClientError(OAuthToolkitError): 14 | """ 15 | Class for critical errors 16 | """ 17 | pass 18 | -------------------------------------------------------------------------------- /docs/rest-framework/permissions.rst: -------------------------------------------------------------------------------- 1 | Permissions 2 | =========== 3 | 4 | Django OAuth Toolkit provides a few utility classes to use along with other permissions in Django REST Framework, 5 | so you can easily add scoped-based permission checks to your API views. 6 | 7 | 8 | 9 | TokenHasScope 10 | ------------- 11 | 12 | TODO: add docs for TokenHasScope permission class with usage examples 13 | 14 | 15 | TokenHasReadWriteScope 16 | ---------------------- 17 | 18 | TODO: add docs for TokenHasReadWriteScope permission class with usage examples 19 | -------------------------------------------------------------------------------- /oauth2_provider/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_generator import * 2 | from .test_validators import * 3 | from .test_models import * 4 | from .test_mixins import * 5 | from .test_authorization_code import * 6 | from .test_implicit import * 7 | from .test_password import * 8 | from .test_client_credential import * 9 | from .test_scopes import * 10 | from .test_rest_framework import * 11 | from .test_application_views import * 12 | from .test_decorators import * 13 | from .test_oauth2_backends import * 14 | from .test_auth_backends import * 15 | from .test_oauth2_validators import * 16 | -------------------------------------------------------------------------------- /docs/views/application.rst: -------------------------------------------------------------------------------- 1 | Application Views 2 | ================= 3 | 4 | A set of views is provided to let users handle application instances without accessing Django Admin 5 | Site. Application views are listed at the url `applications/` and you can register a new one at the 6 | url `applications/register`. You can override default templates located in 7 | `templates/oauth2_provider` folder and provide a custom layout. Every view provides access only to 8 | data belonging to the logged in user who performs the request. 9 | 10 | 11 | .. automodule:: oauth2_provider.views.application 12 | :members: 13 | -------------------------------------------------------------------------------- /oauth2_provider/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import base64 4 | 5 | 6 | class TestCaseUtils(object): 7 | def get_basic_auth_header(self, user, password): 8 | """ 9 | Return a dict containg the correct headers to set to make HTTP Basic Auth request 10 | """ 11 | user_pass = '{0}:{1}'.format(user, password) 12 | auth_string = base64.b64encode(user_pass.encode('utf-8')) 13 | auth_headers = { 14 | 'HTTP_AUTHORIZATION': 'Basic ' + auth_string.decode("utf-8"), 15 | } 16 | 17 | return auth_headers 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # PyCharm stuff 39 | .idea 40 | 41 | # Sphinx build dir 42 | _build 43 | 44 | # Sqlite database files 45 | *.sqlite 46 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Install with pip 5 | 6 | pip install django-oauth-toolkit 7 | 8 | Add `oauth2_provider` to your `INSTALLED_APPS` 9 | 10 | .. code-block:: python 11 | 12 | INSTALLED_APPS = ( 13 | ... 14 | 'oauth2_provider', 15 | ) 16 | 17 | 18 | If you need an OAuth2 provider you'll want to add the following to your urls.py 19 | 20 | .. code-block:: python 21 | 22 | urlpatterns = patterns( 23 | ... 24 | url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), 25 | ) 26 | 27 | Next step is our :doc:`first tutorial `. -------------------------------------------------------------------------------- /oauth2_provider/tests/test_oauth2_backends.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, RequestFactory 2 | 3 | 4 | from ..backends import get_oauthlib_core 5 | 6 | 7 | class TestOAuthLibCore(TestCase): 8 | def setUp(self): 9 | self.factory = RequestFactory() 10 | 11 | def test_validate_authorization_request_unsafe_query(self): 12 | auth_headers = { 13 | 'HTTP_AUTHORIZATION': 'Bearer ' + "a_casual_token", 14 | } 15 | request = self.factory.get("/fake-resource?next=/fake", **auth_headers) 16 | 17 | oauthlib_core = get_oauthlib_core() 18 | oauthlib_core.verify_request(request, scopes=[]) 19 | -------------------------------------------------------------------------------- /oauth2_provider/tests/test_validators.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.test import TestCase 4 | from django.core.validators import ValidationError 5 | 6 | from ..validators import validate_uris 7 | 8 | 9 | class TestValidators(TestCase): 10 | def test_validate_good_uris(self): 11 | good_urls = 'http://example.com/ http://example.it/?key=val' 12 | # Check ValidationError not thrown 13 | validate_uris(good_urls) 14 | 15 | def test_validate_bad_uris(self): 16 | bad_urls = 'http://example.com http://example' 17 | self.assertRaises(ValidationError, validate_uris, bad_urls) 18 | -------------------------------------------------------------------------------- /example/example/templates/example/login.html: -------------------------------------------------------------------------------- 1 | {% extends "example/base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% if form.errors %} 6 |

Your username and password didn't match. Please try again.

7 | {% endif %} 8 | 9 |
10 | {% csrf_token %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
21 | 22 | 23 | 24 |
25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /docs/views/details.rst: -------------------------------------------------------------------------------- 1 | Views code and details 2 | ====================== 3 | 4 | 5 | Generic 6 | ------- 7 | Generic views are intended to use in a "batteries included" fashion to protect own views with OAuth2 authentication and 8 | Scopes handling. 9 | 10 | .. automodule:: oauth2_provider.views.generic 11 | :members: 12 | 13 | Mixins 14 | ------ 15 | These views are mainly for internal use, but advanced users may use them as basic components to customize OAuth2 logic 16 | inside their Django applications. 17 | 18 | .. automodule:: oauth2_provider.views.mixins 19 | :members: 20 | 21 | Base 22 | ---- 23 | Views needed to implement the main OAuth2 authorization flows supported by Django OAuth Toolkit. 24 | 25 | .. automodule:: oauth2_provider.views.base 26 | :members: 27 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | Our configurations are all namespaced under the `OAUTH2_PROVIDER` settings with the solely exception of 5 | `OAUTH2_PROVIDER_APPLICATION_MODEL`: this is because of the way Django currently implements 6 | swappable models. See issue #90 (https://github.com/evonove/django-oauth-toolkit/issues/90) for details. 7 | 8 | For example: 9 | 10 | .. code-block:: python 11 | 12 | OAUTH2_PROVIDER = { 13 | 'SCOPES': { 14 | 'read': 'Read scope', 15 | 'write': 'Write scope', 16 | }, 17 | 18 | 'CLIENT_ID_GENERATOR_CLASS': 'oauth2_provider.generators.ClientIdGenerator', 19 | 20 | } 21 | 22 | 23 | TODO: add reference documentation for DOT settings 24 | 25 | A big *thank you* to the guys from Django REST Framework for inspiring this. -------------------------------------------------------------------------------- /oauth2_provider/backends.py: -------------------------------------------------------------------------------- 1 | from .compat import get_user_model 2 | from .oauth2_backends import get_oauthlib_core 3 | 4 | UserModel = get_user_model() 5 | OAuthLibCore = get_oauthlib_core() 6 | 7 | 8 | class OAuth2Backend(object): 9 | """ 10 | Authenticate against an OAuth2 access token 11 | """ 12 | 13 | def authenticate(self, **credentials): 14 | request = credentials.get('request') 15 | if request is not None: 16 | oauthlib_core = get_oauthlib_core() 17 | valid, r = oauthlib_core.verify_request(request, scopes=[]) 18 | if valid: 19 | return r.user 20 | return None 21 | 22 | def get_user(self, user_id): 23 | try: 24 | return UserModel.objects.get(pk=user_id) 25 | except UserModel.DoesNotExist: 26 | return None 27 | -------------------------------------------------------------------------------- /oauth2_provider/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.conf.urls import patterns, url 3 | 4 | from . import views 5 | 6 | urlpatterns = patterns( 7 | '', 8 | url(r'^authorize/$', views.AuthorizationView.as_view(), name="authorize"), 9 | url(r'^token/$', views.TokenView.as_view(), name="token"), 10 | ) 11 | 12 | # Application management views 13 | urlpatterns += patterns( 14 | '', 15 | url(r'^applications/$', views.ApplicationList.as_view(), name="list"), 16 | url(r'^applications/register/$', views.ApplicationRegistration.as_view(), name="register"), 17 | url(r'^applications/(?P\d+)/$', views.ApplicationDetail.as_view(), name="detail"), 18 | url(r'^applications/(?P\d+)/delete/$', views.ApplicationDelete.as_view(), name="delete"), 19 | url(r'^applications/(?P\d+)/update/$', views.ApplicationUpdate.as_view(), name="update"), 20 | ) 21 | -------------------------------------------------------------------------------- /oauth2_provider/templates/oauth2_provider/application_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "oauth2_provider/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load url from future %} 5 | {% block content %} 6 |
7 |

{% trans "Are you sure to delete the application" %} {{ application.name }}?

8 |
9 | {% csrf_token %} 10 | 11 |
12 |
13 | {% trans "Cancel" %} 14 | 15 |
16 |
17 |
18 |
19 | {% endblock content %} -------------------------------------------------------------------------------- /oauth2_provider/templates/oauth2_provider/application_list.html: -------------------------------------------------------------------------------- 1 | {% extends "oauth2_provider/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load url from future %} 5 | {% block content %} 6 |
7 |

{% trans "Your applications" %}

8 | {% if applications %} 9 | 14 | 15 | New Application 16 | {% else %} 17 |

{% trans "No applications defined" %}. {% trans "Click here" %} {% trans "if you want to register a new one" %}

18 | {% endif %} 19 |
20 | {% endblock content %} -------------------------------------------------------------------------------- /oauth2_provider/ext/rest_framework/authentication.py: -------------------------------------------------------------------------------- 1 | from rest_framework.authentication import BaseAuthentication 2 | 3 | from ...oauth2_backends import get_oauthlib_core 4 | 5 | 6 | class OAuth2Authentication(BaseAuthentication): 7 | """ 8 | OAuth 2 authentication backend using `django-oauth-toolkit` 9 | """ 10 | www_authenticate_realm = 'api' 11 | 12 | def authenticate(self, request): 13 | """ 14 | Returns two-tuple of (user, token) if authentication succeeds, 15 | or None otherwise. 16 | """ 17 | oauthlib_core = get_oauthlib_core() 18 | valid, r = oauthlib_core.verify_request(request, scopes=[]) 19 | if valid: 20 | return r.user, r.access_token 21 | else: 22 | return None 23 | 24 | def authenticate_header(self, request): 25 | """ 26 | Bearer is the only finalized type currently 27 | """ 28 | return 'Bearer realm="%s"' % self.www_authenticate_realm 29 | -------------------------------------------------------------------------------- /oauth2_provider/compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | The `compat` module provides support for backwards compatibility with older 3 | versions of django and python.. 4 | """ 5 | 6 | from __future__ import unicode_literals 7 | 8 | import django 9 | from django.conf import settings 10 | 11 | # urlparse in python3 has been renamed to urllib.parse 12 | try: 13 | from urlparse import urlparse, parse_qs, urlunparse 14 | except ImportError: 15 | from urllib.parse import urlparse, parse_qs, urlunparse 16 | 17 | try: 18 | from urllib import urlencode, unquote_plus 19 | except ImportError: 20 | from urllib.parse import urlencode, unquote_plus 21 | 22 | # Django 1.5 add support for custom auth user model 23 | if django.VERSION >= (1, 5): 24 | AUTH_USER_MODEL = settings.AUTH_USER_MODEL 25 | else: 26 | AUTH_USER_MODEL = 'auth.User' 27 | 28 | try: 29 | from django.contrib.auth import get_user_model 30 | except ImportError: 31 | from django.contrib.auth.models import User 32 | get_user_model = lambda: User 33 | -------------------------------------------------------------------------------- /oauth2_provider/views/generic.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import View 2 | 3 | from oauthlib.oauth2 import Server 4 | 5 | from ..settings import oauth2_settings 6 | from .mixins import ProtectedResourceMixin, ScopedResourceMixin, ReadWriteScopedResourceMixin 7 | 8 | 9 | class ProtectedResourceView(ProtectedResourceMixin, View): 10 | """ 11 | Generic view protecting resources by providing OAuth2 authentication out of the box 12 | """ 13 | server_class = Server 14 | validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS 15 | 16 | 17 | class ScopedProtectedResourceView(ScopedResourceMixin, ProtectedResourceView): 18 | """ 19 | Generic view protecting resources by providing OAuth2 authentication and Scopes handling out of the box 20 | """ 21 | pass 22 | 23 | 24 | class ReadWriteScopedResourceView(ReadWriteScopedResourceMixin, ProtectedResourceView): 25 | """ 26 | Generic view protecting resources with OAuth2 authentication and read/write scopes. 27 | GET, HEAD, OPTIONS http methods require "read" scope. Otherwise "write" scope is required. 28 | """ 29 | pass 30 | -------------------------------------------------------------------------------- /oauth2_provider/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import get_application_model 4 | 5 | 6 | class AllowForm(forms.Form): 7 | allow = forms.BooleanField(required=False) 8 | redirect_uri = forms.CharField(widget=forms.HiddenInput()) 9 | scope = forms.CharField(required=False, widget=forms.HiddenInput()) 10 | client_id = forms.CharField(widget=forms.HiddenInput()) 11 | state = forms.CharField(required=False, widget=forms.HiddenInput()) 12 | response_type = forms.CharField(widget=forms.HiddenInput()) 13 | 14 | def __init__(self, *args, **kwargs): 15 | data = kwargs.get('data') 16 | # backwards compatible support for plural `scopes` query parameter 17 | if data and 'scopes' in data: 18 | data['scope'] = data['scopes'] 19 | return super(AllowForm, self).__init__(*args, **kwargs) 20 | 21 | 22 | class RegistrationForm(forms.ModelForm): 23 | """ 24 | TODO: add docstring 25 | """ 26 | class Meta: 27 | model = get_application_model() 28 | fields = ('name', 'client_id', 'client_secret', 'client_type', 'authorization_grant_type', 'redirect_uris') 29 | -------------------------------------------------------------------------------- /example/example/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class ConsumerForm(forms.Form): 5 | client_id = forms.CharField() 6 | authorization_url = forms.URLField() 7 | 8 | 9 | class ConsumerExchangeForm(forms.Form): 10 | code = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) 11 | state = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) 12 | token_url = forms.URLField() 13 | grant_type = forms.CharField(widget=forms.HiddenInput(), initial='authorization_code') 14 | redirect_url = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) 15 | client_id = forms.CharField() 16 | client_secret = forms.CharField() 17 | 18 | 19 | class AccessTokenDataForm(forms.Form): 20 | access_token = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) 21 | token_type = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) 22 | expires_in = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) 23 | refresh_token = forms.CharField(widget=forms.TextInput(attrs={'readonly': 'readonly'})) 24 | token_url = forms.URLField() 25 | client_id = forms.CharField() 26 | client_secret = forms.CharField() 27 | -------------------------------------------------------------------------------- /docs/rfc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom Sphinx documentation module to link to parts of the OAuth2 RFC. 3 | """ 4 | from docutils import nodes, utils 5 | 6 | base_url = "http://tools.ietf.org/html/rfc6749" 7 | 8 | 9 | def rfclink(name, rawtext, text, lineno, inliner, options={}, content=[]): 10 | """Link to the OAuth2 draft. 11 | 12 | Returns 2 part tuple containing list of nodes to insert into the 13 | document and a list of system messages. Both are allowed to be 14 | empty. 15 | 16 | :param name: The role name used in the document. 17 | :param rawtext: The entire markup snippet, with role. 18 | :param text: The text marked with the role. 19 | :param lineno: The line number where rawtext appears in the input. 20 | :param inliner: The inliner instance that called us. 21 | :param options: Directive options for customization. 22 | :param content: The directive content for customization. 23 | """ 24 | 25 | node = nodes.reference(rawtext, "RFC6749 Section " + text, refuri="%s#section-%s" % (base_url, text)) 26 | 27 | return [node], [] 28 | 29 | 30 | def setup(app): 31 | """ 32 | Install the plugin. 33 | 34 | :param app: Sphinx application context. 35 | """ 36 | app.add_role('rfc', rfclink) 37 | return 38 | -------------------------------------------------------------------------------- /oauth2_provider/middleware.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate 2 | 3 | 4 | class OAuth2TokenMiddleware(object): 5 | """ 6 | Middleware for OAuth2 user authentication 7 | 8 | This middleware is able to work along with AuthenticationMiddleware and its behaviour depends 9 | on the order it's processed with. 10 | 11 | If it comes *after* AuthenticationMiddleware and request.user is valid, leave it as is and does 12 | not proceed with token validation. If request.user is the Anonymous user proceeds and try to 13 | authenticate the user using the OAuth2 access token. 14 | 15 | If it comes *before* AuthenticationMiddleware, or AuthenticationMiddleware is not used at all, 16 | tries to authenticate user with the OAuth2 access token and set request.user field. Setting 17 | also request._cached_user field makes AuthenticationMiddleware use that instead of the one from 18 | the session. 19 | """ 20 | def process_request(self, request): 21 | # do something only if request contains a Bearer token 22 | if request.META.get('HTTP_AUTHORIZATION', '').startswith('Bearer'): 23 | if not hasattr(request, 'user') or request.user.is_anonymous(): 24 | user = authenticate(request=request) 25 | if user: 26 | request.user = request._cached_user = user 27 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py26-django14, py26-django15, py26-django16, 4 | py27-django14, py27-django15, py27-django16, 5 | py33-django15, py33-django16, 6 | docs 7 | 8 | [testenv] 9 | downloadcache = {toxworkdir}/cache/ 10 | commands=coverage run -a runtests.py 11 | deps = 12 | -r{toxinidir}/requirements/testing.txt 13 | 14 | [testenv:py26-django14] 15 | basepython = python2.6 16 | deps = 17 | Django<1.5 18 | {[testenv]deps} 19 | 20 | [testenv:py26-django15] 21 | basepython = python2.6 22 | deps = 23 | Django<1.6 24 | {[testenv]deps} 25 | 26 | [testenv:py26-django16] 27 | basepython = python2.6 28 | deps = 29 | Django<1.7 30 | {[testenv]deps} 31 | 32 | [testenv:py27-django14] 33 | basepython = python2.7 34 | deps = 35 | Django<1.5 36 | {[testenv]deps} 37 | 38 | [testenv:py27-django15] 39 | basepython = python2.7 40 | deps = 41 | Django<1.6 42 | {[testenv]deps} 43 | 44 | [testenv:py33-django15] 45 | basepython = python3.3 46 | deps = 47 | Django<1.6 48 | {[testenv]deps} 49 | 50 | [testenv:py27-django16] 51 | basepython = python2.7 52 | deps = 53 | Django<1.7 54 | {[testenv]deps} 55 | 56 | [testenv:py33-django16] 57 | basepython = python3.3 58 | deps = 59 | Django<1.7 60 | {[testenv]deps} 61 | 62 | [testenv:docs] 63 | basepython=python 64 | changedir=docs 65 | deps=sphinx 66 | commands=make html -------------------------------------------------------------------------------- /oauth2_provider/tests/test_generator.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.test import TestCase 4 | 5 | from ..settings import oauth2_settings 6 | from ..generators import (BaseHashGenerator, ClientIdGenerator, ClientSecretGenerator, 7 | generate_client_id, generate_client_secret) 8 | 9 | 10 | class MockHashGenerator(BaseHashGenerator): 11 | def hash(self): 12 | return 42 13 | 14 | 15 | class TestGenerators(TestCase): 16 | def tearDown(self): 17 | oauth2_settings.CLIENT_ID_GENERATOR_CLASS = ClientIdGenerator 18 | oauth2_settings.CLIENT_SECRET_GENERATOR_CLASS = ClientSecretGenerator 19 | 20 | def test_generate_client_id(self): 21 | g = oauth2_settings.CLIENT_ID_GENERATOR_CLASS() 22 | self.assertEqual(len(g.hash()), 40) 23 | 24 | oauth2_settings.CLIENT_ID_GENERATOR_CLASS = MockHashGenerator 25 | self.assertEqual(generate_client_id(), 42) 26 | 27 | def test_generate_secret_id(self): 28 | g = oauth2_settings.CLIENT_SECRET_GENERATOR_CLASS() 29 | self.assertEqual(len(g.hash()), 128) 30 | 31 | oauth2_settings.CLIENT_SECRET_GENERATOR_CLASS = MockHashGenerator 32 | self.assertEqual(generate_client_secret(), 42) 33 | 34 | def test_basegen_misuse(self): 35 | g = BaseHashGenerator() 36 | self.assertRaises(NotImplementedError, g.hash) 37 | -------------------------------------------------------------------------------- /example/example/fixtures/oauth2_provider_fixtures.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 2, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "test", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": false, 11 | "is_staff": false, 12 | "groups": [], 13 | "user_permissions": [], 14 | "password": "pbkdf2_sha256$10000$bkV6jtZeivL0$EKBSPj1VvcysvZzejhcTvlUkWC+qFWlw2lLtStU9+GE=", 15 | "email": "", 16 | "date_joined": "2013-08-31T14:16:25.509Z" 17 | } 18 | }, 19 | 20 | { 21 | "pk": 1, 22 | "model": "example.myapplication", 23 | "fields": { 24 | "redirect_uris": "", 25 | "name": "test_app", 26 | "description": "", 27 | "client_type": "public", 28 | "user": 1, 29 | "client_id": "client_id", 30 | "client_secret": "client_secret", 31 | "authorization_grant_type": "password" 32 | } 33 | }, 34 | 35 | { 36 | "pk": 1, 37 | "model": "oauth2_provider.accesstoken", 38 | "fields": { 39 | "application": 1, 40 | "token": "test_access_token", 41 | "expires": "2023-09-06T20:29:49Z", 42 | "user": 2, 43 | "scope": "can_create_application" 44 | } 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django OAuth Toolkit documentation master file, created by 2 | sphinx-quickstart on Mon May 20 19:40:43 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Django OAuth Toolkit Documentation 7 | ============================================= 8 | 9 | Django OAuth Toolkit can help you providing out of the box all the endpoints, data and logic needed to add OAuth2 10 | capabilities to your Django projects. Django OAuth Toolkit makes extensive use of the excellent 11 | `OAuthLib `_, so that everything is 12 | `rfc-compliant `_. 13 | 14 | See our :doc:`Changelog ` for information on updates. 15 | 16 | Support 17 | ------- 18 | 19 | If you need support please send a message to the `Django OAuth Toolkit Google Group `_ 20 | 21 | Requirements 22 | ------------ 23 | 24 | * Python 2.7, 3.3 25 | * Django 1.4, 1.5, 1.6 26 | 27 | Index 28 | ===== 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | 33 | install 34 | tutorial/tutorial 35 | rest-framework/rest-framework 36 | views/views 37 | views/details 38 | models 39 | advanced_topics 40 | settings 41 | glossary 42 | 43 | .. toctree:: 44 | :maxdepth: 1 45 | 46 | contributing 47 | changelog 48 | 49 | 50 | Indices and tables 51 | ================== 52 | 53 | * :ref:`genindex` 54 | * :ref:`modindex` 55 | -------------------------------------------------------------------------------- /oauth2_provider/generators.py: -------------------------------------------------------------------------------- 1 | from oauthlib.common import generate_client_id as oauthlib_generate_client_id 2 | 3 | from .settings import oauth2_settings 4 | 5 | 6 | CLIENT_ID_CHARACTER_SET = r'_-.:;=?!@0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 7 | 8 | 9 | class BaseHashGenerator(object): 10 | """ 11 | All generators should extend this class overriding `.hash()` method. 12 | """ 13 | def hash(self): 14 | raise NotImplementedError() 15 | 16 | 17 | class ClientIdGenerator(BaseHashGenerator): 18 | def hash(self): 19 | """ 20 | Generate a client_id without colon char as in http://tools.ietf.org/html/rfc2617#section-2 21 | for Basic Authentication scheme 22 | """ 23 | client_id_charset = CLIENT_ID_CHARACTER_SET.replace(":", "") 24 | return oauthlib_generate_client_id(length=40, chars=client_id_charset) 25 | 26 | 27 | class ClientSecretGenerator(BaseHashGenerator): 28 | def hash(self): 29 | return oauthlib_generate_client_id(length=128, chars=CLIENT_ID_CHARACTER_SET) 30 | 31 | 32 | def generate_client_id(): 33 | """ 34 | Generate a suitable client id 35 | """ 36 | client_id_generator = oauth2_settings.CLIENT_ID_GENERATOR_CLASS() 37 | return client_id_generator.hash() 38 | 39 | 40 | def generate_client_secret(): 41 | """ 42 | Generate a suitable client secret 43 | """ 44 | client_secret_generator = oauth2_settings.CLIENT_SECRET_GENERATOR_CLASS() 45 | return client_secret_generator.hash() 46 | -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 19 | # if running multiple sites in the same mod_wsgi process. To fix this, use 20 | # mod_wsgi daemon mode with each site in its own daemon process, or use 21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings" 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 23 | 24 | # This application object is used by any WSGI server configured to use this 25 | # file. This includes Django's development server, if the WSGI_APPLICATION 26 | # setting points here. 27 | from django.core.wsgi import get_wsgi_application 28 | application = get_wsgi_application() 29 | 30 | # Apply WSGI middleware here. 31 | # from helloworld.wsgi import HelloWorldApplication 32 | # application = HelloWorldApplication(application) 33 | -------------------------------------------------------------------------------- /oauth2_provider/templates/oauth2_provider/authorize.html: -------------------------------------------------------------------------------- 1 | {% extends "oauth2_provider/base.html" %} 2 | 3 | {% load i18n %} 4 | {% block content %} 5 |
6 | {% if not error %} 7 |
8 |

{% trans "Authorize" %} {{ application.name }}?

9 | {% csrf_token %} 10 | 11 | {% for field in form %} 12 | {% if field.is_hidden %} 13 | {{ field }} 14 | {% endif %} 15 | {% endfor %} 16 | 17 |

{% trans "Application requires following permissions" %}

18 |
    19 | {% for scope in scopes_descriptions %} 20 |
  • {{ scope }}
  • 21 | {% endfor %} 22 |
23 | 24 | {{ form.errors }} 25 | {{ form.non_field_errors }} 26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 |
34 | 35 | {% else %} 36 |

Error: {{ error.error }}

37 |

{{ error.description }}

38 | {% endif %} 39 |
40 | {% endblock %} -------------------------------------------------------------------------------- /oauth2_provider/templates/oauth2_provider/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock title %} 6 | 7 | 8 | 9 | 10 | {% block css %} 11 | {{ block.super }} 12 | 13 | {% endblock css %} 14 | 15 | 39 | 40 | 41 | 42 | 43 |
44 | {% block content %} 45 | {% endblock content %} 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Massimiliano Pippi, Federico Frenguelli and contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. 27 | -------------------------------------------------------------------------------- /example/example/templates/example/consumer.html: -------------------------------------------------------------------------------- 1 | {% extends "example/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block content %} 5 |

Test your OAuth2 provider, I'll be your consumer

6 | {% if authorization_link %} 7 |

Ok, here is the link you have to use to reach your authorization page and beg for an authorization 8 | token

9 |

Now click, give your authorization and see you later, possibly with an access token

10 | {{ authorization_link }} 11 | {% else %} 12 |

(At this point, you should have created an Application instance on your side, up and running.)

13 | {% if not error %} 14 |
15 | {{ form.non_field_errors }} 16 |
17 | Build an authorization link for your provider 18 | {{ form.client_id.errors }} 19 | 20 | {{ form.client_id }} 21 | Your Application's client_id field. 22 | {{ form.authorization_url.errors }} 23 | 24 | {{ form.authorization_url }} 25 | 26 | The url to the authorization page, it's ok if it points to localhost 27 | (e.g. http://localhost:8000/o/authorize). 28 | 29 | 30 |
31 | {% csrf_token %} 32 |
33 | {% endif %} 34 | {% endif %} 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | import os 6 | import re 7 | 8 | 9 | def get_version(package): 10 | """ 11 | Return package version as listed in `__version__` in `init.py`. 12 | """ 13 | init_py = open(os.path.join(package, '__init__.py')).read() 14 | return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) 15 | 16 | 17 | version = get_version('oauth2_provider') 18 | 19 | 20 | LONG_DESCRIPTION = open('README.rst').read() 21 | 22 | setup( 23 | name="django-oauth-toolkit", 24 | version=version, 25 | description="OAuth2 goodies for Django", 26 | long_description=LONG_DESCRIPTION, 27 | classifiers=[ 28 | "Development Status :: 4 - Beta", 29 | "Environment :: Web Environment", 30 | "Framework :: Django", 31 | "License :: OSI Approved :: BSD License", 32 | "Operating System :: OS Independent", 33 | "Programming Language :: Python :: 2.6", 34 | "Programming Language :: Python :: 2.7", 35 | "Programming Language :: Python :: 3.3", 36 | "Topic :: Software Development :: Libraries :: Python Modules", 37 | ], 38 | keywords='django oauth oauth2 oauthlib', 39 | author="Federico Frenguelli, Massimiliano Pippi", 40 | author_email='synasius@gmail.com, mpippi@gmail.com', 41 | url='https://github.com/evonove/django-oauth-toolkit', 42 | license='BSD', 43 | packages=find_packages(), 44 | include_package_data=True, 45 | test_suite='runtests', 46 | install_requires=[ 47 | 'django>=1.4', 48 | 'django-braces>=1.2.2', 49 | 'six>=1.3.0', 50 | 'oauthlib==0.6.1', 51 | ], 52 | zip_safe=False, 53 | ) 54 | -------------------------------------------------------------------------------- /oauth2_provider/templates/oauth2_provider/application_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "oauth2_provider/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load url from future %} 5 | {% block content %} 6 |
7 |

{{ application.name }}

8 | 9 |
    10 |
  • 11 |

    {% trans "Client id" %}

    12 | 13 |
  • 14 | 15 |
  • 16 |

    {% trans "Client secret" %}

    17 | 18 |
  • 19 | 20 |
  • 21 |

    {% trans "Client type" %}

    22 |

    {{ application.client_type }}

    23 |
  • 24 | 25 |
  • 26 |

    {% trans "Authorization Grant Type" %}

    27 |

    {{ application.authorization_grant_type }}

    28 |
  • 29 | 30 |
  • 31 |

    {% trans "Redirect Uris" %}

    32 | 33 |
  • 34 |
35 | 36 | 41 |
42 | {% endblock content %} -------------------------------------------------------------------------------- /oauth2_provider/templates/oauth2_provider/application_form.html: -------------------------------------------------------------------------------- 1 | {% extends "oauth2_provider/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load url from future %} 5 | {% block content %} 6 |
7 |
8 |

9 | {% block app-form-title %} 10 | {% trans "Edit application" %} {{ application.name }} 11 | {% endblock app-form-title %} 12 |

13 | {% csrf_token %} 14 | 15 | {% for field in form %} 16 |
17 | 18 |
19 | {{ field }} 20 | {% for error in field.errors %} 21 | {{ error }} 22 | {% endfor %} 23 |
24 |
25 | {% endfor %} 26 | 27 |
28 | {% for error in form.non_field_errors %} 29 | {{ error }} 30 | {% endfor %} 31 |
32 | 33 |
34 | 40 |
41 |
42 |
43 | {% endblock %} -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | Glossary 2 | ======== 3 | 4 | .. Put definition of specific terms here, and reference them inside docs with :term:`My term` syntax 5 | 6 | .. glossary:: 7 | 8 | Authorization Server 9 | The authorization server asks resource owners for their consensus to let client applications access their data. 10 | It also manages and issues the tokens needed for all the authorization flows supported by OAuth2 spec. 11 | Usually the same application offering resources through an OAuth2-protected API also behaves like an 12 | authorization server. 13 | 14 | Resource Server 15 | An application providing access to its own resources through an API protected following the OAuth2 spec. 16 | 17 | Application 18 | TODO 19 | 20 | Client 21 | A client is an application authorized to access OAuth2-protected resources on behalf and with the authorization 22 | of the resource owner. 23 | 24 | Resource Owner 25 | The user of an application which exposes resources to third party applications through OAuth2. The 26 | resource owner must give her authorization for third party applications to be able to access her data. 27 | 28 | Access Token 29 | A token needed to access resources protected by OAuth2. It has a lifetime which is usually quite short. 30 | 31 | Authorization Code 32 | The authorization code is obtained by using an authorization server as an intermediary between the client and 33 | resource owner. It is used to authenticate the client and grant the transmission of the Access Token. 34 | 35 | Authorization Token 36 | A token the authorization server issues to clients that can be swapped for an access token. It has a very short 37 | lifetime since the swap has to be performed shortly after users provide their authorization. 38 | 39 | Refresh Token 40 | A token the authorization server may issue to clients and can be swapped for a brand new access token, without 41 | repeating the authorization process. It has no expire time. -------------------------------------------------------------------------------- /example/example/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | All responses will have Access-Control-Allow-Origin, and Access-Control-Allow-Methods 3 | header items. 4 | 5 | If a request has Access-Control-Request-Methods in the header, then an 6 | HttpResponse object is returned with header containing Access-Control-Allow-Origin, 7 | Access-Control-Allow-Methods, and Access-Control-Allow-Headers items. 8 | 9 | """ 10 | from django import http 11 | from django.conf import settings 12 | 13 | 14 | XS_SHARING_ALLOWED_ORIGINS = getattr(settings, "XS_SHARING_ALLOWED_ORIGINS", '*') 15 | XS_SHARING_ALLOWED_METHODS = getattr(settings, "XS_SHARING_ALLOWED_METHODS", ['POST', 'GET', 'OPTIONS', 'PUT', 'DELETE']) 16 | XS_SHARING_ALLOWED_HEADERS = getattr(settings, "XS_SHARING_ALLOWED_HEADERS", ['x-requested-with', 'content-type', 'accept', 'origin', 'authorization']) 17 | 18 | 19 | class XsSharingMiddleware(object): 20 | """ 21 | This middleware allows cross-domain XHR using the html5 postMessage API. 22 | 23 | eg. 24 | Access-Control-Allow-Origin: http://api.example.com 25 | Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE 26 | Access-Control-Allow-Headers: ["Content-Type"] 27 | 28 | """ 29 | def process_request(self, request): 30 | 31 | if 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' in request.META: 32 | response = http.HttpResponse() 33 | response['Access-Control-Allow-Origin'] = XS_SHARING_ALLOWED_ORIGINS 34 | response['Access-Control-Allow-Methods'] = ",".join(XS_SHARING_ALLOWED_METHODS) 35 | response['Access-Control-Allow-Headers'] = ",".join(XS_SHARING_ALLOWED_HEADERS) 36 | return response 37 | 38 | return None 39 | 40 | def process_response(self, request, response): 41 | # Avoid unnecessary work 42 | if response.has_header('Access-Control-Allow-Origin'): 43 | return response 44 | 45 | response['Access-Control-Allow-Origin'] = XS_SHARING_ALLOWED_ORIGINS 46 | response['Access-Control-Allow-Methods'] = ",".join(XS_SHARING_ALLOWED_METHODS) 47 | 48 | return response 49 | -------------------------------------------------------------------------------- /oauth2_provider/ext/rest_framework/permissions.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core.exceptions import ImproperlyConfigured 4 | 5 | from rest_framework.permissions import BasePermission 6 | 7 | from ...settings import oauth2_settings 8 | 9 | 10 | log = logging.getLogger('oauth2_provider') 11 | 12 | SAFE_HTTP_METHODS = ['GET', 'HEAD', 'OPTIONS'] 13 | 14 | 15 | class TokenHasScope(BasePermission): 16 | """ 17 | The request is authenticated as a user and the token used has the right scope 18 | """ 19 | 20 | def has_permission(self, request, view): 21 | token = request.auth 22 | 23 | if not token: 24 | return False 25 | 26 | if hasattr(token, 'scope'): # OAuth 2 27 | required_scopes = self.get_scopes(request, view) 28 | log.debug("Required scopes to access resource: {0}".format(required_scopes)) 29 | 30 | return token.is_valid(required_scopes) 31 | 32 | assert False, ('TokenHasScope requires either the' 33 | '`oauth2_provider.rest_framework.OAuth2Authentication` authentication ' 34 | 'class to be used.') 35 | 36 | def get_scopes(self, request, view): 37 | try: 38 | return getattr(view, 'required_scopes') 39 | except AttributeError: 40 | raise ImproperlyConfigured( 41 | 'TokenHasScope requires the view to define the required_scopes attribute') 42 | 43 | 44 | class TokenHasReadWriteScope(TokenHasScope): 45 | """ 46 | The request is authenticated as a user and the token used has the right scope 47 | """ 48 | 49 | def get_scopes(self, request, view): 50 | try: 51 | required_scopes = super(TokenHasReadWriteScope, self).get_scopes(request, view) 52 | except ImproperlyConfigured: 53 | required_scopes = [] 54 | 55 | # TODO: code duplication!! see dispatch in ReadWriteScopedResourceMixin 56 | if request.method.upper() in SAFE_HTTP_METHODS: 57 | read_write_scope = oauth2_settings.READ_SCOPE 58 | else: 59 | read_write_scope = oauth2_settings.WRITE_SCOPE 60 | 61 | return required_scopes + [read_write_scope] 62 | -------------------------------------------------------------------------------- /example/example/templates/example/home.html: -------------------------------------------------------------------------------- 1 | {% extends "example/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block content %} 5 | 6 |
7 |

Hello, OAuth!

8 |

9 | Welcome to the OAuth Playground, an utility to show off and test Django OAuth Toolkit's capabilities. 10 | This app is particularly useful to complete some of the tutorials. 11 |

12 |

13 | Github Repo » 14 | Official Docs » 15 |

16 |
17 | 18 |
19 |
20 |

OAuth2 Consumer

21 |

Authorization Code flow

22 |

Do you have an OAuth2 provider implementing the Authorization Code flow and want to test how it goes?

23 |

Use me as a Consumer! »

24 |
25 |
26 |

OAuth2 Provider

27 |

Create your own app on the playground

28 |

29 | Login with test/test username and password 30 | and create your own app, 31 | then play with the test APIs exposed by the playground. 32 |

33 |

Use me as a Provider! »

34 |
35 |
36 | 37 |
38 | 39 |
40 |

41 |   Django OAuth Toolkit version {{version}}

42 |
43 | 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /example/example/templates/example/consumer-client.html: -------------------------------------------------------------------------------- 1 | {% extends "example/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block content %} 5 |

I'll let you know, I'm a supersimple API client...

6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 |

14 | {% endblock %}
15 | 
16 | 
17 | {% block javascript %}
18 |     
62 | {% endblock javascript %}


--------------------------------------------------------------------------------
/oauth2_provider/views/application.py:
--------------------------------------------------------------------------------
 1 | from django.core.urlresolvers import reverse_lazy
 2 | from django.views.generic import CreateView, DetailView, DeleteView, ListView, UpdateView
 3 | 
 4 | from braces.views import LoginRequiredMixin
 5 | 
 6 | from ..forms import RegistrationForm
 7 | from ..models import get_application_model
 8 | 
 9 | 
10 | class ApplicationOwnerIsUserMixin(LoginRequiredMixin):
11 |     """
12 |     This mixin is used to provide an Application queryset filtered by the current request.user.
13 |     """
14 |     model = get_application_model()
15 | 
16 |     def get_queryset(self):
17 |         queryset = super(ApplicationOwnerIsUserMixin, self).get_queryset()
18 |         return queryset.filter(user=self.request.user)
19 | 
20 | 
21 | class ApplicationRegistration(LoginRequiredMixin, CreateView):
22 |     """
23 |     View used to register a new Application for the request.user
24 |     """
25 |     form_class = RegistrationForm
26 |     template_name = "oauth2_provider/application_registration_form.html"
27 | 
28 |     def form_valid(self, form):
29 |         form.instance.user = self.request.user
30 |         return super(ApplicationRegistration, self).form_valid(form)
31 | 
32 | 
33 | class ApplicationDetail(ApplicationOwnerIsUserMixin, DetailView):
34 |     """
35 |     Detail view for an application instance owned by the request.user
36 |     """
37 |     context_object_name = 'application'
38 |     template_name = "oauth2_provider/application_detail.html"
39 | 
40 | 
41 | class ApplicationList(ApplicationOwnerIsUserMixin, ListView):
42 |     """
43 |     List view for all the applications owned by the request.user
44 |     """
45 |     context_object_name = 'applications'
46 |     template_name = "oauth2_provider/application_list.html"
47 | 
48 | 
49 | class ApplicationDelete(ApplicationOwnerIsUserMixin, DeleteView):
50 |     """
51 |     View used to delete an application owned by the request.user
52 |     """
53 |     context_object_name = 'application'
54 |     success_url = reverse_lazy('oauth2_provider:list')
55 |     template_name = "oauth2_provider/application_confirm_delete.html"
56 | 
57 | 
58 | class ApplicationUpdate(ApplicationOwnerIsUserMixin, UpdateView):
59 |     """
60 |     View used to update an application owned by the request.user
61 |     """
62 |     context_object_name = 'application'
63 |     template_name = "oauth2_provider/application_form.html"
64 | 


--------------------------------------------------------------------------------
/example/example/urls.py:
--------------------------------------------------------------------------------
 1 | from django.conf.urls import patterns, include, url
 2 | from django.contrib import admin
 3 | from django.core.urlresolvers import reverse_lazy
 4 | from django.views.generic import TemplateView
 5 | from oauth2_provider import VERSION
 6 | 
 7 | from .views import (
 8 |     ConsumerView, ConsumerExchangeView, ConsumerDoneView, ApiEndpoint, ApiClientView
 9 | )
10 | from .api_v1 import get_system_info, applications_list, applications_detail
11 | 
12 | admin.autodiscover()
13 | 
14 | urlpatterns = patterns(
15 |     '',
16 |     url(
17 |         regex=r'^$',
18 |         view=TemplateView.as_view(template_name='example/home.html'),
19 |         kwargs={'version': VERSION},
20 |         name='home'
21 |     ),
22 |     url(
23 |         regex=r'^accounts/login/$',
24 |         view='django.contrib.auth.views.login',
25 |         kwargs={'template_name': 'example/login.html'}
26 |     ),
27 |     url(
28 |         regex='^accounts/logout/$',
29 |         view='django.contrib.auth.views.logout',
30 |         kwargs={'next_page': reverse_lazy('home')}
31 |     ),
32 | 
33 |     # the Django admin
34 |     url(r'^admin/', include(admin.site.urls)),
35 | 
36 |     # consumer logic
37 |     url(
38 |         regex=r'^consumer/$',
39 |         view=ConsumerView.as_view(),
40 |         name="consumer"
41 |     ),
42 |     url(
43 |         regex=r'^consumer/exchange/',
44 |         view=ConsumerExchangeView.as_view(),
45 |         name='consumer-exchange'
46 |     ),
47 |     url(
48 |         regex=r'^consumer/done/',
49 |         view=ConsumerDoneView.as_view(),
50 |         name='consumer-done'
51 |     ),
52 |     url(
53 |         regex=r'^consumer/client/',
54 |         view=TemplateView.as_view(template_name='example/consumer-client.html'),
55 |         name='consumer-client'
56 |     ),
57 | 
58 |     # oauth2 urls
59 |     url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
60 | 
61 |     # api stuff to test server functionalities
62 |     url(r'^apiclient$', ApiClientView.as_view(), name='api-client'),
63 |     url(r'^api/hello$', ApiEndpoint.as_view(), name='Hello'),
64 | 
65 |     # api v1
66 |     url(r'^api/v1/system_info$', get_system_info, name="System Info"),
67 |     url(r'^api/v1/applications$', applications_list, name="Application List"),
68 |     url(r'^api/v1/applications/(?P\w+)/$', applications_detail, name="Application Detail"),
69 | )
70 | 


--------------------------------------------------------------------------------
/docs/views/function_based.rst:
--------------------------------------------------------------------------------
 1 | Function-based views
 2 | ====================
 3 | 
 4 | Django OAuth Toolkit provides decorators to help you in protecting your function-based views.
 5 | 
 6 | .. function:: protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Server)
 7 | 
 8 |     Decorator to protect views by providing OAuth2 authentication out of the box, optionally with
 9 |     scope handling. Basic usage, without using scopes::
10 | 
11 |         from oauth2_provider.decorators import protected_resource
12 | 
13 |         @protected_resource()
14 |         def my_view(request):
15 |             # An access token is required to get here...
16 |             # ...
17 |             pass
18 | 
19 |     If you want to check scopes as well when accessing a view you can pass them along as
20 |     decorator's parameter::
21 | 
22 |         from oauth2_provider.decorators import protected_resource
23 | 
24 |         @protected_resource(scopes=['can_make_it can_break_it'])
25 |         def my_view(request):
26 |             # An access token AND the right scopes are required to get here...
27 |             # ...
28 |             pass
29 | 
30 |     The decorator also accept server and validator classes if you want or need to use your own
31 |     OAuth2 logic::
32 | 
33 |         from oauth2_provider.decorators import protected_resource
34 |         from myapp.oauth2_validators import MyValidator
35 | 
36 |         @protected_resource(validator_cls=MyValidator)
37 |         def my_view(request):
38 |             # You have to leverage your own logic to get here...
39 |             # ...
40 |             pass
41 | 
42 | 
43 | .. function:: rw_protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Server)
44 | 
45 |     Decorator to protect views by providing OAuth2 authentication and read/write scopes out of the
46 |     box. GET, HEAD, OPTIONS http methods require "read" scope.
47 |     Otherwise "write" scope is required::
48 | 
49 |         from oauth2_provider.decorators import protected_resource
50 | 
51 |         @rw_protected_resource()
52 |         def my_view(request):
53 |             # If this is a POST, you have to provide 'write' scope to get here...
54 |             # ...
55 |             pass
56 | 
57 |     If you need, you can ask for other scopes over "read" and "write"::
58 | 
59 |         from oauth2_provider.decorators import protected_resource
60 | 
61 |         @rw_protected_resource(scopes=['exotic_scope'])
62 |         def my_view(request):
63 |             # If this is a POST, you have to provide 'exotic_scope write' scopes to get here...
64 |             # ...
65 |             pass
66 | 


--------------------------------------------------------------------------------
/oauth2_provider/tests/test_mixins.py:
--------------------------------------------------------------------------------
 1 | from __future__ import unicode_literals
 2 | 
 3 | from django.core.exceptions import ImproperlyConfigured
 4 | from django.views.generic import View
 5 | from django.test import TestCase, RequestFactory
 6 | 
 7 | from oauthlib.oauth2 import Server
 8 | 
 9 | from ..views.mixins import OAuthLibMixin, ScopedResourceMixin
10 | from ..oauth2_validators import OAuth2Validator
11 | 
12 | 
13 | class TestOAuthLibMixin(TestCase):
14 |     @classmethod
15 |     def setUpClass(cls):
16 |         cls.request_factory = RequestFactory()
17 | 
18 |     def test_missing_server_class(self):
19 |         class TestView(OAuthLibMixin, View):
20 |             validator_class = OAuth2Validator
21 | 
22 |         test_view = TestView()
23 | 
24 |         self.assertRaises(ImproperlyConfigured, test_view.get_server)
25 | 
26 |     def test_missing_validator_class(self):
27 |         class TestView(OAuthLibMixin, View):
28 |             server_class = Server
29 | 
30 |         test_view = TestView()
31 | 
32 |         self.assertRaises(ImproperlyConfigured, test_view.get_server)
33 | 
34 |     def test_correct_server(self):
35 |         class TestView(OAuthLibMixin, View):
36 |             server_class = Server
37 |             validator_class = OAuth2Validator
38 | 
39 |         request = self.request_factory.get("/fake-req")
40 |         request.user = "fake"
41 |         test_view = TestView()
42 | 
43 |         self.assertIsInstance(test_view.get_server(), Server)
44 | 
45 |     def test_custom_backend(self):
46 |         class AnotherOauthLibBackend(object):
47 |             pass
48 | 
49 |         class TestView(OAuthLibMixin, View):
50 |             server_class = Server
51 |             validator_class = OAuth2Validator
52 |             oauthlib_core_class = AnotherOauthLibBackend
53 | 
54 |         request = self.request_factory.get("/fake-req")
55 |         request.user = "fake"
56 |         test_view = TestView()
57 | 
58 |         self.assertEqual(test_view.get_oauthlib_core_class(),
59 |                          AnotherOauthLibBackend)
60 | 
61 | 
62 | class TestScopedResourceMixin(TestCase):
63 |     @classmethod
64 |     def setUpClass(cls):
65 |         cls.request_factory = RequestFactory()
66 | 
67 |     def test_missing_required_scopes(self):
68 |         class TestView(ScopedResourceMixin, View):
69 |             pass
70 | 
71 |         test_view = TestView()
72 | 
73 |         self.assertRaises(ImproperlyConfigured, test_view.get_scopes)
74 | 
75 |     def test_correct_required_scopes(self):
76 |         class TestView(ScopedResourceMixin, View):
77 |             required_scopes = ['scope1', 'scope2']
78 | 
79 |         test_view = TestView()
80 | 
81 |         self.assertEqual(test_view.get_scopes(), ['scope1', 'scope2'])
82 | 


--------------------------------------------------------------------------------
/docs/tutorial/tutorial_02.rst:
--------------------------------------------------------------------------------
 1 | Part 2 - protect your APIs
 2 | ==========================
 3 | 
 4 | Scenario
 5 | --------
 6 | It's very common for an :term:`Authorization Server` being also the :term:`Resource Server`, usually exposing an API to
 7 | let others access its own resources. Django OAuth Toolkit implements an easy way to protect the views of a Django
 8 | application with OAuth2, in this tutorial we will see how to do it.
 9 | 
10 | Make your API
11 | -------------
12 | We start where we left the :doc:`part 1 of the tutorial `: you have an authorization server and we want it
13 | to provide an API to access some kind of resources. We don't need an actual resource, so we will simply expose an
14 | endpoint protected with OAuth2: let's do it in a *class based view* fashion!
15 | 
16 | Django OAuth Toolkit provides a set of generic class based view you can use to add OAuth behaviour to your views. Open
17 | your `views.py` module and import the view:
18 | 
19 | .. code-block:: python
20 | 
21 |     from oauth2_provider.views.generic import ProtectedResourceView
22 | 
23 | Then create the view which will respond to the API endpoint:
24 | 
25 | .. code-block:: python
26 | 
27 |     class ApiEndpoint(ProtectedResourceView):
28 |         def get(self, request, *args, **kwargs):
29 |             return HttpResponse('Hello, OAuth2!')
30 | 
31 | That's it, our API will expose only one method, responding to `GET` requests. Now open your `urls.py` and specify the
32 | URL this view will respond to:
33 | 
34 | .. code-block:: python
35 | 
36 |     from .views import ApiEndpoint
37 | 
38 |     urlpatterns = patterns(
39 |         '',
40 |         url(r'^admin/', include(admin.site.urls)),
41 |         url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')),  # look ma, I'm a provider!
42 |         url(r'^api/hello', ApiEndpoint.as_view()),  # and also a resource server!
43 |     )
44 | 
45 | Since we inherit from `ProtectedResourceView`, we're done and our API is OAuth2 protected - for the sake of the lazy
46 | programmer.
47 | 
48 | Testing your API
49 | ----------------
50 | Time to make requests to your API.
51 | 
52 | For a quick test, try accessing your app at the url `/api/hello` with your browser
53 | and verify that it reponds with a `403` (in fact no `HTTP_AUTHORIZATION` header was provided).
54 | You can test your API with anything that can perform HTTP requests, but for this tutorial you can use the online
55 | `consumer client `_.
56 | Just fill the form with the URL of the API endpoint (i.e. http://localhost:8000/api/hello if you're on localhost) and
57 | the access token coming from the :doc:`part 1 of the tutorial `. Going in the Django admin and get the
58 | token from there is not considered cheating, so it's an option.
59 | 
60 | Try performing a request and check that your :term:`Resource Server` aka :term:`Authorization Server` correctly responds with
61 | an HTTP 200.
62 | 
63 | :doc:`Part 3 of the tutorial ` will show how to use an access token to authenticate
64 | users.


--------------------------------------------------------------------------------
/docs/tutorial/tutorial_03.rst:
--------------------------------------------------------------------------------
 1 | Part 3 - OAuth2 token authentication
 2 | ====================================
 3 | 
 4 | Scenario
 5 | --------
 6 | You want to use an :term:`Access Token` to authenticate users against Django's authentication
 7 | system.
 8 | 
 9 | Setup a provider
10 | ----------------
11 | You need a fully-functional OAuth2 provider which is able to release access tokens: just follow
12 | the steps in :doc:`the part 1 of the tutorial `. To enable OAuth2 token authentication
13 | you need a middleware that checks for tokens inside requests and a custom authentication backend
14 | which takes care of token verification. In your settings.py:
15 | 
16 | .. code-block:: python
17 | 
18 |     AUTHENTICATION_BACKENDS = (
19 |         'oauth2_provider.backends.OAuth2Backend',
20 |         # Uncomment following if you want to access the admin
21 |         #'django.contrib.auth.backends.ModelBackend'
22 |         '...',
23 |     )
24 | 
25 |     MIDDLEWARE_CLASSES = (
26 |         '...',
27 |         'oauth2_provider.middleware.OAuth2TokenMiddleware',
28 |         '...',
29 |     )
30 | 
31 | You will likely use the `django.contrib.auth.backends.ModelBackend` along with the OAuth2 backend
32 | (or you might not be able to log in into the admin), only pay attention to the order in which
33 | Django processes authentication backends.
34 | 
35 | If you put the OAuth2 backend *after* the AuthenticationMiddleware and `request.user` is valid,
36 | the backend will do nothing; if `request.user` is the Anonymous user it will try to authenticate
37 | the user using the OAuth2 access token.
38 | 
39 | If you put the OAuth2 backend *before* AuthenticationMiddleware, or AuthenticationMiddleware is
40 | not used at all, it will try to authenticate user with the OAuth2 access token and set
41 | `request.user` and `request._cached_user` fields so that AuthenticationMiddleware (when active)
42 | will not try to get user from the session.
43 | 
44 | Protect your view
45 | -----------------
46 | The authentication backend will run smoothly with, for example, `login_required` decorators, so
47 | that you can have a view like this in your `views.py` module:
48 | 
49 | .. code-block:: python
50 | 
51 |     from django.contrib.auth.decorators import login_required
52 |     from django.http.response import HttpResponse
53 | 
54 |     @login_required()
55 |     def secret_page(request, *args, **kwargs):
56 |         return HttpResponse('Secret contents!', status=200)
57 | 
58 | To check everything works properly, mount the view above to some url:
59 | 
60 | .. code-block:: python
61 | 
62 |     urlpatterns = patterns(
63 |         '',
64 |         url(r'^secret$', 'my.views.secret_page', name='secret'),
65 |         '...',
66 |     )
67 | 
68 | You should have an :term:`Application` registered at this point, if you don't follow the steps in
69 | the previous tutorials to create one. Obtain an :term:`Access Token`, either following the OAuth2
70 | flow of your application or manually creating in the Django admin.
71 | Now supposing your access token value is `123456` you can try to access your authenticated view:
72 | 
73 | ::
74 | 
75 |     curl -H "Authorization: Bearer 123456" -X GET http://localhost:8000/secret
76 | 


--------------------------------------------------------------------------------
/oauth2_provider/tests/test_oauth2_validators.py:
--------------------------------------------------------------------------------
 1 | from django.test import TestCase
 2 | 
 3 | import mock
 4 | from oauthlib.common import Request
 5 | 
 6 | from ..oauth2_validators import OAuth2Validator
 7 | from ..models import get_application_model
 8 | from ..compat import get_user_model
 9 | 
10 | UserModel = get_user_model()
11 | AppModel = get_application_model()
12 | 
13 | 
14 | class TestOAuth2Validator(TestCase):
15 |     def setUp(self):
16 |         self.user = UserModel.objects.create_user("user", "test@user.com", "123456")
17 |         self.request = mock.MagicMock(wraps=Request)
18 |         self.request.client = None
19 |         self.validator = OAuth2Validator()
20 |         self.application = AppModel.objects.create(
21 |             client_id='client_id', client_secret='client_secret', user=self.user,
22 |             client_type=AppModel.CLIENT_PUBLIC, authorization_grant_type=AppModel.GRANT_PASSWORD)
23 | 
24 |     def tearDown(self):
25 |         self.application.delete()
26 | 
27 |     def test_authenticate_request_body(self):
28 |         self.request.client_id = 'client_id'
29 |         self.request.client_secret = ''
30 |         self.assertFalse(self.validator._authenticate_request_body(self.request))
31 | 
32 |         self.request.client_secret = 'wrong_client_secret'
33 |         self.assertFalse(self.validator._authenticate_request_body(self.request))
34 | 
35 |         self.request.client_secret = 'client_secret'
36 |         self.assertTrue(self.validator._authenticate_request_body(self.request))
37 | 
38 |     def test_extract_basic_auth(self):
39 |         self.request.headers = {'HTTP_AUTHORIZATION': 'Basic 123456'}
40 |         self.assertEqual(self.validator._extract_basic_auth(self.request), '123456')
41 |         self.request.headers = {}
42 |         self.assertIsNone(self.validator._extract_basic_auth(self.request))
43 |         self.request.headers = {'HTTP_AUTHORIZATION': 'Dummy 123456'}
44 |         self.assertIsNone(self.validator._extract_basic_auth(self.request))
45 | 
46 |     def test_authenticate_client_id(self):
47 |         self.assertTrue(self.validator.authenticate_client_id('client_id', self.request))
48 | 
49 |     def test_authenticate_client_id_fail(self):
50 |         self.application.client_type = AppModel.CLIENT_CONFIDENTIAL
51 |         self.application.save()
52 |         self.assertFalse(self.validator.authenticate_client_id('client_id', self.request))
53 |         self.assertFalse(self.validator.authenticate_client_id('fake_client_id', self.request))
54 | 
55 |     def test_client_authentication_required(self):
56 |         self.request.headers = {'HTTP_AUTHORIZATION': 'Basic 123456'}
57 |         self.assertTrue(self.validator.client_authentication_required(self.request))
58 |         self.request.headers = {}
59 |         self.request.client_id = 'client_id'
60 |         self.request.client_secret = 'client_secret'
61 |         self.assertTrue(self.validator.client_authentication_required(self.request))
62 |         self.request.client_secret = ''
63 |         self.assertFalse(self.validator.client_authentication_required(self.request))
64 |         self.application.client_type = AppModel.CLIENT_CONFIDENTIAL
65 |         self.application.save()
66 |         self.request.client = ''
67 |         self.assertTrue(self.validator.client_authentication_required(self.request))
68 | 


--------------------------------------------------------------------------------
/oauth2_provider/tests/settings.py:
--------------------------------------------------------------------------------
  1 | import os
  2 | 
  3 | DEBUG = True
  4 | TEMPLATE_DEBUG = DEBUG
  5 | 
  6 | ADMINS = ()
  7 | 
  8 | MANAGERS = ADMINS
  9 | 
 10 | DATABASES = {
 11 |     'default': {
 12 |         'ENGINE': 'django.db.backends.sqlite3',
 13 |         'NAME': 'example.sqlite',
 14 |     }
 15 | }
 16 | 
 17 | ALLOWED_HOSTS = []
 18 | 
 19 | TIME_ZONE = 'America/Chicago'
 20 | 
 21 | LANGUAGE_CODE = 'en-us'
 22 | 
 23 | SITE_ID = 1
 24 | 
 25 | USE_I18N = True
 26 | USE_L10N = True
 27 | USE_TZ = True
 28 | 
 29 | MEDIA_ROOT = ''
 30 | MEDIA_URL = ''
 31 | 
 32 | STATIC_ROOT = ''
 33 | STATIC_URL = '/static/'
 34 | 
 35 | STATICFILES_DIRS = ()
 36 | 
 37 | STATICFILES_FINDERS = (
 38 |     'django.contrib.staticfiles.finders.FileSystemFinder',
 39 |     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
 40 | )
 41 | 
 42 | # Make this unique, and don't share it with anybody.
 43 | SECRET_KEY = "1234567890evonove"
 44 | 
 45 | TEMPLATE_LOADERS = (
 46 |     'django.template.loaders.filesystem.Loader',
 47 |     'django.template.loaders.app_directories.Loader',
 48 | )
 49 | 
 50 | MIDDLEWARE_CLASSES = (
 51 |     'django.middleware.common.CommonMiddleware',
 52 |     'django.contrib.sessions.middleware.SessionMiddleware',
 53 |     'django.middleware.csrf.CsrfViewMiddleware',
 54 |     'django.contrib.auth.middleware.AuthenticationMiddleware',
 55 |     'django.contrib.messages.middleware.MessageMiddleware',
 56 | )
 57 | 
 58 | ROOT_URLCONF = 'oauth2_provider.tests.urls'
 59 | 
 60 | TEMPLATE_DIRS = ()
 61 | 
 62 | INSTALLED_APPS = (
 63 |     'django.contrib.auth',
 64 |     'django.contrib.contenttypes',
 65 |     'django.contrib.sessions',
 66 |     'django.contrib.sites',
 67 |     'django.contrib.staticfiles',
 68 |     'django.contrib.admin',
 69 | 
 70 |     'oauth2_provider',
 71 |     'oauth2_provider.tests',
 72 | )
 73 | 
 74 | LOGGING = {
 75 |     'version': 1,
 76 |     'disable_existing_loggers': False,
 77 |     'formatters': {
 78 |         'verbose': {
 79 |             'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
 80 |         },
 81 |         'simple': {
 82 |             'format': '%(levelname)s %(message)s'
 83 |         },
 84 |     },
 85 |     'filters': {
 86 |         'require_debug_false': {
 87 |             '()': 'django.utils.log.RequireDebugFalse'
 88 |         }
 89 |     },
 90 |     'handlers': {
 91 |         'mail_admins': {
 92 |             'level': 'ERROR',
 93 |             'filters': ['require_debug_false'],
 94 |             'class': 'django.utils.log.AdminEmailHandler'
 95 |         },
 96 |         'console': {
 97 |             'level': 'DEBUG',
 98 |             'class': 'logging.StreamHandler',
 99 |             'formatter': 'simple'
100 |         },
101 |         'null': {
102 |             'level': 'DEBUG',
103 |             'class': 'django.utils.log.NullHandler',
104 |         },
105 |     },
106 |     'loggers': {
107 |         'django.request': {
108 |             'handlers': ['mail_admins'],
109 |             'level': 'ERROR',
110 |             'propagate': True,
111 |         },
112 |         'oauth2_provider': {
113 |             'handlers': ['null'],
114 |             'level': 'DEBUG',
115 |             'propagate': True,
116 |         },
117 |     }
118 | }
119 | 
120 | OAUTH2_PROVIDER = {
121 |     '_SCOPES': ['example']
122 | }
123 | 


--------------------------------------------------------------------------------
/docs/advanced_topics.rst:
--------------------------------------------------------------------------------
 1 | Advanced topics
 2 | +++++++++++++++
 3 | 
 4 | 
 5 | Extending the Application model
 6 | ===============================
 7 | 
 8 | An Application instance represents a :term:`Client` on the :term:`Authorization server`. Usually an Application is
 9 | issued to client's developers after they log in on an Authorization Server and pass in some data
10 | which identify the Application itself (let's say, the application name). Django OAuth Toolkit
11 | provides a very basic implementation of the Application model containing only the data strictly
12 | required during all the OAuth processes but you will likely need some extra info, like application
13 | logo, acceptance of some user agreement and so on.
14 | 
15 | .. class:: AbstractApplication(models.Model)
16 | 
17 |     This is the base class implementing the bare minimum for Django OAuth Toolkit to work
18 | 
19 |     * :attr:`client_id` The client identifier issued to the client during the registration process as described in :rfc:`2.2`
20 |     * :attr:`user` ref to a Django user
21 |     * :attr:`redirect_uris` The list of allowed redirect uri. The string consists of valid URLs separated by space
22 |     * :attr:`client_type` Client type as described in :rfc:`2.1`
23 |     * :attr:`authorization_grant_type` Authorization flows available to the Application
24 |     * :attr:`client_secret` Confidential secret issued to the client during the registration process as described in :rfc:`2.2`
25 |     * :attr:`name` Friendly name for the Application
26 | 
27 | Django OAuth Toolkit lets you extend the AbstractApplication model in a fashion like Django's
28 | custom user models.
29 | 
30 | If you need, let's say, application logo and user agreement acceptance field, you can to this in
31 | your Django app (provided that your app is in the list of the INSTALLED_APPS in your settings
32 | module)::
33 | 
34 |     from django.db import models
35 |     from oauth2_provider.models import AbstractApplication
36 | 
37 |     class MyApplication(AbstractApplication):
38 |         logo = models.ImageField()
39 |         agree = models.BooleanField()
40 | 
41 | Then you need to tell Django OAuth Toolkit which model you want to use to represent applications.
42 | Write something like this in your settings module::
43 | 
44 |     OAUTH2_PROVIDER_APPLICATION_MODEL='your_app_name.MyApplication'
45 | 
46 | That's all, now Django OAuth Toolkit will use your model wherever an Application instance is needed.
47 | 
48 |     **Notice:** `OAUTH2_PROVIDER_APPLICATION_MODEL` is the only setting variable that is not namespaced, this
49 |     is because of the way Django currently implements swappable models.
50 |     See issue #90 (https://github.com/evonove/django-oauth-toolkit/issues/90) for details
51 | 
52 | 
53 | Skip authorization form
54 | =======================
55 | 
56 | Depending on the OAuth2 flow in use and the access token policy, users might be prompted  for the
57 | same authorization multiple times: sometimes this is acceptable or even desiderable but other it isn't.
58 | To control DOT behaviour you can use `approval_prompt` parameter when hitting the authorization endpoint.
59 | Possible values are:
60 |  * `force` - users are always prompted for authorization.
61 | 
62 |  * `auto` - users are prompted only the first time, subsequent authorizations for the same application
63 |    and scopes will be automatically accepted.


--------------------------------------------------------------------------------
/oauth2_provider/decorators.py:
--------------------------------------------------------------------------------
 1 | from functools import wraps
 2 | 
 3 | from oauthlib.oauth2 import Server
 4 | from django.http import HttpResponseForbidden
 5 | from django.core.exceptions import ImproperlyConfigured
 6 | 
 7 | from .oauth2_validators import OAuth2Validator
 8 | from .oauth2_backends import OAuthLibCore
 9 | from .settings import oauth2_settings
10 | 
11 | 
12 | def protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Server):
13 |     """
14 |     Decorator to protect views by providing OAuth2 authentication out of the box, optionally with
15 |     scope handling.
16 | 
17 |         @protected_resource()
18 |         def my_view(request):
19 |             # An access token is required to get here...
20 |             # ...
21 |             pass
22 |     
23 |     """
24 |     _scopes = scopes or []
25 | 
26 |     def decorator(view_func):
27 |         @wraps(view_func)
28 |         def _validate(request, *args, **kwargs):
29 |             validator = validator_cls()
30 |             core = OAuthLibCore(server_cls(validator))
31 |             valid, oauthlib_req = core.verify_request(request, scopes=_scopes)
32 |             if valid:
33 |                 request.resource_owner = oauthlib_req.user
34 |                 return view_func(request, *args, **kwargs)
35 |             return HttpResponseForbidden()
36 |         return _validate
37 |     return decorator
38 | 
39 | 
40 | def rw_protected_resource(scopes=None, validator_cls=OAuth2Validator, server_cls=Server):
41 |     """
42 |     Decorator to protect views by providing OAuth2 authentication and read/write scopes out of the
43 |     box.
44 |     GET, HEAD, OPTIONS http methods require "read" scope. Otherwise "write" scope is required.
45 | 
46 |         @rw_protected_resource()
47 |         def my_view(request):
48 |             # If this is a POST, you have to provide 'write' scope to get here...
49 |             # ...
50 |             pass
51 | 
52 |     """
53 |     _scopes = scopes or []
54 | 
55 |     def decorator(view_func):
56 |         @wraps(view_func)
57 |         def _validate(request, *args, **kwargs):
58 |             # Check if provided scopes are acceptable
59 |             provided_scopes = oauth2_settings._SCOPES
60 |             read_write_scopes = [oauth2_settings.READ_SCOPE, oauth2_settings.WRITE_SCOPE]
61 | 
62 |             if not set(read_write_scopes).issubset(set(provided_scopes)):
63 |                 raise ImproperlyConfigured(
64 |                     "rw_protected_resource decorator requires following scopes {0}"
65 |                     " to be in OAUTH2_PROVIDER['SCOPES'] list in settings".format(
66 |                         read_write_scopes)
67 |                 )
68 | 
69 |             # Check if method is safe
70 |             if request.method.upper() in ['GET', 'HEAD', 'OPTIONS']:
71 |                 _scopes.append(oauth2_settings.READ_SCOPE)
72 |             else:
73 |                 _scopes.append(oauth2_settings.WRITE_SCOPE)
74 | 
75 |             # proceed with validation
76 |             validator = validator_cls()
77 |             core = OAuthLibCore(server_cls(validator))
78 |             valid, oauthlib_req = core.verify_request(request, scopes=_scopes)
79 |             if valid:
80 |                 request.resource_owner = oauthlib_req.user
81 |                 return view_func(request, *args, **kwargs)
82 |             return HttpResponseForbidden()
83 |         return _validate
84 |     return decorator
85 | 


--------------------------------------------------------------------------------
/oauth2_provider/tests/test_application_views.py:
--------------------------------------------------------------------------------
 1 | from __future__ import unicode_literals
 2 | 
 3 | from django.core.urlresolvers import reverse
 4 | from django.test import TestCase
 5 | 
 6 | from ..models import get_application_model
 7 | from ..compat import get_user_model
 8 | 
 9 | Application = get_application_model()
10 | UserModel = get_user_model()
11 | 
12 | 
13 | class BaseTest(TestCase):
14 |     def setUp(self):
15 |         self.foo_user = UserModel.objects.create_user("foo_user", "test@user.com", "123456")
16 |         self.bar_user = UserModel.objects.create_user("bar_user", "dev@user.com", "123456")
17 | 
18 |     def tearDown(self):
19 |         self.foo_user.delete()
20 |         self.bar_user.delete()
21 | 
22 | 
23 | class TestApplicationRegistrationView(BaseTest):
24 |     def test_application_registration_user(self):
25 |         self.client.login(username="foo_user", password="123456")
26 | 
27 |         form_data = {
28 |             'name': 'Foo app',
29 |             'client_id': 'client_id',
30 |             'client_secret': 'client_secret',
31 |             'client_type': Application.CLIENT_CONFIDENTIAL,
32 |             'redirect_uris': 'http://example.com',
33 |             'authorization_grant_type': Application.GRANT_AUTHORIZATION_CODE
34 |         }
35 | 
36 |         response = self.client.post(reverse('oauth2_provider:register'), form_data)
37 |         self.assertEqual(response.status_code, 302)
38 | 
39 |         app = Application.objects.get(name="Foo app")
40 |         self.assertEqual(app.user.username, "foo_user")
41 | 
42 | 
43 | class TestApplicationViews(BaseTest):
44 |     def _create_application(self, name, user):
45 |         app = Application.objects.create(
46 |             name=name, redirect_uris="http://example.com",
47 |             client_type=Application.CLIENT_CONFIDENTIAL, authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
48 |             user=user)
49 |         return app
50 | 
51 |     def setUp(self):
52 |         super(TestApplicationViews, self).setUp()
53 |         self.app_foo_1 = self._create_application('app foo_user 1', self.foo_user)
54 |         self.app_foo_2 = self._create_application('app foo_user 2', self.foo_user)
55 |         self.app_foo_3 = self._create_application('app foo_user 3', self.foo_user)
56 | 
57 |         self.app_bar_1 = self._create_application('app bar_user 1', self.bar_user)
58 |         self.app_bar_2 = self._create_application('app bar_user 2', self.bar_user)
59 | 
60 |     def tearDown(self):
61 |         super(TestApplicationViews, self).tearDown()
62 |         Application.objects.all().delete()
63 | 
64 |     def test_application_list(self):
65 |         self.client.login(username="foo_user", password="123456")
66 | 
67 |         response = self.client.get(reverse('oauth2_provider:list'))
68 |         self.assertEqual(response.status_code, 200)
69 |         self.assertEqual(len(response.context['object_list']), 3)
70 | 
71 |     def test_application_detail_owner(self):
72 |         self.client.login(username="foo_user", password="123456")
73 | 
74 |         response = self.client.get(reverse('oauth2_provider:detail', args=(self.app_foo_1.pk,)))
75 |         self.assertEqual(response.status_code, 200)
76 | 
77 |     def test_application_detail_not_owner(self):
78 |         self.client.login(username="foo_user", password="123456")
79 | 
80 |         response = self.client.get(reverse('oauth2_provider:detail', args=(self.app_bar_1.pk,)))
81 |         self.assertEqual(response.status_code, 404)
82 | 


--------------------------------------------------------------------------------
/oauth2_provider/tests/test_decorators.py:
--------------------------------------------------------------------------------
 1 | import json
 2 | from datetime import timedelta
 3 | 
 4 | from django.test import TestCase, RequestFactory
 5 | from django.utils import timezone
 6 | 
 7 | from ..decorators import protected_resource, rw_protected_resource
 8 | from ..settings import oauth2_settings
 9 | from ..models import get_application_model, AccessToken
10 | from ..compat import get_user_model
11 | from .test_utils import TestCaseUtils
12 | 
13 | 
14 | Application = get_application_model()
15 | UserModel = get_user_model()
16 | 
17 | 
18 | class TestProtectedResourceDecorator(TestCase, TestCaseUtils):
19 |     @classmethod
20 |     def setUpClass(cls):
21 |         cls.request_factory = RequestFactory()
22 | 
23 |     def setUp(self):
24 |         self.user = UserModel.objects.create_user("test_user", "test@user.com", "123456")
25 |         self.application = Application.objects.create(
26 |             name="test_client_credentials_app",
27 |             user=self.user,
28 |             client_type=Application.CLIENT_PUBLIC,
29 |             authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS,
30 |         )
31 | 
32 |         self.access_token = AccessToken.objects.create(
33 |             user=self.user,
34 |             scope='read write',
35 |             expires=timezone.now() + timedelta(seconds=300),
36 |             token='secret-access-token-key',
37 |             application=self.application
38 |         )
39 | 
40 |         oauth2_settings._SCOPES = ['read', 'write']
41 | 
42 |     def test_access_denied(self):
43 |         @protected_resource()
44 |         def view(request, *args, **kwargs):
45 |             return 'protected contents'
46 | 
47 |         request = self.request_factory.get("/fake-resource")
48 |         response = view(request)
49 |         self.assertEqual(response.status_code, 403)
50 | 
51 |     def test_access_allowed(self):
52 |         @protected_resource()
53 |         def view(request, *args, **kwargs):
54 |             return 'protected contents'
55 | 
56 |         @protected_resource(scopes=['can_touch_this'])
57 |         def scoped_view(request, *args, **kwargs):
58 |             return 'moar protected contents'
59 | 
60 |         auth_headers = {
61 |             'HTTP_AUTHORIZATION': 'Bearer ' + self.access_token.token,
62 |         }
63 |         request = self.request_factory.get("/fake-resource", **auth_headers)
64 |         response = view(request)
65 |         self.assertEqual(response, "protected contents")
66 | 
67 |         # now with scopes
68 |         self.access_token.scope = 'can_touch_this'
69 |         self.access_token.save()
70 |         auth_headers = {
71 |             'HTTP_AUTHORIZATION': 'Bearer ' + self.access_token.token,
72 |         }
73 |         request = self.request_factory.get("/fake-resource", **auth_headers)
74 |         response = scoped_view(request)
75 |         self.assertEqual(response, "moar protected contents")
76 | 
77 |     def test_rw_protected(self):
78 |         self.access_token.scope = 'exotic_scope write'
79 |         self.access_token.save()
80 |         auth_headers = {
81 |             'HTTP_AUTHORIZATION': 'Bearer ' + self.access_token.token,
82 |         }
83 | 
84 |         @rw_protected_resource(scopes=['exotic_scope'])
85 |         def scoped_view(request, *args, **kwargs):
86 |             return 'other protected contents'
87 | 
88 |         request = self.request_factory.post("/fake-resource", **auth_headers)
89 |         response = scoped_view(request)
90 |         self.assertEqual(response, "other protected contents")
91 | 
92 |         request = self.request_factory.get("/fake-resource", **auth_headers)
93 |         response = scoped_view(request)
94 |         self.assertEqual(response.status_code, 403)
95 | 


--------------------------------------------------------------------------------
/example/example/templates/example/consumer-done.html:
--------------------------------------------------------------------------------
 1 | {% extends "example/base.html" %}
 2 | {% load url from future %}
 3 | 
 4 | {% block content %}
 5 | 

Show me your token!

6 |
7 | 8 | Error! 9 |
10 | {% if form %} 11 |
12 | {{ form.non_field_errors }} 13 |
14 | The Authorization server granted me the following: 15 | 16 | {{ form.access_token }} 17 | 18 | 19 | {{ form.token_type }} 20 | 21 | 22 | {{ form.expires_in }} 23 | 24 | 25 | {{ form.refresh_token }} 26 | 27 |

Now you can try obtaining another access token using your refresh token and providing client 28 | credentials

29 | 30 | 31 | {{ form.client_id }} 32 | 33 | 34 | {{ form.client_secret }} 35 | 36 | 37 | {{ form.token_url }} 38 | 39 | The url in your server where to retrieve the access token, it's ok if it points to localhost 40 | (e.g. http://localhost:8000/o/token/). 41 | 42 | 43 | 44 |
45 |
46 | {% else %} 47 |

It seems you've got nothing to show :(

48 | {% endif %} 49 | {% endblock %} 50 | 51 | {% block javascript %} 52 | 96 | {% endblock javascript %} -------------------------------------------------------------------------------- /example/example/api_v1.py: -------------------------------------------------------------------------------- 1 | from oauth2_provider.decorators import protected_resource 2 | from oauth2_provider import VERSION 3 | 4 | import json 5 | from django.http import HttpResponse 6 | from django import get_version 7 | from django.views.decorators.csrf import csrf_exempt 8 | from django.core import serializers 9 | from django.views.decorators.http import require_http_methods 10 | from django.http import HttpResponseBadRequest, HttpResponseNotFound 11 | 12 | from oauthlib.oauth2 import Server 13 | 14 | from .models import MyApplication 15 | 16 | 17 | class MyServer(Server): 18 | """ 19 | A custom server which bypasses OAuth controls for every GET request 20 | """ 21 | def verify_request(self, uri, http_method='GET', body=None, headers=None, scopes=None): 22 | ok, request = super(MyServer, self).verify_request(uri, http_method, body, headers, scopes) 23 | if request.http_method == 'GET': 24 | ok = True # possibly override failures 25 | return ok, request 26 | 27 | 28 | @csrf_exempt # so we can see 405 errors instead of 403 29 | @require_http_methods(["GET"]) 30 | def get_system_info(request, *args, **kwargs): 31 | """ 32 | A simple "read only" api endpoint, unprotected 33 | """ 34 | data = { 35 | 'DOT version': VERSION, 36 | 'oauthlib version': '0.5.1', 37 | 'Django version': get_version(), 38 | } 39 | 40 | return HttpResponse(json.dumps(data), content_type='application/json', *args, **kwargs) 41 | 42 | 43 | @csrf_exempt 44 | @protected_resource(server_cls=MyServer, scopes=["can_create_application"]) 45 | @require_http_methods(["GET", "POST"]) 46 | def applications_list(request, *args, **kwargs): 47 | """ 48 | List resources with GET, create a new one with POST. With custom server_cls we bypass oauth2 49 | controls and let everyone list applications. 50 | """ 51 | if request.method == 'GET': 52 | # hide default Application in the playground 53 | data = serializers.serialize("json", MyApplication.objects.exclude(pk=1)) 54 | return HttpResponse(data, content_type='application/json', *args, **kwargs) 55 | elif request.method == 'POST': 56 | if request.is_ajax(): 57 | try: 58 | data = json.loads(request.body) 59 | data['user'] = request.resource_owner 60 | obj = MyApplication.objects.create(**data) 61 | out = serializers.serialize("json", [obj]) 62 | except (ValueError, TypeError): 63 | return HttpResponseBadRequest() 64 | 65 | return HttpResponse(out, content_type='application/json', status=201, *args, **kwargs) 66 | 67 | 68 | @csrf_exempt 69 | @protected_resource() 70 | @require_http_methods(["GET", "PUT", "DELETE"]) 71 | def applications_detail(request, lookup, *args, **kwargs): 72 | """ 73 | Show resource with GET, update it with PUT, destroy with DELETE 74 | """ 75 | try: 76 | resource = MyApplication.objects.filter(user=request.resource_owner)\ 77 | .filter(client_id=lookup).get() 78 | # hide default Application in the playground 79 | if resource.pk == 1: 80 | raise MyApplication.DoesNotExist 81 | except MyApplication.DoesNotExist: 82 | return HttpResponseNotFound() 83 | 84 | if request.method == 'GET': 85 | data = serializers.serialize("json", [resource]) 86 | return HttpResponse(data, content_type='application/json', status=200, *args, **kwargs) 87 | elif request.method == 'PUT': 88 | try: 89 | data = json.loads(request.body) 90 | for k, v in data.iteritems(): 91 | setattr(resource, k, v) 92 | resource.save() 93 | data = serializers.serialize("json", [resource]) 94 | return HttpResponse(data, content_type='application/json') 95 | except (ValueError, TypeError): 96 | return HttpResponseBadRequest() 97 | elif request.method == 'DELETE': 98 | resource.delete() 99 | return HttpResponse(status=204) -------------------------------------------------------------------------------- /oauth2_provider/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | try: 4 | from unittest import skipIf 5 | except ImportError: 6 | from django.utils.unittest.case import skipIf 7 | 8 | import django 9 | from django.test import TestCase 10 | from django.test.utils import override_settings 11 | from django.core.exceptions import ValidationError 12 | 13 | from ..models import AccessToken, get_application_model 14 | from ..compat import get_user_model 15 | 16 | 17 | Application = get_application_model() 18 | UserModel = get_user_model() 19 | 20 | 21 | class TestModels(TestCase): 22 | def setUp(self): 23 | self.user = UserModel.objects.create_user("test_user", "test@user.com", "123456") 24 | 25 | def test_allow_scopes(self): 26 | self.client.login(username="test_user", password="123456") 27 | app = Application( 28 | name="test_app", 29 | redirect_uris="http://localhost http://example.com http://example.it", 30 | user=self.user, 31 | client_type=Application.CLIENT_CONFIDENTIAL, 32 | authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, 33 | ) 34 | 35 | access_token = AccessToken( 36 | user=self.user, 37 | scope='read write', 38 | expires=0, 39 | token='', 40 | application=app 41 | ) 42 | 43 | self.assertTrue(access_token.allow_scopes(['read', 'write'])) 44 | self.assertTrue(access_token.allow_scopes(['write', 'read'])) 45 | self.assertTrue(access_token.allow_scopes(['write', 'read', 'read'])) 46 | self.assertTrue(access_token.allow_scopes([])) 47 | self.assertFalse(access_token.allow_scopes(['write', 'destroy'])) 48 | 49 | def test_grant_authorization_code_redirect_uris(self): 50 | app = Application( 51 | name="test_app", 52 | redirect_uris="", 53 | user=self.user, 54 | client_type=Application.CLIENT_CONFIDENTIAL, 55 | authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, 56 | ) 57 | 58 | self.assertRaises(ValidationError, app.full_clean) 59 | 60 | def test_grant_implicit_redirect_uris(self): 61 | app = Application( 62 | name="test_app", 63 | redirect_uris="", 64 | user=self.user, 65 | client_type=Application.CLIENT_CONFIDENTIAL, 66 | authorization_grant_type=Application.GRANT_IMPLICIT, 67 | ) 68 | 69 | self.assertRaises(ValidationError, app.full_clean) 70 | 71 | def test_str(self): 72 | app = Application( 73 | redirect_uris="", 74 | user=self.user, 75 | client_type=Application.CLIENT_CONFIDENTIAL, 76 | authorization_grant_type=Application.GRANT_IMPLICIT, 77 | ) 78 | self.assertEqual("%s" % app, app.client_id) 79 | 80 | app.name = "test_app" 81 | self.assertEqual("%s" % app, "test_app") 82 | 83 | @skipIf(django.VERSION < (1, 5), "Behavior is broken on 1.4 and there is no solution") 84 | @override_settings(OAUTH2_PROVIDER_APPLICATION_MODEL='tests.TestApplication') 85 | class TestCustomApplicationModel(TestCase): 86 | def setUp(self): 87 | self.user = UserModel.objects.create_user("test_user", "test@user.com", "123456") 88 | 89 | def test_related_objects(self): 90 | """ 91 | If a custom application model is installed, it should be present in 92 | the related objects and not the swapped out one. 93 | 94 | See issue #90 (https://github.com/evonove/django-oauth-toolkit/issues/90) 95 | """ 96 | # Django internals caches the related objects. 97 | del UserModel._meta._related_objects_cache 98 | related_object_names = [ro.name for ro in UserModel._meta.get_all_related_objects()] 99 | self.assertNotIn('oauth2_provider:application', related_object_names) 100 | self.assertIn('tests:testapplication', related_object_names) 101 | -------------------------------------------------------------------------------- /oauth2_provider/tests/test_password.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import json 4 | 5 | from django.test import TestCase, RequestFactory 6 | from django.core.urlresolvers import reverse 7 | 8 | from ..models import get_application_model 9 | from ..settings import oauth2_settings 10 | from ..views import ProtectedResourceView 11 | from ..compat import get_user_model 12 | from .test_utils import TestCaseUtils 13 | 14 | 15 | Application = get_application_model() 16 | UserModel = get_user_model() 17 | 18 | 19 | # mocking a protected resource view 20 | class ResourceView(ProtectedResourceView): 21 | def get(self, request, *args, **kwargs): 22 | return "This is a protected resource" 23 | 24 | 25 | class BaseTest(TestCaseUtils, TestCase): 26 | def setUp(self): 27 | self.factory = RequestFactory() 28 | self.test_user = UserModel.objects.create_user("test_user", "test@user.com", "123456") 29 | self.dev_user = UserModel.objects.create_user("dev_user", "dev@user.com", "123456") 30 | 31 | self.application = Application( 32 | name="Test Password Application", 33 | user=self.dev_user, 34 | client_type=Application.CLIENT_PUBLIC, 35 | authorization_grant_type=Application.GRANT_PASSWORD, 36 | ) 37 | self.application.save() 38 | 39 | oauth2_settings._SCOPES = ['read', 'write'] 40 | 41 | def tearDown(self): 42 | self.application.delete() 43 | self.test_user.delete() 44 | self.dev_user.delete() 45 | 46 | 47 | class TestPasswordTokenView(BaseTest): 48 | def test_get_token(self): 49 | """ 50 | Request an access token using Resource Owner Password Flow 51 | """ 52 | token_request_data = { 53 | 'grant_type': 'password', 54 | 'username': 'test_user', 55 | 'password': '123456', 56 | } 57 | auth_headers = self.get_basic_auth_header(self.application.client_id, self.application.client_secret) 58 | 59 | response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers) 60 | self.assertEqual(response.status_code, 200) 61 | 62 | content = json.loads(response.content.decode("utf-8")) 63 | self.assertEqual(content['token_type'], "Bearer") 64 | self.assertEqual(content['scope'], "read write") 65 | self.assertEqual(content['expires_in'], oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) 66 | 67 | def test_bad_credentials(self): 68 | """ 69 | Request an access token using Resource Owner Password Flow 70 | """ 71 | token_request_data = { 72 | 'grant_type': 'password', 73 | 'username': 'test_user', 74 | 'password': 'NOT_MY_PASS', 75 | } 76 | auth_headers = self.get_basic_auth_header(self.application.client_id, self.application.client_secret) 77 | 78 | response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers) 79 | self.assertEqual(response.status_code, 400) 80 | 81 | 82 | class TestPasswordProtectedResource(BaseTest): 83 | def test_password_resource_access_allowed(self): 84 | token_request_data = { 85 | 'grant_type': 'password', 86 | 'username': 'test_user', 87 | 'password': '123456', 88 | } 89 | auth_headers = self.get_basic_auth_header(self.application.client_id, self.application.client_secret) 90 | 91 | response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers) 92 | content = json.loads(response.content.decode("utf-8")) 93 | access_token = content['access_token'] 94 | 95 | # use token to access the resource 96 | auth_headers = { 97 | 'HTTP_AUTHORIZATION': 'Bearer ' + access_token, 98 | } 99 | request = self.factory.get("/fake-resource", **auth_headers) 100 | request.user = self.test_user 101 | 102 | view = ResourceView.as_view() 103 | response = view(request) 104 | self.assertEqual(response, "This is a protected resource") 105 | -------------------------------------------------------------------------------- /example/example/templates/example/base.html: -------------------------------------------------------------------------------- 1 | {% load url from future %} 2 | 3 | 4 | 5 | 6 | Django OAuth Toolkit Example 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 74 | 75 |
76 | {% block content %}{% endblock %} 77 |
78 | 79 | 80 | 81 | {% block javascript %}{% endblock javascript %} 82 | 83 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.7.0 [2014-03-01] 5 | ------------------ 6 | 7 | * Created a setting for the default value for approval prompt. 8 | * Improved docs 9 | * Don't pin django-braces and six versions 10 | 11 | **Backwards incompatible changes in 0.7.0** 12 | 13 | * Make Application model truly "swappable" (introduces a new non-namespaced setting OAUTH2_PROVIDER_APPLICATION_MODEL) 14 | 15 | 16 | 0.6.1 [2014-02-05] 17 | ------------------ 18 | 19 | * added support for `scope` query parameter keeping backwards compatibility for the original `scopes` parameter. 20 | * __str__ method in Application model returns name when available 21 | 22 | 23 | 0.6.0 [2014-01-26] 24 | ------------------ 25 | 26 | * oauthlib 0.6.1 support 27 | * Django dev branch support 28 | * Python 2.6 support 29 | * Skip authorization form via `approval_prompt` parameter 30 | 31 | **Bugfixes** 32 | 33 | * Several fixes to the docs 34 | * Issue #71: Fix migrations 35 | * Issue #65: Use OAuth2 password grant with multiple devices 36 | * Issue #84: Add information about login template to tutorial. 37 | * Issue #64: Fix urlencode clientid secret 38 | 39 | 40 | 0.5.0 [2013-09-17] 41 | ------------------ 42 | 43 | * oauthlib 0.6.0 support 44 | 45 | **Backwards incompatible changes in 0.5.0** 46 | 47 | * backends.py module has been renamed to oauth2_backends.py so you should change your imports whether you're extending this module 48 | 49 | **Bugfixes** 50 | 51 | * Issue #54: Auth backend proposal to address #50 52 | * Issue #61: Fix contributing page 53 | * Issue #55: Add support for authenticating confidential client with request body params 54 | * Issue #53: Quote characters in the url query that are safe for Django but not for oauthlib 55 | 56 | 0.4.1 [2013-09-06] 57 | ------------------ 58 | 59 | * Optimize queries on access token validation 60 | 61 | 0.4.0 [2013-08-09] 62 | ------------------ 63 | 64 | **New Features** 65 | 66 | * Add Application management views, you no more need the admin to register, update and delete your application. 67 | * Add support to configurable application model 68 | * Add support for function based views 69 | 70 | **Backwards incompatible changes in 0.4.0** 71 | 72 | * `SCOPE` attribute in settings is now a dictionary to store `{'scope_name': 'scope_description'}` 73 | * Namespace 'oauth2_provider' is mandatory in urls. See issue #36 74 | 75 | **Bugfixes** 76 | 77 | * Issue #25: Bug in the Basic Auth parsing in Oauth2RequestValidator 78 | * Issue #24: Avoid generation of client_id with ":" colon char when using HTTP Basic Auth 79 | * Issue #21: IndexError when trying to authorize an application 80 | * Issue #9: Default_redirect_uri is mandatory when grant_type is implicit, authorization_code or all-in-one 81 | * Issue #22: Scopes need a verbose description 82 | * Issue #33: Add django-oauth-toolkit version on example main page 83 | * Issue #36: Add mandatory namespace to urls 84 | * Issue #31: Add docstring to OAuthToolkitError and FatalClientError 85 | * Issue #32: Add docstring to validate_uris 86 | * Issue #34: Documentation tutorial part1 needs corsheaders explanation 87 | * Issue #36: Add mandatory namespace to urls 88 | * Issue #45: Add docs for AbstractApplication 89 | * Issue #47: Add docs for views decorators 90 | 91 | 0.3.2 [2013-07-10] 92 | ------------------ 93 | 94 | * Bugfix #37: Error in migrations with custom user on Django 1.5 95 | 96 | 0.3.1 [2013-07-10] 97 | ------------------ 98 | 99 | * Bugfix #27: OAuthlib refresh token refactoring 100 | 101 | 0.3.0 [2013-06-14] 102 | ---------------------- 103 | 104 | * `Django REST Framework `_ integration layer 105 | * Bugfix #13: Populate request with client and user in validate_bearer_token 106 | * Bugfix #12: Fix paths in documentation 107 | 108 | **Backwards incompatible changes in 0.3.0** 109 | 110 | * `requested_scopes` parameter in ScopedResourceMixin changed to `required_scopes` 111 | 112 | 0.2.1 [2013-06-06] 113 | ------------------ 114 | 115 | * Core optimizations 116 | 117 | 0.2.0 [2013-06-05] 118 | ------------------ 119 | 120 | * Add support for Django1.4 and Django1.6 121 | * Add support for Python 3.3 122 | * Add a default ReadWriteScoped view 123 | * Add tutorial to docs 124 | 125 | 0.1.0 [2013-05-31] 126 | ------------------ 127 | 128 | * Support OAuth2 Authorization Flows 129 | 130 | 0.0.0 [2013-05-17] 131 | ------------------ 132 | 133 | * Discussion with Daniel Greenfeld at Django Circus 134 | * Ignition 135 | -------------------------------------------------------------------------------- /oauth2_provider/tests/test_auth_backends.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, RequestFactory 2 | from django.test.utils import override_settings 3 | from django.contrib.auth.models import AnonymousUser 4 | from django.utils.timezone import now, timedelta 5 | from django.conf.global_settings import MIDDLEWARE_CLASSES 6 | 7 | from ..compat import get_user_model 8 | from ..models import get_application_model 9 | from ..models import AccessToken 10 | from ..backends import OAuth2Backend 11 | from ..middleware import OAuth2TokenMiddleware 12 | 13 | UserModel = get_user_model() 14 | ApplicationModel = get_application_model() 15 | 16 | 17 | class BaseTest(TestCase): 18 | """ 19 | Base class for cases in this module 20 | """ 21 | def setUp(self): 22 | self.user = UserModel.objects.create_user("user", "test@user.com", "123456") 23 | self.app = ApplicationModel.objects.create( 24 | name='app', 25 | client_type=ApplicationModel.CLIENT_CONFIDENTIAL, 26 | authorization_grant_type=ApplicationModel.GRANT_CLIENT_CREDENTIALS, 27 | user=self.user 28 | ) 29 | self.token = AccessToken.objects.create(user=self.user, 30 | token='tokstr', 31 | application=self.app, 32 | expires=now() + timedelta(days=365)) 33 | self.factory = RequestFactory() 34 | 35 | def tearDown(self): 36 | self.user.delete() 37 | self.app.delete() 38 | self.token.delete() 39 | 40 | 41 | class TestOAuth2Backend(BaseTest): 42 | 43 | def test_authenticate(self): 44 | auth_headers = { 45 | 'HTTP_AUTHORIZATION': 'Bearer ' + 'tokstr', 46 | } 47 | request = self.factory.get("/a-resource", **auth_headers) 48 | 49 | backend = OAuth2Backend() 50 | credentials = {'request': request} 51 | u = backend.authenticate(**credentials) 52 | self.assertEqual(u, self.user) 53 | 54 | def test_authenticate_fail(self): 55 | auth_headers = { 56 | 'HTTP_AUTHORIZATION': 'Bearer ' + 'badstring', 57 | } 58 | request = self.factory.get("/a-resource", **auth_headers) 59 | 60 | backend = OAuth2Backend() 61 | credentials = {'request': request} 62 | self.assertIsNone(backend.authenticate(**credentials)) 63 | 64 | credentials = {'username': 'u', 'password': 'p'} 65 | self.assertIsNone(backend.authenticate(**credentials)) 66 | 67 | def test_get_user(self): 68 | backend = OAuth2Backend() 69 | self.assertEqual(self.user, backend.get_user(self.user.pk)) 70 | self.assertIsNone(backend.get_user(123456)) 71 | 72 | 73 | @override_settings( 74 | AUTHENTICATION_BACKENDS=( 75 | 'oauth2_provider.backends.OAuth2Backend', 76 | 'django.contrib.auth.backends.ModelBackend', 77 | ), 78 | MIDDLEWARE_CLASSES=MIDDLEWARE_CLASSES+('oauth2_provider.middleware.OAuth2TokenMiddleware',) 79 | ) 80 | class TestOAuth2Middleware(BaseTest): 81 | 82 | def setUp(self): 83 | super(TestOAuth2Middleware, self).setUp() 84 | self.anon_user = AnonymousUser() 85 | 86 | def test_middleware_wrong_headers(self): 87 | m = OAuth2TokenMiddleware() 88 | request = self.factory.get("/a-resource") 89 | self.assertIsNone(m.process_request(request)) 90 | auth_headers = { 91 | 'HTTP_AUTHORIZATION': 'Beerer ' + 'badstring', # a Beer token for you! 92 | } 93 | request = self.factory.get("/a-resource", **auth_headers) 94 | self.assertIsNone(m.process_request(request)) 95 | 96 | def test_middleware_user_is_set(self): 97 | m = OAuth2TokenMiddleware() 98 | auth_headers = { 99 | 'HTTP_AUTHORIZATION': 'Bearer ' + 'tokstr', 100 | } 101 | request = self.factory.get("/a-resource", **auth_headers) 102 | request.user = self.user 103 | self.assertIsNone(m.process_request(request)) 104 | request.user = self.anon_user 105 | self.assertIsNone(m.process_request(request)) 106 | 107 | def test_middleware_success(self): 108 | m = OAuth2TokenMiddleware() 109 | auth_headers = { 110 | 'HTTP_AUTHORIZATION': 'Bearer ' + 'tokstr', 111 | } 112 | request = self.factory.get("/a-resource", **auth_headers) 113 | m.process_request(request) 114 | self.assertEqual(request.user, self.user) 115 | -------------------------------------------------------------------------------- /example/example/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.core.urlresolvers import reverse 3 | from django.views.generic import FormView, TemplateView, View 4 | 5 | from oauth2_provider.compat import urlencode 6 | from oauth2_provider.views.generic import ProtectedResourceView 7 | 8 | from .forms import ConsumerForm, ConsumerExchangeForm, AccessTokenDataForm 9 | 10 | import json 11 | from collections import namedtuple 12 | 13 | 14 | ApiUrl = namedtuple('ApiUrl', 'name, url') 15 | 16 | 17 | class ConsumerExchangeView(FormView): 18 | """ 19 | The exchange view shows a form to manually perform the auth token swap 20 | """ 21 | form_class = ConsumerExchangeForm 22 | template_name = 'example/consumer-exchange.html' 23 | 24 | def get(self, request, *args, **kwargs): 25 | try: 26 | self.initial = { 27 | 'code': request.GET['code'], 28 | 'state': request.GET['state'], 29 | 'redirect_url': request.build_absolute_uri(reverse('consumer-exchange')) 30 | } 31 | except KeyError: 32 | kwargs['noparams'] = True 33 | 34 | form_class = self.get_form_class() 35 | form = self.get_form(form_class) 36 | return self.render_to_response(self.get_context_data(form=form, **kwargs)) 37 | 38 | 39 | class ConsumerView(FormView): 40 | """ 41 | The homepage to access Consumer's functionalities in the case of Authorization Code flow. 42 | It offers a form useful for building "authorization links" 43 | """ 44 | form_class = ConsumerForm 45 | success_url = '/consumer/' 46 | template_name = 'example/consumer.html' 47 | 48 | def __init__(self, **kwargs): 49 | self.authorization_link = None 50 | super(ConsumerView, self).__init__(**kwargs) 51 | 52 | def get_success_url(self): 53 | url = super(ConsumerView, self).get_success_url() 54 | return '{url}?{qs}'.format(url=url, qs=urlencode({'authorization_link': self.authorization_link})) 55 | 56 | def get(self, request, *args, **kwargs): 57 | kwargs['authorization_link'] = request.GET.get('authorization_link', None) 58 | 59 | form_class = self.get_form_class() 60 | form = self.get_form(form_class) 61 | return self.render_to_response(self.get_context_data(form=form, **kwargs)) 62 | 63 | def post(self, request, *args, **kwargs): 64 | self.request = request 65 | return super(ConsumerView, self).post(request, *args, **kwargs) 66 | 67 | def form_valid(self, form): 68 | qs = urlencode({ 69 | 'client_id': form.cleaned_data['client_id'], 70 | 'response_type': 'code', 71 | 'state': 'random_state_string', 72 | }) 73 | self.authorization_link = "{url}?{qs}".format(url=form.cleaned_data['authorization_url'], qs=qs) 74 | return super(ConsumerView, self).form_valid(form) 75 | 76 | 77 | class ConsumerDoneView(TemplateView): 78 | """ 79 | If exchange succeeded, come here, show a token and let users use the refresh token 80 | """ 81 | template_name = 'example/consumer-done.html' 82 | 83 | def get(self, request, *args, **kwargs): 84 | # do not show form when url is accessed without paramters 85 | if 'access_token' in request.GET: 86 | form = AccessTokenDataForm(initial={ 87 | 'access_token': request.GET.get('access_token', None), 88 | 'token_type': request.GET.get('token_type', None), 89 | 'expires_in': request.GET.get('expires_in', None), 90 | 'refresh_token': request.GET.get('refresh_token', None), 91 | }) 92 | kwargs['form'] = form 93 | 94 | return super(ConsumerDoneView, self).get(request, *args, **kwargs) 95 | 96 | 97 | class ApiClientView(TemplateView): 98 | """ 99 | TODO 100 | """ 101 | template_name = 'example/api-client.html' 102 | 103 | def get(self, request, *args, **kwargs): 104 | from .urls import urlpatterns 105 | endpoints = [] 106 | for u in urlpatterns: 107 | if 'api/' in u.regex.pattern: 108 | endpoints.append(ApiUrl(name=u.name, url=reverse(u.name, 109 | args=u.regex.groupindex.keys()))) 110 | kwargs['endpoints'] = endpoints 111 | return super(ApiClientView, self).get(request, *args, **kwargs) 112 | 113 | 114 | class ApiEndpoint(ProtectedResourceView): 115 | def get(self, request, *args, **kwargs): 116 | return HttpResponse('Hello, OAuth2!') 117 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Setup 6 | ===== 7 | 8 | Fork `django-oauth-toolkit` repository on `GitHub `_ and follow these steps: 9 | 10 | * Create a virtualenv and activate it 11 | * Clone your repository locally 12 | * cd into the repository and type `pip install -r requirements/optional.txt` (this will install both optional and base requirements, useful during development) 13 | 14 | Issues 15 | ====== 16 | 17 | You can find the list of bugs, enhancements and feature requests on the 18 | `issue tracker `_. If you want to fix an issue, pick up one and 19 | add a comment stating you're working on it. If the resolution implies a discussion or if you realize the comments on the 20 | issue are growing pretty fast, move the discussion to the `Google Group `_. 21 | 22 | Pull requests 23 | ============= 24 | 25 | Please avoid providing a pull request from your `master` and use **topic branches** instead; you can add as many commits 26 | as you want but please keep them in one branch which aims to solve one single issue. Then submit your pull request. To 27 | create a topic branch, simply do:: 28 | 29 | git checkout -b fix-that-issue 30 | Switched to a new branch 'fix-that-issue' 31 | 32 | When you're ready to submit your pull request, first push the topic branch to your GitHub repo:: 33 | 34 | git push origin fix-that-issue 35 | 36 | Now you can go to your repository dashboard on GitHub and open a pull request starting from your topic branch. You can 37 | apply your pull request to the `master` branch of django-oauth-toolkit (this should be the default behaviour of GitHub 38 | user interface). 39 | 40 | Next you should add a comment about your branch, and if the pull request refers to a certain issue, insert a link to it. 41 | The repo managers will be notified of your pull request and it will be reviewed, in the meantime you can continue to add 42 | commits to your topic branch (and push them up to GitHub) either if you see something that needs changing, or in 43 | response to a reviewer's comments. If a reviewer asks for changes, you do not need to close the pull and reissue it 44 | after making changes. Just make the changes locally, push them to GitHub, then add a comment to the discussion section 45 | of the pull request. 46 | 47 | Pull upstream changes into your fork regularly 48 | ============================================== 49 | 50 | It's a good practice to pull upstream changes from master into your fork on a regular basis, infact if you work on 51 | outdated code and your changes diverge too far from master, the pull request has to be rejected. 52 | 53 | To pull in upstream changes:: 54 | 55 | git remote add upstream https://github.com/evonove/django-oauth-toolkit.git 56 | git fetch upstream 57 | 58 | Then merge the changes that you fetched:: 59 | 60 | git merge upstream/master 61 | 62 | For more info, see http://help.github.com/fork-a-repo/ 63 | 64 | .. note:: Please be sure to rebase your commits on the master when possible, so your commits can be fast-forwarded: we 65 | try to avoid *merge commits* when they are not necessary. 66 | 67 | How to get your pull request accepted 68 | ===================================== 69 | 70 | We really want your code, so please follow these simple guidelines to make the process as smooth as possible. 71 | 72 | Run the tests! 73 | -------------- 74 | 75 | Django OAuth Toolkit aims to support different Python and Django versions, so we use **tox** to run tests on multiple 76 | configurations. At any time during the development and at least before submitting the pull request, please run the 77 | testsuite via:: 78 | 79 | tox 80 | 81 | The first thing the core committers will do is run this command. Any pull request that fails this test suite will be 82 | **immediately rejected**. 83 | 84 | Add the tests! 85 | -------------- 86 | 87 | Whenever you add code, you have to add tests as well. We cannot accept untested code, so unless it is a peculiar 88 | situation you previously discussed with the core commiters, if your pull request reduces the test coverage it will be 89 | **immediately rejected**. 90 | 91 | Code conventions matter 92 | ----------------------- 93 | 94 | There are no good nor bad conventions, just follow PEP8 (run some lint tool for this) and nobody will argue. 95 | Try reading our code and grasp the overall philosophy regarding method and variable names, avoid *black magics* for 96 | the sake of readability, keep in mind that *simple is better than complex*. If you feel the code is not straightforward, 97 | add a comment. If you think a function is not trivial, add a docstrings. 98 | 99 | The contents of this page are heavily based on the docs from `django-admin2 `_ 100 | -------------------------------------------------------------------------------- /oauth2_provider/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is largely inspired by django-rest-framework settings. 3 | 4 | Settings for the OAuth2 Provider are all namespaced in the OAUTH2_PROVIDER setting. 5 | For example your project's `settings.py` file might look like this: 6 | 7 | OAUTH2_PROVIDER = { 8 | 'CLIENT_ID_GENERATOR_CLASS': 9 | 'oauth2_provider.generators.ClientIdGenerator', 10 | 'CLIENT_SECRET_GENERATOR_CLASS': 11 | 'oauth2_provider.generators.ClientSecretGenerator', 12 | } 13 | 14 | This module provides the `oauth2_settings` object, that is used to access 15 | OAuth2 Provider settings, checking for user settings first, then falling 16 | back to the defaults. 17 | """ 18 | from __future__ import unicode_literals 19 | 20 | import six 21 | 22 | from django.conf import settings 23 | try: 24 | # Available in Python 2.7+ 25 | import importlib 26 | except ImportError: 27 | from django.utils import importlib 28 | 29 | 30 | USER_SETTINGS = getattr(settings, 'OAUTH2_PROVIDER', None) 31 | 32 | DEFAULTS = { 33 | 'CLIENT_ID_GENERATOR_CLASS': 'oauth2_provider.generators.ClientIdGenerator', 34 | 'CLIENT_SECRET_GENERATOR_CLASS': 'oauth2_provider.generators.ClientSecretGenerator', 35 | 'OAUTH2_VALIDATOR_CLASS': 'oauth2_provider.oauth2_validators.OAuth2Validator', 36 | 'SCOPES': {"read": "Reading scope", "write": "Writing scope"}, 37 | 'READ_SCOPE': 'read', 38 | 'WRITE_SCOPE': 'write', 39 | 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 60, 40 | 'ACCESS_TOKEN_EXPIRE_SECONDS': 36000, 41 | 'APPLICATION_MODEL': getattr(settings, 'OAUTH2_PROVIDER_APPLICATION_MODEL', 'oauth2_provider.Application'), 42 | 'REQUEST_APPROVAL_PROMPT': 'force', 43 | 44 | # Special settings that will be evaluated at runtime 45 | '_SCOPES': [], 46 | } 47 | 48 | # List of settings that cannot be empty 49 | MANDATORY = ( 50 | 'CLIENT_ID_GENERATOR_CLASS', 51 | 'CLIENT_SECRET_GENERATOR_CLASS', 52 | 'OAUTH2_VALIDATOR_CLASS', 53 | 'SCOPES', 54 | ) 55 | 56 | # List of settings that may be in string import notation. 57 | IMPORT_STRINGS = ( 58 | 'CLIENT_ID_GENERATOR_CLASS', 59 | 'CLIENT_SECRET_GENERATOR_CLASS', 60 | 'OAUTH2_VALIDATOR_CLASS', 61 | ) 62 | 63 | 64 | def perform_import(val, setting_name): 65 | """ 66 | If the given setting is a string import notation, 67 | then perform the necessary import or imports. 68 | """ 69 | if isinstance(val, six.string_types): 70 | return import_from_string(val, setting_name) 71 | elif isinstance(val, (list, tuple)): 72 | return [import_from_string(item, setting_name) for item in val] 73 | return val 74 | 75 | 76 | def import_from_string(val, setting_name): 77 | """ 78 | Attempt to import a class from a string representation. 79 | """ 80 | try: 81 | parts = val.split('.') 82 | module_path, class_name = '.'.join(parts[:-1]), parts[-1] 83 | module = importlib.import_module(module_path) 84 | return getattr(module, class_name) 85 | except ImportError as e: 86 | msg = "Could not import '%s' for setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e) 87 | raise ImportError(msg) 88 | 89 | 90 | class OAuth2ProviderSettings(object): 91 | """ 92 | A settings object, that allows OAuth2 Provider settings to be accessed as properties. 93 | 94 | Any setting with string import paths will be automatically resolved 95 | and return the class, rather than the string literal. 96 | """ 97 | 98 | def __init__(self, user_settings=None, defaults=None, import_strings=None, mandatory=None): 99 | self.user_settings = user_settings or {} 100 | self.defaults = defaults or {} 101 | self.import_strings = import_strings or () 102 | self.mandatory = mandatory or () 103 | 104 | def __getattr__(self, attr): 105 | if attr not in self.defaults.keys(): 106 | raise AttributeError("Invalid OAuth2Provider setting: '%s'" % attr) 107 | 108 | try: 109 | # Check if present in user settings 110 | val = self.user_settings[attr] 111 | except KeyError: 112 | # Fall back to defaults 113 | val = self.defaults[attr] 114 | 115 | # Coerce import strings into classes 116 | if val and attr in self.import_strings: 117 | val = perform_import(val, attr) 118 | 119 | # Overriding special settings 120 | if attr == '_SCOPES': 121 | val = list(six.iterkeys(self.SCOPES)) 122 | 123 | self.validate_setting(attr, val) 124 | 125 | # Cache the result 126 | setattr(self, attr, val) 127 | return val 128 | 129 | def validate_setting(self, attr, val): 130 | if not val and attr in self.mandatory: 131 | raise AttributeError("OAuth2Provider setting: '%s' is mandatory" % attr) 132 | 133 | 134 | oauth2_settings = OAuth2ProviderSettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS, MANDATORY) 135 | -------------------------------------------------------------------------------- /example/example/templates/example/consumer-exchange.html: -------------------------------------------------------------------------------- 1 | {% extends "example/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block content %} 5 |

Token Trading!

6 |
7 | 8 | Error retrieving access token! 9 |
10 | {% if not error %} 11 | {% if not noparams %} 12 |

This step of the OAuth2 authentication process is usually performed automatically by the consumer. 13 | For testing purposes, we simulate the POST request to the token endpoint provided by the Authorization 14 | Server with a form.

15 |
16 | {{ form.non_field_errors }} 17 |
18 | Trade your authorization token for a more powerful access token 19 | {{ form.code.errors }} 20 | 21 | {{ form.code }} 22 | The authorization token provided by your server 23 | {{ form.state.errors }} 24 | 25 | {{ form.state }} 26 | 27 | Sort of csrf. 28 | 29 | {{ form.token_url.errors }} 30 | 31 | {{ form.token_url }} 32 | 33 | The url in your server where to retrieve the access token, it's ok if it points to localhost 34 | (e.g. http://localhost:8000/o/token/). 35 | 36 | {{ form.redirect_url.errors }} 37 | 38 | {{ form.redirect_url }} 39 | 40 | The url of the consumer redirect_uri, must match at least one of the uris provided by the 41 | Application instance in the Authorization server. 42 | 43 | {{ form.client_id.errors }} 44 | 45 | {{ form.client_id }} 46 | 47 | One more time. 48 | 49 | {{ form.client_secret.errors }} 50 | 51 | {{ form.client_secret }} 52 | 53 | Get it from Application instance in the Authorization server. 54 | 55 | 56 | 57 |
58 | {% csrf_token %} 59 |
60 | {% else %} 61 |

You're not supposed to be here!

62 | {% endif %} 63 | {% endif %} 64 | {% endblock %} 65 | 66 | {% block javascript %} 67 | 111 | {% endblock javascript %} 112 | -------------------------------------------------------------------------------- /oauth2_provider/oauth2_backends.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from oauthlib import oauth2 4 | from oauthlib.common import urlencode, urlencoded, quote 5 | 6 | from .exceptions import OAuthToolkitError, FatalClientError 7 | from .settings import oauth2_settings 8 | from .compat import urlparse, urlunparse 9 | 10 | 11 | class OAuthLibCore(object): 12 | """ 13 | TODO: add docs 14 | """ 15 | def __init__(self, server=None): 16 | """ 17 | :params server: An instance of oauthlib.oauth2.Server class 18 | """ 19 | self.server = server or oauth2.Server(oauth2_settings.OAUTH2_VALIDATOR_CLASS()) 20 | 21 | def _get_escaped_full_path(self, request): 22 | """ 23 | Django considers "safe" some characters that aren't so for oauthlib. We have to search for 24 | them and properly escape. 25 | """ 26 | parsed = list(urlparse(request.get_full_path())) 27 | unsafe = set(c for c in parsed[4]).difference(urlencoded) 28 | for c in unsafe: 29 | parsed[4] = parsed[4].replace(c, quote(c, safe='')) 30 | 31 | return urlunparse(parsed) 32 | 33 | def _extract_params(self, request): 34 | """ 35 | Extract parameters from the Django request object. Such parameters will then be passed to 36 | OAuthLib to build its own Request object 37 | """ 38 | uri = self._get_escaped_full_path(request) 39 | http_method = request.method 40 | headers = request.META.copy() 41 | if 'wsgi.input' in headers: 42 | del headers['wsgi.input'] 43 | if 'wsgi.errors' in headers: 44 | del headers['wsgi.errors'] 45 | if 'HTTP_AUTHORIZATION' in headers: 46 | headers['Authorization'] = headers['HTTP_AUTHORIZATION'] 47 | body = urlencode(request.POST.items()) 48 | return uri, http_method, body, headers 49 | 50 | def validate_authorization_request(self, request): 51 | """ 52 | A wrapper method that calls validate_authorization_request on `server_class` instance. 53 | 54 | :param request: The current django.http.HttpRequest object 55 | """ 56 | try: 57 | uri, http_method, body, headers = self._extract_params(request) 58 | 59 | scopes, credentials = self.server.validate_authorization_request( 60 | uri, http_method=http_method, body=body, headers=headers) 61 | 62 | return scopes, credentials 63 | except oauth2.FatalClientError as error: 64 | raise FatalClientError(error=error) 65 | except oauth2.OAuth2Error as error: 66 | raise OAuthToolkitError(error=error) 67 | 68 | def create_authorization_response(self, request, scopes, credentials, allow): 69 | """ 70 | A wrapper method that calls create_authorization_response on `server_class` 71 | instance. 72 | 73 | :param request: The current django.http.HttpRequest object 74 | :param scopes: A list of provided scopes 75 | :param credentials: Authorization credentials dictionary containing 76 | `client_id`, `state`, `redirect_uri`, `response_type` 77 | :param allow: True if the user authorize the client, otherwise False 78 | """ 79 | try: 80 | if not allow: 81 | raise oauth2.AccessDeniedError() 82 | 83 | # add current user to credentials. this will be used by OAUTH2_VALIDATOR_CLASS 84 | credentials['user'] = request.user 85 | 86 | headers, body, status = self.server.create_authorization_response( 87 | uri=credentials['redirect_uri'], scopes=scopes, credentials=credentials) 88 | uri = headers.get("Location", None) 89 | 90 | return uri, headers, body, status 91 | 92 | except oauth2.FatalClientError as error: 93 | raise FatalClientError(error=error, redirect_uri=credentials['redirect_uri']) 94 | except oauth2.OAuth2Error as error: 95 | raise OAuthToolkitError(error=error, redirect_uri=credentials['redirect_uri']) 96 | 97 | def create_token_response(self, request): 98 | """ 99 | A wrapper method that calls create_token_response on `server_class` instance. 100 | 101 | :param request: The current django.http.HttpRequest object 102 | """ 103 | uri, http_method, body, headers = self._extract_params(request) 104 | 105 | headers, body, status = self.server.create_token_response(uri, http_method, body, 106 | headers) 107 | uri = headers.get("Location", None) 108 | 109 | return uri, headers, body, status 110 | 111 | def verify_request(self, request, scopes): 112 | """ 113 | A wrapper method that calls verify_request on `server_class` instance. 114 | 115 | :param request: The current django.http.HttpRequest object 116 | :param scopes: A list of scopes required to verify so that request is verified 117 | """ 118 | uri, http_method, body, headers = self._extract_params(request) 119 | 120 | valid, r = self.server.verify_request(uri, http_method, body, headers, scopes=scopes) 121 | return valid, r 122 | 123 | 124 | def get_oauthlib_core(): 125 | """ 126 | Utility function that take a request and returns an instance of 127 | `oauth2_provider.backends.OAuthLibCore` 128 | """ 129 | from oauthlib.oauth2 import Server 130 | 131 | server = Server(oauth2_settings.OAUTH2_VALIDATOR_CLASS()) 132 | return OAuthLibCore(server) 133 | -------------------------------------------------------------------------------- /oauth2_provider/tests/test_client_credential.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import json 4 | 5 | try: 6 | import urllib.parse as urllib 7 | except ImportError: 8 | import urllib 9 | 10 | from django.core.urlresolvers import reverse 11 | from django.test import TestCase, RequestFactory 12 | from django.views.generic import View 13 | 14 | from oauthlib.oauth2 import BackendApplicationServer 15 | 16 | from ..models import get_application_model 17 | from ..oauth2_validators import OAuth2Validator 18 | from ..settings import oauth2_settings 19 | from ..views import ProtectedResourceView 20 | from ..views.mixins import OAuthLibMixin 21 | from ..compat import get_user_model 22 | from .test_utils import TestCaseUtils 23 | 24 | 25 | Application = get_application_model() 26 | UserModel = get_user_model() 27 | 28 | 29 | # mocking a protected resource view 30 | class ResourceView(ProtectedResourceView): 31 | def get(self, request, *args, **kwargs): 32 | return "This is a protected resource" 33 | 34 | 35 | class BaseTest(TestCaseUtils, TestCase): 36 | def setUp(self): 37 | self.factory = RequestFactory() 38 | self.test_user = UserModel.objects.create_user("test_user", "test@user.com", "123456") 39 | self.dev_user = UserModel.objects.create_user("dev_user", "dev@user.com", "123456") 40 | 41 | self.application = Application( 42 | name="test_client_credentials_app", 43 | user=self.dev_user, 44 | client_type=Application.CLIENT_PUBLIC, 45 | authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS, 46 | ) 47 | self.application.save() 48 | 49 | oauth2_settings._SCOPES = ['read', 'write'] 50 | 51 | def tearDown(self): 52 | self.application.delete() 53 | self.test_user.delete() 54 | self.dev_user.delete() 55 | 56 | 57 | class TestClientCredential(BaseTest): 58 | def test_client_credential_access_allowed(self): 59 | """ 60 | Request an access token using Client Credential Flow 61 | """ 62 | token_request_data = { 63 | 'grant_type': 'client_credentials', 64 | } 65 | auth_headers = self.get_basic_auth_header(self.application.client_id, self.application.client_secret) 66 | 67 | response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers) 68 | self.assertEqual(response.status_code, 200) 69 | 70 | content = json.loads(response.content.decode("utf-8")) 71 | access_token = content['access_token'] 72 | 73 | # use token to access the resource 74 | auth_headers = { 75 | 'HTTP_AUTHORIZATION': 'Bearer ' + access_token, 76 | } 77 | request = self.factory.get("/fake-resource", **auth_headers) 78 | request.user = self.test_user 79 | 80 | view = ResourceView.as_view() 81 | response = view(request) 82 | self.assertEqual(response, "This is a protected resource") 83 | 84 | 85 | class TestExtendedRequest(BaseTest): 86 | @classmethod 87 | def setUpClass(cls): 88 | cls.request_factory = RequestFactory() 89 | 90 | def test_extended_request(self): 91 | class TestView(OAuthLibMixin, View): 92 | server_class = BackendApplicationServer 93 | validator_class = OAuth2Validator 94 | 95 | def get_scopes(self): 96 | return ['read', 'write'] 97 | 98 | token_request_data = { 99 | 'grant_type': 'client_credentials', 100 | } 101 | auth_headers = self.get_basic_auth_header(self.application.client_id, self.application.client_secret) 102 | response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers) 103 | self.assertEqual(response.status_code, 200) 104 | 105 | content = json.loads(response.content.decode("utf-8")) 106 | access_token = content['access_token'] 107 | 108 | # use token to access the resource 109 | auth_headers = { 110 | 'HTTP_AUTHORIZATION': 'Bearer ' + access_token, 111 | } 112 | 113 | request = self.request_factory.get("/fake-req", **auth_headers) 114 | request.user = "fake" 115 | 116 | test_view = TestView() 117 | self.assertIsInstance(test_view.get_server(), BackendApplicationServer) 118 | 119 | valid, r = test_view.verify_request(request) 120 | self.assertTrue(valid) 121 | self.assertEqual(r.user, self.dev_user) 122 | self.assertEqual(r.client, self.application) 123 | self.assertEqual(r.scopes, ['read', 'write']) 124 | 125 | 126 | class TestClientResourcePasswordBased(BaseTest): 127 | def test_client_resource_password_based(self): 128 | """ 129 | Request an access token using Resource Owner Password Based flow 130 | """ 131 | 132 | self.application.delete() 133 | self.application = Application( 134 | name="test_client_credentials_app", 135 | user=self.dev_user, 136 | client_type=Application.CLIENT_CONFIDENTIAL, 137 | authorization_grant_type=Application.GRANT_PASSWORD, 138 | ) 139 | self.application.save() 140 | 141 | token_request_data = { 142 | 'grant_type': 'password', 143 | 'username': 'test_user', 144 | 'password': '123456' 145 | } 146 | auth_headers = self.get_basic_auth_header(urllib.quote_plus(self.application.client_id), urllib.quote_plus(self.application.client_secret)) 147 | response = self.client.post(reverse('oauth2_provider:token'), data=token_request_data, **auth_headers) 148 | self.assertEqual(response.status_code, 200) 149 | 150 | content = json.loads(response.content.decode("utf-8")) 151 | access_token = content['access_token'] 152 | 153 | # use token to access the resource 154 | auth_headers = { 155 | 'HTTP_AUTHORIZATION': 'Bearer ' + access_token, 156 | } 157 | request = self.factory.get("/fake-resource", **auth_headers) 158 | request.user = self.test_user 159 | 160 | view = ResourceView.as_view() 161 | response = view(request) 162 | self.assertEqual(response, "This is a protected resource") 163 | 164 | 165 | -------------------------------------------------------------------------------- /oauth2_provider/tests/test_rest_framework.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.conf.urls import patterns, url, include 4 | from django.http import HttpResponse 5 | from django.test import TestCase 6 | from django.utils import timezone, unittest 7 | 8 | 9 | from .test_utils import TestCaseUtils 10 | from ..models import AccessToken, get_application_model 11 | from ..settings import oauth2_settings 12 | from ..compat import get_user_model 13 | 14 | 15 | Application = get_application_model() 16 | UserModel = get_user_model() 17 | 18 | 19 | try: 20 | from rest_framework import permissions 21 | from rest_framework.views import APIView 22 | from ..ext.rest_framework import OAuth2Authentication, TokenHasScope, TokenHasReadWriteScope 23 | 24 | class MockView(APIView): 25 | permission_classes = (permissions.IsAuthenticated,) 26 | 27 | def get(self, request): 28 | return HttpResponse({'a': 1, 'b': 2, 'c': 3}) 29 | 30 | def post(self, request): 31 | return HttpResponse({'a': 1, 'b': 2, 'c': 3}) 32 | 33 | class OAuth2View(MockView): 34 | authentication_classes = [OAuth2Authentication] 35 | 36 | class ScopedView(OAuth2View): 37 | permission_classes = [permissions.IsAuthenticated, TokenHasScope] 38 | required_scopes = ['scope1'] 39 | 40 | class ReadWriteScopedView(OAuth2View): 41 | permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope] 42 | 43 | urlpatterns = patterns( 44 | '', 45 | url(r'^oauth2/', include('oauth2_provider.urls')), 46 | url(r'^oauth2-test/$', OAuth2View.as_view()), 47 | url(r'^oauth2-scoped-test/$', ScopedView.as_view()), 48 | url(r'^oauth2-read-write-test/$', ReadWriteScopedView.as_view()), 49 | ) 50 | 51 | rest_framework_installed = True 52 | except ImportError: 53 | rest_framework_installed = False 54 | 55 | 56 | class BaseTest(TestCaseUtils, TestCase): 57 | """ 58 | TODO: add docs 59 | """ 60 | pass 61 | 62 | 63 | class TestOAuth2Authentication(BaseTest): 64 | urls = 'oauth2_provider.tests.test_rest_framework' 65 | 66 | def setUp(self): 67 | oauth2_settings._SCOPES = ['read', 'write', 'scope1', 'scope2'] 68 | 69 | self.test_user = UserModel.objects.create_user("test_user", "test@user.com", "123456") 70 | self.dev_user = UserModel.objects.create_user("dev_user", "dev@user.com", "123456") 71 | 72 | self.application = Application.objects.create( 73 | name="Test Application", 74 | redirect_uris="http://localhost http://example.com http://example.it", 75 | user=self.dev_user, 76 | client_type=Application.CLIENT_CONFIDENTIAL, 77 | authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, 78 | ) 79 | 80 | self.access_token = AccessToken.objects.create( 81 | user=self.test_user, 82 | scope='read write', 83 | expires=timezone.now() + timedelta(seconds=300), 84 | token='secret-access-token-key', 85 | application=self.application 86 | ) 87 | 88 | def _create_authorization_header(self, token): 89 | return "Bearer {0}".format(token) 90 | 91 | @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') 92 | def test_authentication_allow(self): 93 | auth = self._create_authorization_header(self.access_token.token) 94 | response = self.client.get("/oauth2-test/", HTTP_AUTHORIZATION=auth) 95 | self.assertEqual(response.status_code, 200) 96 | 97 | @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') 98 | def test_authentication_denied(self): 99 | auth = self._create_authorization_header("fake-token") 100 | response = self.client.get("/oauth2-test/", HTTP_AUTHORIZATION=auth) 101 | self.assertEqual(response.status_code, 401) 102 | 103 | @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') 104 | def test_scoped_permission_allow(self): 105 | self.access_token.scope = 'scope1' 106 | self.access_token.save() 107 | 108 | auth = self._create_authorization_header(self.access_token.token) 109 | response = self.client.get("/oauth2-scoped-test/", HTTP_AUTHORIZATION=auth) 110 | self.assertEqual(response.status_code, 200) 111 | 112 | @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') 113 | def test_scoped_permission_deny(self): 114 | self.access_token.scope = 'scope2' 115 | self.access_token.save() 116 | 117 | auth = self._create_authorization_header(self.access_token.token) 118 | response = self.client.get("/oauth2-scoped-test/", HTTP_AUTHORIZATION=auth) 119 | self.assertEqual(response.status_code, 403) 120 | 121 | @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') 122 | def test_read_write_permission_get_allow(self): 123 | self.access_token.scope = 'read' 124 | self.access_token.save() 125 | 126 | auth = self._create_authorization_header(self.access_token.token) 127 | response = self.client.get("/oauth2-read-write-test/", HTTP_AUTHORIZATION=auth) 128 | self.assertEqual(response.status_code, 200) 129 | 130 | @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') 131 | def test_read_write_permission_post_allow(self): 132 | self.access_token.scope = 'write' 133 | self.access_token.save() 134 | 135 | auth = self._create_authorization_header(self.access_token.token) 136 | response = self.client.post("/oauth2-read-write-test/", HTTP_AUTHORIZATION=auth) 137 | self.assertEqual(response.status_code, 200) 138 | 139 | @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') 140 | def test_read_write_permission_get_deny(self): 141 | self.access_token.scope = 'write' 142 | self.access_token.save() 143 | 144 | auth = self._create_authorization_header(self.access_token.token) 145 | response = self.client.get("/oauth2-read-write-test/", HTTP_AUTHORIZATION=auth) 146 | self.assertEqual(response.status_code, 403) 147 | 148 | @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') 149 | def test_read_write_permission_post_deny(self): 150 | self.access_token.scope = 'read' 151 | self.access_token.save() 152 | 153 | auth = self._create_authorization_header(self.access_token.token) 154 | response = self.client.post("/oauth2-read-write-test/", HTTP_AUTHORIZATION=auth) 155 | self.assertEqual(response.status_code, 403) 156 | -------------------------------------------------------------------------------- /example/example/settings/base.py: -------------------------------------------------------------------------------- 1 | # Django settings for example project. 2 | import os 3 | from os.path import join, abspath, dirname 4 | 5 | import django.conf.global_settings as DEFAULT_SETTINGS 6 | 7 | # Root directory of our project 8 | PROJECT_ROOT = abspath(join(abspath(dirname(__file__)), "..",)) 9 | 10 | DEBUG = os.environ.get('DJANGO_DEBUG', True) 11 | TEMPLATE_DEBUG = DEBUG 12 | 13 | ADMINS = ( 14 | # ('Your Name', 'your_email@example.com'), 15 | ) 16 | 17 | DATABASES = {} 18 | 19 | MANAGERS = ADMINS 20 | 21 | # Hosts/domain names that are valid for this site; required if DEBUG is False 22 | # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts 23 | ALLOWED_HOSTS = [] 24 | 25 | # Local time zone for this installation. Choices can be found here: 26 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 27 | # although not all choices may be available on all operating systems. 28 | # In a Windows environment this must be set to your system time zone. 29 | TIME_ZONE = 'America/Chicago' 30 | 31 | # Language code for this installation. All choices can be found here: 32 | # http://www.i18nguy.com/unicode/language-identifiers.html 33 | LANGUAGE_CODE = 'en-us' 34 | 35 | SITE_ID = 1 36 | 37 | # If you set this to False, Django will make some optimizations so as not 38 | # to load the internationalization machinery. 39 | USE_I18N = True 40 | 41 | # If you set this to False, Django will not format dates, numbers and 42 | # calendars according to the current locale. 43 | USE_L10N = True 44 | 45 | # If you set this to False, Django will not use timezone-aware datetimes. 46 | USE_TZ = True 47 | 48 | # Absolute filesystem path to the directory that will hold user-uploaded files. 49 | # Example: "/var/www/example.com/media/" 50 | MEDIA_ROOT = join(PROJECT_ROOT, "media") 51 | 52 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 53 | # trailing slash. 54 | # Examples: "http://example.com/media/", "http://media.example.com/" 55 | MEDIA_URL = '/media/' 56 | 57 | # Absolute path to the directory static files should be collected to. 58 | # Don't put anything in this directory yourself; store your static files 59 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 60 | # Example: "/var/www/example.com/static/" 61 | STATIC_ROOT = join(PROJECT_ROOT, "static") 62 | 63 | # URL prefix for static files. 64 | # Example: "http://example.com/static/", "http://static.example.com/" 65 | STATIC_URL = '/static/' 66 | 67 | # Additional locations of static files 68 | STATICFILES_DIRS = ( 69 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 70 | # Always use forward slashes, even on Windows. 71 | # Don't forget to use absolute paths, not relative paths. 72 | ) 73 | 74 | # List of finder classes that know how to find static files in 75 | # various locations. 76 | STATICFILES_FINDERS = ( 77 | 'django.contrib.staticfiles.finders.FileSystemFinder', 78 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 79 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 80 | ) 81 | 82 | # Make this unique, and don't share it with anybody. 83 | SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'do_not_use_this_key') 84 | 85 | # List of callables that know how to import templates from various sources. 86 | TEMPLATE_LOADERS = ( 87 | 'django.template.loaders.filesystem.Loader', 88 | 'django.template.loaders.app_directories.Loader', 89 | # 'django.template.loaders.eggs.Loader', 90 | ) 91 | 92 | MIDDLEWARE_CLASSES = ( 93 | 'django.middleware.common.CommonMiddleware', 94 | 'django.contrib.sessions.middleware.SessionMiddleware', 95 | 'django.middleware.csrf.CsrfViewMiddleware', 96 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 97 | 'django.contrib.messages.middleware.MessageMiddleware', 98 | 'example.middleware.XsSharingMiddleware', 99 | # Uncomment the next line for simple clickjacking protection: 100 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 101 | ) 102 | 103 | TEMPLATE_CONTEXT_PROCESSORS = DEFAULT_SETTINGS.TEMPLATE_CONTEXT_PROCESSORS + ( 104 | "django.core.context_processors.request", 105 | ) 106 | 107 | ROOT_URLCONF = 'example.urls' 108 | 109 | # Python dotted path to the WSGI application used by Django's runserver. 110 | WSGI_APPLICATION = 'example.wsgi.application' 111 | 112 | TEMPLATE_DIRS = ( 113 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 114 | # Always use forward slashes, even on Windows. 115 | # Don't forget to use absolute paths, not relative paths. 116 | os.path.join(os.path.dirname(os.path.realpath(__file__)), 'templates'), 117 | ) 118 | 119 | INSTALLED_APPS = ( 120 | 'django.contrib.auth', 121 | 'django.contrib.contenttypes', 122 | 'django.contrib.sessions', 123 | 'django.contrib.sites', 124 | 'django.contrib.messages', 125 | 'django.contrib.staticfiles', 126 | 'django.contrib.admin', 127 | 'oauth2_provider', 128 | 'south', 129 | 'example', 130 | ) 131 | 132 | # A sample logging configuration. The only tangible logging 133 | # performed by this configuration is to send an email to 134 | # the site admins on every HTTP 500 error when DEBUG=False. 135 | # See http://docs.djangoproject.com/en/dev/topics/logging for 136 | # more details on how to customize your logging configuration. 137 | LOGGING = { 138 | 'version': 1, 139 | 'disable_existing_loggers': False, 140 | 'formatters': { 141 | 'verbose': { 142 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 143 | }, 144 | 'simple': { 145 | 'format': '%(levelname)s %(message)s' 146 | }, 147 | }, 148 | 'filters': { 149 | 'require_debug_false': { 150 | '()': 'django.utils.log.RequireDebugFalse' 151 | } 152 | }, 153 | 'handlers': { 154 | 'mail_admins': { 155 | 'level': 'ERROR', 156 | 'filters': ['require_debug_false'], 157 | 'class': 'django.utils.log.AdminEmailHandler' 158 | }, 159 | 'console': { 160 | 'level': 'DEBUG', 161 | 'class': 'logging.StreamHandler', 162 | 'formatter': 'simple' 163 | } 164 | }, 165 | 'loggers': { 166 | 'django.request': { 167 | 'handlers': ['mail_admins'], 168 | 'level': 'ERROR', 169 | 'propagate': True, 170 | }, 171 | 'oauth2_provider': { 172 | 'handlers': ['console'], 173 | 'level': 'DEBUG', 174 | 'propagate': True, 175 | }, 176 | 'oauthlib': { 177 | 'handlers': ['console'], 178 | 'level': 'DEBUG', 179 | 'propagate': True, 180 | } 181 | } 182 | } 183 | 184 | OAUTH2_PROVIDER = { 185 | 'SCOPES': {'example': 'This is an example scope'}, 186 | 'APPLICATION_MODEL': 'example.MyApplication' 187 | } 188 | 189 | from django.core.urlresolvers import reverse_lazy 190 | 191 | LOGIN_REDIRECT_URL = reverse_lazy('home') 192 | -------------------------------------------------------------------------------- /example/example/templates/example/api-client.html: -------------------------------------------------------------------------------- 1 | {% extends "example/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block content %} 5 |

Play with the API

6 | 7 |
8 |
9 |
10 |
11 | 12 |
13 | 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 | 30 | 31 | If you have not registered an Application, try using this builtin token: test_access_token 32 | 33 |
34 |
35 |
36 | 37 |
38 | 39 | 40 | Use JSON format, e.g. {"param":"value"} 41 | 42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 |
50 |
51 |
52 |
53 |

Response:

54 |

 55 |     
56 |
57 | 58 |

API Cheat Sheet

59 | 60 |
61 | 62 |

GET /system_info

63 |

Show simple system informations, this resource is not protected in any way.

64 |

Authentication: none

65 | 66 |

GET /applications

67 |

Retrieve the list of Applications present in the playground

68 |

Authentication: none

69 | 70 |

POST /applications

71 |

Create a new Application

72 |

Authentication: OAuth2, scopes required: can_create_application

73 |

Parameters:

74 |
    75 |
  • name (optional) - Friendly name for the Application
  • 76 |
  • client_id (optional) - The client identifier issued to the client during the registration
  • 77 |
  • client_secret (optional) - Confidential secret issued to the client duringthe registration
  • 78 |
  • client_type (required, values: [public,private]) - Confidential secret issued to the client duringthe registration
  • 79 |
  • authorization_grant_type (required, values: [authorization-code,implicit,passwordl,client-credentials,all-in-one]) - Authorization flow
  • 80 |
81 | 82 |

GET /applications/:lookup

83 |

Retrieve Application detail

84 |

Authentication: OAuth2

85 | 86 |

PUT /applications/:lookup

87 |

Update Application details

88 |

Authentication: OAuth2

89 |

Parameters: same as application creation

90 | 91 |

DELETE /applications/:lookup

92 |

Delete application

93 |

Authentication: OAuth2

94 | 95 |
96 | {% endblock %} 97 | 98 | 99 | {% block javascript %} 100 | 167 | {% endblock javascript %} -------------------------------------------------------------------------------- /docs/rest-framework/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | Django OAuth Toolkit provide a support layer for `Django REST Framework `_. 5 | This tutorial it's based on the Django REST Framework example and shows you how to easily integrate with it. 6 | 7 | Step 1: Minimal setup 8 | ---------------------------- 9 | 10 | Create a virtualenv and install following packages using `pip`... 11 | 12 | :: 13 | 14 | pip install django-oauth-toolkit djangorestframework 15 | 16 | Start a new Django project and add `'rest_framework'` and `'oauth2_provider'` to your `INSTALLED_APPS` setting. 17 | 18 | .. code-block:: python 19 | 20 | INSTALLED_APPS = ( 21 | 'django.contrib.admin', 22 | ... 23 | 'oauth2_provider', 24 | 'rest_framework', 25 | ) 26 | 27 | Now we need to tell Django REST Framework to use the new authentication backend. 28 | To do so add the following lines add the end of your `settings.py` module: 29 | 30 | .. code-block:: python 31 | 32 | REST_FRAMEWORK = { 33 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 34 | 'oauth2_provider.ext.rest_framework.OAuth2Authentication', 35 | ) 36 | } 37 | 38 | Step 2: Create a simple API 39 | -------------------------- 40 | 41 | Let's create a simple API for accessing users and groups. 42 | 43 | Here's our project's root `urls.py` module: 44 | 45 | .. code-block:: python 46 | 47 | from django.conf.urls.defaults import url, patterns, include 48 | from django.contrib.auth.models import User, Group 49 | from django.contrib import admin 50 | admin.autodiscover() 51 | 52 | from rest_framework import viewsets, routers 53 | from rest_framework import permissions 54 | 55 | from oauth2_provider.ext.rest_framework import TokenHasReadWriteScope, TokenHasScope 56 | 57 | 58 | # ViewSets define the view behavior. 59 | class UserViewSet(viewsets.ModelViewSet): 60 | permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope] 61 | model = User 62 | 63 | 64 | class GroupViewSet(viewsets.ModelViewSet): 65 | permission_classes = [permissions.IsAuthenticated, TokenHasScope] 66 | required_scopes = ['groups'] 67 | model = Group 68 | 69 | 70 | # Routers provide an easy way of automatically determining the URL conf 71 | router = routers.DefaultRouter() 72 | router.register(r'users', UserViewSet) 73 | router.register(r'groups', GroupViewSet) 74 | 75 | 76 | # Wire up our API using automatic URL routing. 77 | # Additionally, we include login URLs for the browseable API. 78 | urlpatterns = patterns('', 79 | url(r'^', include(router.urls)), 80 | url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), 81 | url(r'^admin/', include(admin.site.urls)), 82 | ) 83 | 84 | Also add the following to your `settings.py` module: 85 | 86 | .. code-block:: python 87 | 88 | OAUTH2_PROVIDER = { 89 | # this is the list of available scopes 90 | 'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'groups': 'Access to your groups'} 91 | } 92 | 93 | REST_FRAMEWORK = { 94 | # ... 95 | 96 | 'DEFAULT_PERMISSION_CLASSES': ( 97 | 'rest_framework.permissions.IsAuthenticated', 98 | ) 99 | } 100 | 101 | `OAUTH2_PROVIDER.SCOPES` parameter contains the scopes that the application will be aware of, 102 | so we can use them for permission check. 103 | 104 | Now run `python manage.py syncdb`, login to admin and create some users and groups. 105 | 106 | Step 3: Register an application 107 | ------------------------------- 108 | 109 | To obtain a valid access_token first we must register an application. DOT has a set of customizable 110 | views you can use to CRUD application instances, just point your browser at: 111 | 112 | `http://localhost:8000/o/applications/` 113 | 114 | Click the button `New Application` and fill the form with the following data: 115 | 116 | * User: *your current user* 117 | * Client Type: *confidential* 118 | * Authorization Grant Type: *Resource owner password-based* 119 | 120 | Save your app! 121 | 122 | Step 4: Get your token and use your API 123 | --------------------------------------- 124 | 125 | At this point we're ready to request an access_token. Open your shell 126 | 127 | :: 128 | 129 | curl -X POST -d "grant_type=password&username=&password=" http://:@localhost:8000/o/token/ 130 | 131 | The *user_name* and *password* are the credential on any user registered in your :term:`Authorization Server`, like any user created in Step 2. 132 | Response should be something like: 133 | 134 | .. code-block:: javascript 135 | 136 | { 137 | "access_token": "", 138 | "token_type": "Bearer", 139 | "expires_in": 36000, 140 | "refresh_token": "", 141 | "scope": "read write groups" 142 | } 143 | 144 | Grab your access_token and start using your new OAuth2 API: 145 | 146 | :: 147 | 148 | # Retrieve users 149 | curl -H "Authorization: Bearer " http://localhost:8000/users/ 150 | curl -H "Authorization: Bearer " http://localhost:8000/users/1/ 151 | 152 | # Retrieve groups 153 | curl -H "Authorization: Bearer " http://localhost:8000/groups/ 154 | 155 | # Insert a new user 156 | curl -H "Authorization: Bearer " -X POST -d"username=foo&password=bar" http://localhost:8000/users/ 157 | 158 | Step 5: Testing Restricted Access 159 | --------------------------------- 160 | 161 | Let's try to access resources usign a token with a restricted scope adding a `scope` parameter to the token request 162 | 163 | :: 164 | 165 | curl -X POST -d "grant_type=password&username=&password=&scope=read" http://:@localhost:8000/o/token/ 166 | 167 | As you can see the only scope provided is `read`: 168 | 169 | .. code-block:: javascript 170 | 171 | { 172 | "access_token": "", 173 | "token_type": "Bearer", 174 | "expires_in": 36000, 175 | "refresh_token": "", 176 | "scope": "read" 177 | } 178 | 179 | We now try to access our resources: 180 | 181 | :: 182 | 183 | # Retrieve users 184 | curl -H "Authorization: Bearer " http://localhost:8000/users/ 185 | curl -H "Authorization: Bearer " http://localhost:8000/users/1/ 186 | 187 | Ok, this one works since users read only requires `read` scope. 188 | 189 | :: 190 | 191 | # 'groups' scope needed 192 | curl -H "Authorization: Bearer " http://localhost:8000/groups/ 193 | 194 | # 'write' scope needed 195 | curl -H "Authorization: Bearer " -X POST -d"username=foo&password=bar" http://localhost:8000/users/ 196 | 197 | You'll get a `"You do not have permission to perform this action"` error because your access_token does not provide the 198 | required scopes `groups` and `write`. 199 | -------------------------------------------------------------------------------- /oauth2_provider/views/base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.http import HttpResponse, HttpResponseRedirect 4 | from django.views.generic import View, FormView 5 | from django.utils import timezone 6 | 7 | from oauthlib.oauth2 import Server 8 | 9 | from braces.views import LoginRequiredMixin, CsrfExemptMixin 10 | 11 | from ..settings import oauth2_settings 12 | from ..exceptions import OAuthToolkitError 13 | from ..forms import AllowForm 14 | from ..models import get_application_model 15 | from .mixins import OAuthLibMixin 16 | 17 | Application = get_application_model() 18 | 19 | log = logging.getLogger('oauth2_provider') 20 | 21 | 22 | class BaseAuthorizationView(LoginRequiredMixin, OAuthLibMixin, View): 23 | """ 24 | Implements a generic endpoint to handle *Authorization Requests* as in :rfc:`4.1.1`. The view 25 | does not implement any strategy to determine *authorize/do not authorize* logic. 26 | The endpoint is used in the following flows: 27 | 28 | * Authorization code 29 | * Implicit grant 30 | 31 | """ 32 | def dispatch(self, request, *args, **kwargs): 33 | self.oauth2_data = {} 34 | return super(BaseAuthorizationView, self).dispatch(request, *args, **kwargs) 35 | 36 | def error_response(self, error, **kwargs): 37 | """ 38 | Handle errors either by redirecting to redirect_uri with a json in the body containing 39 | error details or providing an error response 40 | """ 41 | redirect, error_response = super(BaseAuthorizationView, self).error_response(error, **kwargs) 42 | 43 | if redirect: 44 | return HttpResponseRedirect(error_response['url']) 45 | 46 | status = error_response['error'].status_code 47 | return self.render_to_response(error_response, status=status) 48 | 49 | 50 | class AuthorizationView(BaseAuthorizationView, FormView): 51 | """ 52 | Implements and endpoint to handle *Authorization Requests* as in :rfc:`4.1.1` and prompting the 53 | user with a form to determine if she authorizes the client application to access her data. 54 | This endpoint is reached two times during the authorization process: 55 | * first receive a ``GET`` request from user asking authorization for a certain client 56 | application, a form is served possibly showing some useful info and prompting for 57 | *authorize/do not authorize*. 58 | 59 | * then receive a ``POST`` request possibly after user authorized the access 60 | 61 | Some informations contained in the ``GET`` request and needed to create a Grant token during 62 | the ``POST`` request would be lost between the two steps above, so they are temporary stored in 63 | hidden fields on the form. 64 | A possible alternative could be keeping such informations in the session. 65 | 66 | The endpoint is used in the followin flows: 67 | * Authorization code 68 | * Implicit grant 69 | """ 70 | template_name = 'oauth2_provider/authorize.html' 71 | form_class = AllowForm 72 | 73 | server_class = Server 74 | validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS 75 | 76 | def get_initial(self): 77 | # TODO: move this scopes conversion from and to string into a utils function 78 | scopes = self.oauth2_data.get('scope', self.oauth2_data.get('scopes', [])) 79 | initial_data = { 80 | 'redirect_uri': self.oauth2_data.get('redirect_uri', None), 81 | 'scope': ' '.join(scopes), 82 | 'client_id': self.oauth2_data.get('client_id', None), 83 | 'state': self.oauth2_data.get('state', None), 84 | 'response_type': self.oauth2_data.get('response_type', None), 85 | } 86 | return initial_data 87 | 88 | def form_valid(self, form): 89 | try: 90 | credentials = { 91 | 'client_id': form.cleaned_data.get('client_id'), 92 | 'redirect_uri': form.cleaned_data.get('redirect_uri'), 93 | 'response_type': form.cleaned_data.get('response_type', None), 94 | 'state': form.cleaned_data.get('state', None), 95 | } 96 | 97 | scopes = form.cleaned_data.get('scope') 98 | allow = form.cleaned_data.get('allow') 99 | uri, headers, body, status = self.create_authorization_response( 100 | request=self.request, scopes=scopes, credentials=credentials, allow=allow) 101 | self.success_url = uri 102 | log.debug("Success url for the request: {0}".format(self.success_url)) 103 | return super(AuthorizationView, self).form_valid(form) 104 | 105 | except OAuthToolkitError as error: 106 | return self.error_response(error) 107 | 108 | def get(self, request, *args, **kwargs): 109 | try: 110 | scopes, credentials = self.validate_authorization_request(request) 111 | kwargs['scopes_descriptions'] = [oauth2_settings.SCOPES[scope] for scope in scopes] 112 | kwargs['scopes'] = scopes 113 | # at this point we know an Application instance with such client_id exists in the database 114 | kwargs['application'] = Application.objects.get(client_id=credentials['client_id']) # TODO: cache it! 115 | kwargs.update(credentials) 116 | self.oauth2_data = kwargs 117 | # following two loc are here only because of https://code.djangoproject.com/ticket/17795 118 | form = self.get_form(self.get_form_class()) 119 | kwargs['form'] = form 120 | 121 | # Check to see if the user has already granted access and return 122 | # a successful response depending on 'approval_prompt' url parameter 123 | require_approval = request.GET.get('approval_prompt', oauth2_settings.REQUEST_APPROVAL_PROMPT) 124 | if require_approval == 'auto': 125 | tokens = request.user.accesstoken_set.filter(application=kwargs['application'], 126 | expires__gt=timezone.now()).all() 127 | # check past authorizations regarded the same scopes as the current one 128 | for token in tokens: 129 | if token.allow_scopes(scopes): 130 | uri, headers, body, status = self.create_authorization_response( 131 | request=self.request, scopes=" ".join(scopes), 132 | credentials=credentials, allow=True) 133 | return HttpResponseRedirect(uri) 134 | 135 | return self.render_to_response(self.get_context_data(**kwargs)) 136 | 137 | except OAuthToolkitError as error: 138 | return self.error_response(error) 139 | 140 | 141 | class TokenView(CsrfExemptMixin, OAuthLibMixin, View): 142 | """ 143 | Implements an endpoint to provide access tokens 144 | 145 | The endpoint is used in the following flows: 146 | * Authorization code 147 | * Password 148 | * Client credentials 149 | """ 150 | server_class = Server 151 | validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS 152 | 153 | def post(self, request, *args, **kwargs): 154 | url, headers, body, status = self.create_token_response(request) 155 | response = HttpResponse(content=body, status=status) 156 | 157 | for k, v in headers.items(): 158 | response[k] = v 159 | return response 160 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoOAuthToolkit.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoOAuthToolkit.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoOAuthToolkit" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoOAuthToolkit" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/tutorial/tutorial_01.rst: -------------------------------------------------------------------------------- 1 | Part 1 - Make a Provider in a Minute 2 | ==================================== 3 | 4 | Scenario 5 | -------- 6 | You want to make your own :term:`Authorization Server` to issue access tokens to client applications for a certain API. 7 | 8 | Start Your App 9 | -------------- 10 | During this tutorial you will make an XHR POST from a Heroku deployed app to your localhost instance. 11 | Since the domain that will originate the request (the app on Heroku) is different than the destination domain (your local instance), 12 | you will need to install the `django-cors-headers `_ app. 13 | These "cross-domain" requests are by default forbidden by web browsers unless you use `CORS `_. 14 | 15 | Create a virtualenv and install `django-oauth-toolkit` and `django-cors-headers`: 16 | 17 | :: 18 | 19 | pip install django-oauth-toolkit django-cors-headers 20 | 21 | Start a Django project, add `oauth2_provider` and `corsheaders` to the installed apps, and enable admin: 22 | 23 | .. code-block:: python 24 | 25 | INSTALLED_APPS = { 26 | 'django.contrib.admin', 27 | # ... 28 | 'oauth2_provider', 29 | 'corsheaders', 30 | } 31 | 32 | Include the Django OAuth Toolkit urls in your `urls.py`, choosing the urlspace you prefer. For example: 33 | 34 | .. code-block:: python 35 | 36 | urlpatterns = patterns( 37 | '', 38 | url(r'^admin/', include(admin.site.urls)), 39 | url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), 40 | # ... 41 | ) 42 | 43 | Include the CORS middleware in your `settings.py`: 44 | 45 | .. code-block:: python 46 | 47 | MIDDLEWARE_CLASSES = ( 48 | # ... 49 | 'corsheaders.middleware.CorsMiddleware', 50 | # ... 51 | ) 52 | 53 | Allow CORS requests from all domains (just for the scope of this tutorial): 54 | 55 | .. code-block:: python 56 | 57 | CORS_ORIGIN_ALLOW_ALL = True 58 | 59 | .. _loginTemplate: 60 | 61 | Include the required hidden input in your login template, `registration/login.html`. 62 | The ``{{ next }}`` template context variable will be populated with the correct 63 | redirect value. See the `Django documentation `_ 64 | for details on using login templates. 65 | 66 | .. code-block:: html 67 | 68 | 69 | 70 | As a final step, execute syncdb, start the internal server, and login with your credentials. 71 | 72 | Create an OAuth2 Client Application 73 | ----------------------------------- 74 | Before your :term:`Application` can use the :term:`Authorization Server` for user login, 75 | you must first register the app (also known as the :term:`Client`.) Once registered, your app will be granted access to 76 | the API, subject to approval by its users. 77 | 78 | Let's register your application. 79 | 80 | Point your browser to http://localhost:8000/o/applications/ and add an Application instance. 81 | `Client id` and `Client Secret` are automatically generated, you have to provide the rest of the informations: 82 | 83 | * `User`: the owner of the Application (e.g. a developer, or the currently logged in user.) 84 | 85 | * `Redirect uris`: Applications must register at least one redirection endpoint prior to utilizing the 86 | authorization endpoint. The :term:`Authorization Server` will deliver the access token to the client only if the client 87 | specifies one of the verified redirection uris. For this tutorial, paste verbatim the value 88 | `http://django-oauth-toolkit.herokuapp.com/consumer/exchange/` 89 | 90 | * `Client type`: this value affects the security level at which some communications between the client application and 91 | the authorization server are performed. For this tutorial choose *Confidential*. 92 | 93 | * `Authorization grant type`: choose *Authorization code* 94 | 95 | * `Name`: this is the name of the client application on the server, and will be displayed on the authorization request 96 | page, where users can allow/deny access to their data. 97 | 98 | Take note of the `Client id` and the `Client Secret` then logout (this is needed only for testing the authorization 99 | process we'll explain shortly) 100 | 101 | Test Your Authorization Server 102 | ------------------------------ 103 | Your authorization server is ready and can begin issuing access tokens. To test the process you need an OAuth2 104 | consumer; if you are familiar enough with OAuth2, you can use curl, requests, or anything that speaks http. For the rest 105 | of us, there is a `consumer service `_ deployed on Heroku to test 106 | your provider. 107 | 108 | Build an Authorization Link for Your Users 109 | ++++++++++++++++++++++++++++++++++++++++++ 110 | Authorizing an application to access OAuth2 protected data in an :term:`Authorization Code` flow is always initiated 111 | by the user. Your application can prompt users to click a special link to start the process. Go to the 112 | `Consumer `_ page and complete the form by filling in your 113 | application's details obtained from the steps in this tutorial. Submit the form, and you'll receive a link your users can 114 | use to access the authorization page. 115 | 116 | Authorize the Application 117 | +++++++++++++++++++++++++ 118 | When a user clicks the link, she is redirected to your (possibly local) :term:`Authorization Server`. 119 | If you're not logged in, you will be prompted for username and password. This is because the authorization 120 | page is login protected by django-oauth-toolkit. Login, then you should see the (not so cute) form users can use to give 121 | her authorization to the client application. Flag the *Allow* checkbox and click *Authorize*, you will be redirected 122 | again on to the consumer service. 123 | 124 | __ loginTemplate_ 125 | 126 | If you are not redirected to the correct page after logging in successfully, 127 | you probably need to `setup your login template correctly`__. 128 | 129 | Exchange the token 130 | ++++++++++++++++++ 131 | At this point your autorization server redirected the user to a special page on the consumer passing in an 132 | :term:`Authorization Code`, a special token the consumer will use to obtain the final access token. 133 | This operation is usually done automatically by the client application during the request/response cycle, but we cannot 134 | make a POST request from Heroku to your localhost, so we proceed manually with this step. Fill the form with the 135 | missing data and click *Submit*. 136 | If everything is ok, you will be routed to another page showing your access token, the token type, its lifetime and 137 | the :term:`Refresh Token`. 138 | 139 | Refresh the token 140 | +++++++++++++++++ 141 | The page showing the access token retrieved from the :term:`Authorization Server` also let you make a POST request to 142 | the server itself to swap the refresh token for another, brand new access token. 143 | Just fill in the missing form fields and click the Refresh button: if everything goes smooth you will see the access and 144 | refresh token change their values, otherwise you will likely see an error message. 145 | When finished playing with your authorization server, take note of both the access and refresh tokens, we will use them 146 | for the next part of the tutorial. 147 | 148 | So let's make an API and protect it with your OAuth2 tokens in the :doc:`part 2 of the tutorial `. 149 | 150 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django OAuth Toolkit 2 | ==================== 3 | 4 | *OAuth2 goodies for the Djangonauts!* 5 | 6 | .. image:: https://badge.fury.io/py/django-oauth-toolkit.png 7 | :target: http://badge.fury.io/py/django-oauth-toolkit 8 | 9 | .. image:: https://pypip.in/d/django-oauth-toolkit/badge.png 10 | :target: https://crate.io/packages/django-oauth-toolkit?version=latest 11 | 12 | .. image:: https://travis-ci.org/evonove/django-oauth-toolkit.png 13 | :alt: Build Status 14 | :target: https://travis-ci.org/evonove/django-oauth-toolkit 15 | 16 | .. image:: https://coveralls.io/repos/evonove/django-oauth-toolkit/badge.png 17 | :alt: Coverage Status 18 | :target: https://coveralls.io/r/evonove/django-oauth-toolkit 19 | 20 | If you are facing one or more of the following: 21 | * Your Django app exposes a web API you want to protect with OAuth2 authentication, 22 | * You need to implement an OAuth2 authorization server to provide tokens management for your infrastructure, 23 | 24 | Django OAuth Toolkit can help you providing out of the box all the endpoints, data and logic needed to add OAuth2 25 | capabilities to your Django projects. Django OAuth Toolkit makes extensive use of the excellent 26 | `OAuthLib `_, so that everything is 27 | `rfc-compliant `_. 28 | 29 | Support 30 | ------- 31 | 32 | If you need support please send a message to the `Django OAuth Toolkit Google Group `_ 33 | 34 | Contributing 35 | ------------ 36 | 37 | We love contributions, so please feel free to fix bugs, improve things, provide documentation. Just `follow the 38 | guidelines `_ and submit a PR. 39 | 40 | Requirements 41 | ------------ 42 | 43 | * Python 2.6, 2.7, 3.3 44 | * Django 1.4, 1.5, 1.6 45 | 46 | Installation 47 | ------------ 48 | 49 | Install with pip:: 50 | 51 | pip install django-oauth-toolkit 52 | 53 | Add `oauth2_provider` to your `INSTALLED_APPS` 54 | 55 | .. code-block:: python 56 | 57 | INSTALLED_APPS = ( 58 | ... 59 | 'oauth2_provider', 60 | ) 61 | 62 | 63 | If you need an OAuth2 provider you'll want to add the following to your urls.py. 64 | Notice that `oauth2_provider` namespace is mandatory. 65 | 66 | .. code-block:: python 67 | 68 | urlpatterns = patterns( 69 | ... 70 | url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), 71 | ) 72 | 73 | Documentation 74 | -------------- 75 | 76 | The `full documentation `_ is on *Read the Docs*. 77 | 78 | License 79 | ------- 80 | 81 | django-oauth-toolkit is released under the terms of the **BSD license**. Full details in ``LICENSE`` file. 82 | 83 | Roadmap / Todo list (help wanted) 84 | --------------------------------- 85 | 86 | * OAuth1 support 87 | * OpenID connector 88 | * Nonrel storages support 89 | 90 | Changelog 91 | --------- 92 | 93 | 0.7.1 [2014-04-27] 94 | ~~~~~~~~~~~~~~~~~~ 95 | 96 | * Added database indexes to the OAuth2 related models to improve performances. 97 | 98 | **Warning: schema migration does not work for sqlite3 database, migration should be performed manually** 99 | 100 | 0.7.0 [2014-03-01] 101 | ~~~~~~~~~~~~~~~~~~ 102 | 103 | * Created a setting for the default value for approval prompt. 104 | * Improved docs 105 | * Don't pin django-braces and six versions 106 | 107 | **Backwards incompatible changes in 0.7.0** 108 | 109 | * Make Application model truly "swappable" (introduces a new non-namespaced setting OAUTH2_PROVIDER_APPLICATION_MODEL) 110 | 111 | 0.6.1 [2014-02-05] 112 | ~~~~~~~~~~~~~~~~~~ 113 | 114 | * added support for `scope` query parameter keeping backwards compatibility for the original `scopes` parameter. 115 | * __str__ method in Application model returns content of `name` field when available 116 | 117 | 0.6.0 [2014-01-26] 118 | ~~~~~~~~~~~~~~~~~~ 119 | 120 | * oauthlib 0.6.1 support 121 | * Django dev branch support 122 | * Python 2.6 support 123 | * Skip authorization form via `approval_prompt` parameter 124 | 125 | **Bugfixes** 126 | 127 | * Several fixes to the docs 128 | * Issue #71: Fix migrations 129 | * Issue #65: Use OAuth2 password grant with multiple devices 130 | * Issue #84: Add information about login template to tutorial. 131 | * Issue #64: Fix urlencode clientid secret 132 | 133 | 0.5.0 [2013-09-17] 134 | ~~~~~~~~~~~~~~~~~~ 135 | 136 | * oauthlib 0.6.0 support 137 | 138 | **Backwards incompatible changes in 0.5.0** 139 | 140 | * `backends.py` module has been renamed to `oauth2_backends.py` so you should change your imports whether 141 | you're extending this module 142 | 143 | **Bugfixes** 144 | 145 | * Issue #54: Auth backend proposal to address #50 146 | * Issue #61: Fix contributing page 147 | * Issue #55: Add support for authenticating confidential client with request body params 148 | * Issue #53: Quote characters in the url query that are safe for Django but not for oauthlib 149 | 150 | 0.4.1 [2013-09-06] 151 | ~~~~~~~~~~~~~~~~~~ 152 | 153 | * Optimize queries on access token validation 154 | 155 | 0.4.0 [2013-08-09] 156 | ~~~~~~~~~~~~~~~~~~ 157 | 158 | **New Features** 159 | 160 | * Add Application management views, you no more need the admin to register, update and delete your application. 161 | * Add support to configurable application model 162 | * Add support for function based views 163 | 164 | **Backwards incompatible changes in 0.4.0** 165 | 166 | * `SCOPE` attribute in settings is now a dictionary to store `{'scope_name': 'scope_description'}` 167 | * Namespace 'oauth2_provider' is mandatory in urls. See issue #36 168 | 169 | **Bugfixes** 170 | 171 | * Issue #25: Bug in the Basic Auth parsing in Oauth2RequestValidator 172 | * Issue #24: Avoid generation of client_id with ":" colon char when using HTTP Basic Auth 173 | * Issue #21: IndexError when trying to authorize an application 174 | * Issue #9: Default_redirect_uri is mandatory when grant_type is implicit, authorization_code or all-in-one 175 | * Issue #22: Scopes need a verbose description 176 | * Issue #33: Add django-oauth-toolkit version on example main page 177 | * Issue #36: Add mandatory namespace to urls 178 | * Issue #31: Add docstring to OAuthToolkitError and FatalClientError 179 | * Issue #32: Add docstring to validate_uris 180 | * Issue #34: Documentation tutorial part1 needs corsheaders explanation 181 | * Issue #36: Add mandatory namespace to urls 182 | * Issue #45: Add docs for AbstractApplication 183 | * Issue #47: Add docs for views decorators 184 | 185 | 186 | 0.3.2 [2013-07-10] 187 | ~~~~~~~~~~~~~~~~~~ 188 | 189 | * Bugfix #37: Error in migrations with custom user on Django 1.5 190 | 191 | 0.3.1 [2013-07-10] 192 | ~~~~~~~~~~~~~~~~~~ 193 | 194 | * Bugfix #27: OAuthlib refresh token refactoring 195 | 196 | 0.3.0 [2013-06-14] 197 | ~~~~~~~~~~~~~~~~~~ 198 | 199 | * `Django REST Framework `_ integration layer 200 | * Bugfix #13: Populate request with client and user in validate_bearer_token 201 | * Bugfix #12: Fix paths in documentation 202 | 203 | **Backwards incompatible changes in 0.3.0** 204 | 205 | * `requested_scopes` parameter in ScopedResourceMixin changed to `required_scopes` 206 | 207 | 0.2.1 [2013-06-06] 208 | ~~~~~~~~~~~~~~~~~~ 209 | 210 | * Core optimizations 211 | 212 | 0.2.0 [2013-06-05] 213 | ~~~~~~~~~~~~~~~~~~ 214 | 215 | * Add support for Django1.4 and Django1.6 216 | * Add support for Python 3.3 217 | * Add a default ReadWriteScoped view 218 | * Add tutorial to docs 219 | 220 | 0.1.0 [2013-05-31] 221 | ~~~~~~~~~~~~~~~~~~ 222 | 223 | * Support OAuth2 Authorization Flows 224 | 225 | 0.0.0 [2013-05-17] 226 | ~~~~~~~~~~~~~~~~~~ 227 | 228 | * Discussion with Daniel Greenfeld at Django Circus 229 | * Ignition 230 | --------------------------------------------------------------------------------