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
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 |
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 |
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 |
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
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 |
--------------------------------------------------------------------------------