From 468b5e43e2582513c4ae862efa4511ea8313031e Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 02:06:20 +0100 Subject: [PATCH 01/17] Add tests for OAuth2 authentication --- rest_framework/tests/authentication.py | 110 ++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 2a2bfba954..3ceab80858 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.http import HttpResponse from django.test import Client, TestCase @@ -6,11 +7,15 @@ from rest_framework import permissions from rest_framework import status from rest_framework.authtoken.models import Token -from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication -from rest_framework.compat import patterns +from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication, OAuth2Authentication +from rest_framework.compat import patterns, url, include +from rest_framework.compat import oauth2 +from rest_framework.compat import oauth2_provider from rest_framework.views import APIView import json import base64 +import datetime +import unittest class MockView(APIView): @@ -22,11 +27,16 @@ def post(self, request): def put(self, request): return HttpResponse({'a': 1, 'b': 2, 'c': 3}) + def get(self, request): + return HttpResponse({'a': 1, 'b': 2, 'c': 3}) + urlpatterns = patterns('', (r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])), (r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])), (r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])), (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'), + url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')), + url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])), ) @@ -187,3 +197,99 @@ def test_token_login_form(self): {'username': self.username, 'password': self.password}) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key) + + +class OAuth2Tests(TestCase): + """OAuth 2.0 authentication""" + urls = 'rest_framework.tests.authentication' + + def setUp(self): + self.csrf_client = Client(enforce_csrf_checks=True) + self.username = 'john' + self.email = 'lennon@thebeatles.com' + self.password = 'password' + self.user = User.objects.create_user(self.username, self.email, self.password) + + self.CLIENT_ID = 'client_key' + self.CLIENT_SECRET = 'client_secret' + self.ACCESS_TOKEN = "access_token" + self.REFRESH_TOKEN = "refresh_token" + + self.oauth2_client = oauth2.models.Client.objects.create( + client_id=self.CLIENT_ID, + client_secret=self.CLIENT_SECRET, + redirect_uri='', + client_type=0, + name='example', + user=None, + ) + + self.access_token = oauth2.models.AccessToken.objects.create( + token=self.ACCESS_TOKEN, + client=self.oauth2_client, + user=self.user, + ) + self.refresh_token = oauth2.models.RefreshToken.objects.create( + user=self.user, + access_token=self.access_token, + client=self.oauth2_client + ) + + def _create_authorization_header(self, token=None): + return "Bearer {0}".format(token or self.access_token.token) + + def _client_credentials_params(self): + return {'client_id': self.CLIENT_ID, 'client_secret': self.CLIENT_SECRET} + + @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + def test_get_form_with_wrong_client_data_failing_auth(self): + """Ensure GETing form over OAuth with incorrect client credentials fails""" + auth = self._create_authorization_header() + params = self._client_credentials_params() + params['client_id'] += 'a' + response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 401) + + @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + def test_get_form_passing_auth(self): + """Ensure GETing form over OAuth with correct client credentials succeed""" + auth = self._create_authorization_header() + params = self._client_credentials_params() + response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + + @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + def test_post_form_passing_auth(self): + """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF""" + auth = self._create_authorization_header() + params = self._client_credentials_params() + response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + + @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + def test_post_form_token_removed_failing_auth(self): + """Ensure POSTing when there is no OAuth access token in db fails""" + self.access_token.delete() + auth = self._create_authorization_header() + params = self._client_credentials_params() + response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) + self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + + @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + def test_post_form_with_refresh_token_failing_auth(self): + """Ensure POSTing with refresh token instead of access token fails""" + auth = self._create_authorization_header(token=self.refresh_token.token) + params = self._client_credentials_params() + response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) + self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + + @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + def test_post_form_with_expired_access_token_failing_auth(self): + """Ensure POSTing with expired access token fails with an 'Invalid token' error""" + self.access_token.expires = datetime.datetime.now() - datetime.timedelta(seconds=10) # 10 seconds late + self.access_token.save() + auth = self._create_authorization_header() + params = self._client_credentials_params() + response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) + self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + self.assertIn('Invalid token', response.content) From 4e1f77db1ae48b156cb163a0853776cc23e230b5 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 02:07:53 +0100 Subject: [PATCH 02/17] Add django-oauth2-provider to the installed apps --- rest_framework/runtests/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 03bfc21620..67dc7fff5f 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -97,7 +97,9 @@ # 'django.contrib.admindocs', 'rest_framework', 'rest_framework.authtoken', - 'rest_framework.tests' + 'rest_framework.tests', + 'provider', + 'provider.oauth2', ) STATIC_URL = '/static/' From 592a0a5980e4baf18102769997de8011be72cd6b Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 02:08:21 +0100 Subject: [PATCH 03/17] Add django-oauth2-provider to optionals.txt --- optionals.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/optionals.txt b/optionals.txt index 3d98cc0e73..a36e9e9400 100644 --- a/optionals.txt +++ b/optionals.txt @@ -2,3 +2,4 @@ markdown>=2.1.0 PyYAML>=3.10 defusedxml>=0.3 django-filter>=0.5.4 +-e git+git://github.com/caffeinehit/django-oauth2-provider.git@3198060acfe14730ce2d81310cbf2b13f6403438#egg=django_oauth2_provider-dev \ No newline at end of file From da9d7fb8ec19f289d9d2777738a45007c41a1289 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 02:08:58 +0100 Subject: [PATCH 04/17] Add the OAuth2Authentication class --- rest_framework/authentication.py | 85 +++++++++++++++++++++++++++++++- rest_framework/compat.py | 11 +++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 14b2136b3d..c20d9cb559 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -6,6 +6,7 @@ from django.utils.encoding import DjangoUnicodeDecodeError from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import CsrfViewMiddleware +from rest_framework.compat import oauth2_provider, oauth2 from rest_framework.authtoken.models import Token import base64 @@ -155,4 +156,86 @@ def authenticate_header(self, request): return 'Token' -# TODO: OAuthAuthentication +class OAuth2Authentication(BaseAuthentication): + """ + OAuth 2 authentication backend using `django-oauth2-provider` + """ + require_active = True + + def __init__(self, **kwargs): + super(OAuth2Authentication, self).__init__(**kwargs) + if oauth2_provider is None: + raise ImproperlyConfigured("The 'django-oauth2-provider' package could not be imported. It is required for use with the 'OAuth2Authentication' class.") + + def authenticate(self, request): + """ + The Bearer type is the only finalized type + + Read the spec for more details + http://tools.ietf.org/html/rfc6749#section-7.1 + """ + auth = request.META.get('HTTP_AUTHORIZATION', '').split() + print auth + if not auth or auth[0].lower() != "bearer": + return None + + if len(auth) != 2: + raise exceptions.AuthenticationFailed('Invalid token header') + + return self.authenticate_credentials(request, auth[1]) + + def authenticate_credentials(self, request, access_token): + """ + :returns: two-tuple of (user, auth) if authentication succeeds, or None otherwise. + """ + + # authenticate the client + oauth2_client_form = oauth2.forms.ClientAuthForm(request.REQUEST) + if not oauth2_client_form.is_valid(): + raise exceptions.AuthenticationFailed("Client could not be validated") + client = oauth2_client_form.cleaned_data.get('client') + + # retrieve the `oauth2.models.OAuth2AccessToken` instance from the access_token + auth_backend = oauth2.backends.AccessTokenBackend() + token = auth_backend.authenticate(access_token, client) + if token is None: + raise exceptions.AuthenticationFailed("Invalid token") # does not exist or is expired + + # TODO check scope + # try: + # self.validate_token(request, consumer, token) + # except oauth2.Error, e: + # print "got e" + # raise exceptions.AuthenticationFailed(e.message) + + if not self.check_active(token.user): + raise exceptions.AuthenticationFailed('User not active: %s' % token.user.username) + + if client and token: + request.user = token.user + return (request.user, None) + + raise exceptions.AuthenticationFailed( + 'You are not allowed to access this resource.') + + return None + + def authenticate_header(self, request): + """ + Bearer is the only finalized type currently + + Check details on the `OAuth2Authentication.authenticate` method + """ + return 'Bearer' + + def check_active(self, user): + """ + Ensures the user has an active account. + + Optimized for the ``django.contrib.auth.models.User`` case. + """ + if not self.require_active: + # Ignore & move on. + return True + + return user.is_active diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 07fdddce4c..5bba0c8656 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -426,3 +426,14 @@ def apply_markdown(text): import defusedxml.ElementTree as etree except ImportError: etree = None + + +# OAuth 2 support is optional +try: + import provider as oauth2_provider +except ImportError: + oauth2_provider = None +try: + import provider.oauth2 as oauth2 +except ImportError: + oauth2 = None From d8f455bc0ff920e9e0cd1952f58b5a0eccdc2683 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 02:09:52 +0100 Subject: [PATCH 05/17] Add OAuth2Authentication documentation --- docs/api-guide/authentication.md | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 342fabe7bb..ba7c0c5814 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -182,6 +182,74 @@ Unauthenticated responses that are denied permission will result in an `HTTP 403 If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details. +## OAuth2Authentication + +This authentication uses [OAuth 2.0][rfc6749] authentication scheme. It depends on optional [`django-oauth2-provider`](https://github.com/caffeinehit/django-oauth2-provider). In order to make it work you must install this package and add `provider` and `provider.oauth2` to your `INSTALLED_APPS` : + + INSTALLED_APPS = ( + #(...) + 'provider', + 'provider.oauth2', + ) + +And include the urls needed in your root `urls.py` file to be able to begin the *oauth 2 dance* : + + url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')), + +--- + +** Note:** The *namespace* argument is required ! + +--- + +Finally, sync your database with those two new django apps. + + $ python manage.py syncdb + $ python manage.py migrate + +`OAuth2Authentication` class provides only token verification for requests. The *oauth 2 dance* is taken care by the [`django-oaut2-provider`](https://github.com/caffeinehit/django-oauth2-provider) dependency. Unfortunately, there isn't a lot of [documentation](https://django-oauth2-provider.readthedocs.org/en/latest/) currently on how to *dance* with this package on the client side. + +The Good news is, here is a minimal "How to start" because **OAuth 2** is dramatically simpler than **OAuth 1**, so no more headache with signature, cryptography on client side, and other complex things. + +### How to start with *django-oauth2-provider* ? + +#### Create a client in the django-admin panel + +Go to the admin panel and create a new `Provider.Client` entry. It will create the `client_id` and `client_secret` properties for you. + +#### Request an access token + +Your client interface – I mean by that your iOS code, HTML code, or whatever else language – just have to submit a `POST` request at the url `/oauth2/access_token` with the following fields : + +* `client_id` the client id you've just configured at the previous step. +* `client_secret` again configured at the previous step. +* `username` the username with which you want to log in. +* `password` well, that speaks for itself. + +--- + +**Note:** Remember that you are **highly encourage** to use HTTPS for all your OAuth 2 requests. And by *highly encourage* I mean you SHOULD always use HTTPS otherwise you will expose user passwords for any person who can intercept the request (like a man in the middle attack). + +--- + +You can use the command line to test that your local configuration is working : + + $ curl -X POST -d "client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=password&username=YOUR_USERNAME&password=YOU_PASSWORD" http://localhost:8000/oauth2/access_token/ + +Here is the response you should get : + + {"access_token": "", "scope": "read", "expires_in": 86399, "refresh_token": ""} + +#### Access the api + +The only thing needed to make the `OAuth2Authentication` class work is to insert the `access_token` you've received in the `Authorization` api request header. + +The command line to test the authentication looks like : + + $ curl -H "Authorization: Bearer " http://localhost:8000/api/?client_id=YOUR_CLIENT_ID\&client_secret=YOUR_CLIENT_SECRET + +And hopefully, it will work like a charm. + # Custom authentication To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise. @@ -235,3 +303,4 @@ HTTP digest authentication is a widely implemented scheme that was intended to r [mod_wsgi_official]: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization [juanriaza]: https://github.com/juanriaza [djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth +[rfc6749]: http://tools.ietf.org/html/rfc6749 From 9d5c3060386cc8deb4ee55eda022f0a134e897c0 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 11:53:30 +0100 Subject: [PATCH 06/17] Improve the `django-oauth2-provider` import block to avoid naming collision with `oauth2` used for OAuth 1 --- rest_framework/authentication.py | 10 +++++----- rest_framework/compat.py | 6 +----- rest_framework/tests/authentication.py | 19 +++++++++---------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index c20d9cb559..c94af40514 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -6,7 +6,7 @@ from django.utils.encoding import DjangoUnicodeDecodeError from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import CsrfViewMiddleware -from rest_framework.compat import oauth2_provider, oauth2 +from rest_framework.compat import oauth2_provider from rest_framework.authtoken.models import Token import base64 @@ -190,13 +190,13 @@ def authenticate_credentials(self, request, access_token): """ # authenticate the client - oauth2_client_form = oauth2.forms.ClientAuthForm(request.REQUEST) + oauth2_client_form = oauth2_provider.forms.ClientAuthForm(request.REQUEST) if not oauth2_client_form.is_valid(): raise exceptions.AuthenticationFailed("Client could not be validated") client = oauth2_client_form.cleaned_data.get('client') - # retrieve the `oauth2.models.OAuth2AccessToken` instance from the access_token - auth_backend = oauth2.backends.AccessTokenBackend() + # retrieve the `oauth2_provider.models.OAuth2AccessToken` instance from the access_token + auth_backend = oauth2_provider.backends.AccessTokenBackend() token = auth_backend.authenticate(access_token, client) if token is None: raise exceptions.AuthenticationFailed("Invalid token") # does not exist or is expired @@ -204,7 +204,7 @@ def authenticate_credentials(self, request, access_token): # TODO check scope # try: # self.validate_token(request, consumer, token) - # except oauth2.Error, e: + # except oauth2_provider.Error, e: # print "got e" # raise exceptions.AuthenticationFailed(e.message) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 5bba0c8656..e0a43f3f94 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -430,10 +430,6 @@ def apply_markdown(text): # OAuth 2 support is optional try: - import provider as oauth2_provider + import provider.oauth2 as oauth2_provider except ImportError: oauth2_provider = None -try: - import provider.oauth2 as oauth2 -except ImportError: - oauth2 = None diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index c2c23bccea..1212f0aac7 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -16,7 +16,6 @@ OAuth2Authentication ) from rest_framework.compat import patterns, url, include -from rest_framework.compat import oauth2 from rest_framework.compat import oauth2_provider from rest_framework.tests.utils import RequestFactory from rest_framework.views import APIView @@ -248,7 +247,7 @@ def setUp(self): self.ACCESS_TOKEN = "access_token" self.REFRESH_TOKEN = "refresh_token" - self.oauth2_client = oauth2.models.Client.objects.create( + self.oauth2_client = oauth2_provider.models.Client.objects.create( client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, redirect_uri='', @@ -257,12 +256,12 @@ def setUp(self): user=None, ) - self.access_token = oauth2.models.AccessToken.objects.create( + self.access_token = oauth2_provider.models.AccessToken.objects.create( token=self.ACCESS_TOKEN, client=self.oauth2_client, user=self.user, ) - self.refresh_token = oauth2.models.RefreshToken.objects.create( + self.refresh_token = oauth2_provider.models.RefreshToken.objects.create( user=self.user, access_token=self.access_token, client=self.oauth2_client @@ -274,7 +273,7 @@ def _create_authorization_header(self, token=None): def _client_credentials_params(self): return {'client_id': self.CLIENT_ID, 'client_secret': self.CLIENT_SECRET} - @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') def test_get_form_with_wrong_client_data_failing_auth(self): """Ensure GETing form over OAuth with incorrect client credentials fails""" auth = self._create_authorization_header() @@ -283,7 +282,7 @@ def test_get_form_with_wrong_client_data_failing_auth(self): response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 401) - @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') def test_get_form_passing_auth(self): """Ensure GETing form over OAuth with correct client credentials succeed""" auth = self._create_authorization_header() @@ -291,7 +290,7 @@ def test_get_form_passing_auth(self): response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) - @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') def test_post_form_passing_auth(self): """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF""" auth = self._create_authorization_header() @@ -299,7 +298,7 @@ def test_post_form_passing_auth(self): response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) - @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') def test_post_form_token_removed_failing_auth(self): """Ensure POSTing when there is no OAuth access token in db fails""" self.access_token.delete() @@ -308,7 +307,7 @@ def test_post_form_token_removed_failing_auth(self): response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) - @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') def test_post_form_with_refresh_token_failing_auth(self): """Ensure POSTing with refresh token instead of access token fails""" auth = self._create_authorization_header(token=self.refresh_token.token) @@ -316,7 +315,7 @@ def test_post_form_with_refresh_token_failing_auth(self): response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) - @unittest.skipUnless(oauth2, 'django-oauth2-provider not installed') + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') def test_post_form_with_expired_access_token_failing_auth(self): """Ensure POSTing with expired access token fails with an 'Invalid token' error""" self.access_token.expires = datetime.datetime.now() - datetime.timedelta(seconds=10) # 10 seconds late From 653fcf7e3315f9aefba8474591909e564492ecfe Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 12:02:03 +0100 Subject: [PATCH 07/17] Use the correct doc link style --- docs/api-guide/authentication.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 6a259500b4..c73de1f671 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -184,7 +184,7 @@ If you're using an AJAX style API with SessionAuthentication, you'll need to mak ## OAuth2Authentication -This authentication uses [OAuth 2.0][rfc6749] authentication scheme. It depends on optional [`django-oauth2-provider`](https://github.com/caffeinehit/django-oauth2-provider). In order to make it work you must install this package and add `provider` and `provider.oauth2` to your `INSTALLED_APPS` : +This authentication uses [OAuth 2.0][rfc6749] authentication scheme. It depends on optional [`django-oauth2-provider`][django-oauth2-provider]. In order to make it work you must install this package and add `provider` and `provider.oauth2` to your `INSTALLED_APPS` : INSTALLED_APPS = ( #(...) @@ -207,7 +207,7 @@ Finally, sync your database with those two new django apps. $ python manage.py syncdb $ python manage.py migrate -`OAuth2Authentication` class provides only token verification for requests. The *oauth 2 dance* is taken care by the [`django-oaut2-provider`](https://github.com/caffeinehit/django-oauth2-provider) dependency. Unfortunately, there isn't a lot of [documentation](https://django-oauth2-provider.readthedocs.org/en/latest/) currently on how to *dance* with this package on the client side. +`OAuth2Authentication` class provides only token verification for requests. The *oauth 2 dance* is taken care by the [`django-oaut2-provider`][django-oauth2-provider] dependency. Unfortunately, there isn't a lot of [documentation][django-oauth2-provider--doc] currently on how to *dance* with this package on the client side. The Good news is, here is a minimal "How to start" because **OAuth 2** is dramatically simpler than **OAuth 1**, so no more headache with signature, cryptography on client side, and other complex things. @@ -303,4 +303,6 @@ HTTP digest authentication is a widely implemented scheme that was intended to r [mod_wsgi_official]: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization [juanriaza]: https://github.com/juanriaza [djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth +[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider +[django-oauth2-provider--doc]: https://django-oauth2-provider.readthedocs.org/en/latest/ [rfc6749]: http://tools.ietf.org/html/rfc6749 From d4c2267187128c60927931c685a5c41a95c300bd Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 12:08:28 +0100 Subject: [PATCH 08/17] Clean up some print and comments --- rest_framework/authentication.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index c94af40514..c74078fcbf 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -175,7 +175,6 @@ def authenticate(self, request): http://tools.ietf.org/html/rfc6749#section-7.1 """ auth = request.META.get('HTTP_AUTHORIZATION', '').split() - print auth if not auth or auth[0].lower() != "bearer": return None @@ -202,11 +201,6 @@ def authenticate_credentials(self, request, access_token): raise exceptions.AuthenticationFailed("Invalid token") # does not exist or is expired # TODO check scope - # try: - # self.validate_token(request, consumer, token) - # except oauth2_provider.Error, e: - # print "got e" - # raise exceptions.AuthenticationFailed(e.message) if not self.check_active(token.user): raise exceptions.AuthenticationFailed('User not active: %s' % token.user.username) From 182edb3e9bd5ea9586c2a02befeac405fc1ee59c Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 21:25:42 +0100 Subject: [PATCH 09/17] Add django-oauth2-provider to .travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5f07241085..44786bf225 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ env: install: - pip install $DJANGO - pip install defusedxml==0.3 + - "pip install -e git+git://github.com/caffeinehit/django-oauth2-provider.git#egg=django-oauth2-provider" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install https://github.com/alex/django-filter/tarball/master; fi" - export PYTHONPATH=. From 721dc519ecdb8435bdeed6aa67d99be6968c0972 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 23:27:47 +0100 Subject: [PATCH 10/17] Use django.utils to import the unittest module for a cross python versions compatibility --- rest_framework/tests/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 1212f0aac7..0401ddd934 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User from django.http import HttpResponse from django.test import Client, TestCase +from django.utils import unittest from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions from rest_framework import permissions @@ -22,7 +23,6 @@ import json import base64 import datetime -import unittest factory = RequestFactory() From 8809c46ab5d2a09d5a956ccffcb2ae2db95c5c1b Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sat, 2 Mar 2013 20:16:18 +0100 Subject: [PATCH 11/17] Add new OAuth2 tests --- rest_framework/tests/authentication.py | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 0401ddd934..9d67a00504 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -273,6 +273,36 @@ def _create_authorization_header(self, token=None): def _client_credentials_params(self): return {'client_id': self.CLIENT_ID, 'client_secret': self.CLIENT_SECRET} + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_get_form_with_wrong_authorization_header_token_type_failing(self): + """Ensure that a wrong token type lead to the correct HTTP error status code""" + auth = "Wrong token-type-obsviously" + response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 401) + params = self._client_credentials_params() + response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 401) + + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_get_form_with_wrong_authorization_header_token_format_failing(self): + """Ensure that a wrong token format lead to the correct HTTP error status code""" + auth = "Bearer wrong token format" + response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 401) + params = self._client_credentials_params() + response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 401) + + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_get_form_with_wrong_authorization_header_token_failing(self): + """Ensure that a wrong token lead to the correct HTTP error status code""" + auth = "Bearer wrong-token" + response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 401) + params = self._client_credentials_params() + response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 401) + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') def test_get_form_with_wrong_client_data_failing_auth(self): """Ensure GETing form over OAuth with incorrect client credentials fails""" From c449dd4f4d8c9602c826e906870a87c13d6689de Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sat, 2 Mar 2013 20:17:14 +0100 Subject: [PATCH 12/17] Properly fail to wrong Authorization token type --- rest_framework/authentication.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index c74078fcbf..d4ba7967e6 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -176,7 +176,7 @@ def authenticate(self, request): """ auth = request.META.get('HTTP_AUTHORIZATION', '').split() if not auth or auth[0].lower() != "bearer": - return None + raise exceptions.AuthenticationFailed('Invalid Authorization token type') if len(auth) != 2: raise exceptions.AuthenticationFailed('Invalid token header') @@ -212,8 +212,6 @@ def authenticate_credentials(self, request, access_token): raise exceptions.AuthenticationFailed( 'You are not allowed to access this resource.') - return None - def authenticate_header(self, request): """ Bearer is the only finalized type currently From 6f5764105b2c361a85bc3933c943d98659bdbd68 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sat, 2 Mar 2013 20:18:50 +0100 Subject: [PATCH 13/17] Use the PyPI django-oauth2-provider version --- .travis.yml | 2 +- optionals.txt | 2 +- tox.ini | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 44786bf225..82127276be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ env: install: - pip install $DJANGO - pip install defusedxml==0.3 - - "pip install -e git+git://github.com/caffeinehit/django-oauth2-provider.git#egg=django-oauth2-provider" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install https://github.com/alex/django-filter/tarball/master; fi" - export PYTHONPATH=. diff --git a/optionals.txt b/optionals.txt index a36e9e9400..99868c6606 100644 --- a/optionals.txt +++ b/optionals.txt @@ -2,4 +2,4 @@ markdown>=2.1.0 PyYAML>=3.10 defusedxml>=0.3 django-filter>=0.5.4 --e git+git://github.com/caffeinehit/django-oauth2-provider.git@3198060acfe14730ce2d81310cbf2b13f6403438#egg=django_oauth2_provider-dev \ No newline at end of file +django-oauth2-provider>=0.2.3 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 58d308acae..ea68e2668e 100644 --- a/tox.ini +++ b/tox.ini @@ -21,33 +21,39 @@ deps = django==1.5 basepython = python2.7 deps = django==1.5 django-filter==0.5.4 + django-oauth2-provider==0.2.3 [testenv:py2.6-django1.5] basepython = python2.6 deps = django==1.5 django-filter==0.5.4 defusedxml==0.3 + django-oauth2-provider==0.2.3 [testenv:py2.7-django1.4] basepython = python2.7 deps = django==1.4.3 django-filter==0.5.4 defusedxml==0.3 + django-oauth2-provider==0.2.3 [testenv:py2.6-django1.4] basepython = python2.6 deps = django==1.4.3 django-filter==0.5.4 defusedxml==0.3 + django-oauth2-provider==0.2.3 [testenv:py2.7-django1.3] basepython = python2.7 deps = django==1.3.5 django-filter==0.5.4 defusedxml==0.3 + django-oauth2-provider==0.2.3 [testenv:py2.6-django1.3] basepython = python2.6 deps = django==1.3.5 django-filter==0.5.4 defusedxml==0.3 + django-oauth2-provider==0.2.3 From cda21a306e8fdb713d26726964be10e2862140c3 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sat, 2 Mar 2013 20:20:26 +0100 Subject: [PATCH 14/17] Only add the django-oauth2-provider apps if the module is installed otherwise log a warning --- rest_framework/runtests/settings.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 67dc7fff5f..b1501cf319 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -98,10 +98,18 @@ 'rest_framework', 'rest_framework.authtoken', 'rest_framework.tests', - 'provider', - 'provider.oauth2', ) +try: + import provider + INSTALLED_APPS += ( + 'provider', + 'provider.oauth2', + ) +except ImportError, inst: + import logging + logging.warning("django-oauth2-provider is not install, some tests will be skipped") + STATIC_URL = '/static/' PASSWORD_HASHERS = ( From 30e3775b8b209242141357bad0a69b6cc503c6f9 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sat, 2 Mar 2013 20:36:53 +0100 Subject: [PATCH 15/17] Update the documentation with a warning for incompatibility with Python 3 and taking @tomchristie advice into account on how to reformulate some sentences --- docs/api-guide/authentication.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index c73de1f671..b7251fd0d3 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -184,7 +184,13 @@ If you're using an AJAX style API with SessionAuthentication, you'll need to mak ## OAuth2Authentication -This authentication uses [OAuth 2.0][rfc6749] authentication scheme. It depends on optional [`django-oauth2-provider`][django-oauth2-provider]. In order to make it work you must install this package and add `provider` and `provider.oauth2` to your `INSTALLED_APPS` : +--- + +** Note:** This isn't available for Python 3, because the module [`django-oauth2-provider`][django-oauth2-provider] is not Python 3 ready. + +--- + +This authentication uses [OAuth 2.0][rfc6749] authentication scheme. It depends on the optional [`django-oauth2-provider`][django-oauth2-provider] project. In order to make it work you must install this package and add `provider` and `provider.oauth2` to your `INSTALLED_APPS` : INSTALLED_APPS = ( #(...) @@ -207,7 +213,7 @@ Finally, sync your database with those two new django apps. $ python manage.py syncdb $ python manage.py migrate -`OAuth2Authentication` class provides only token verification for requests. The *oauth 2 dance* is taken care by the [`django-oaut2-provider`][django-oauth2-provider] dependency. Unfortunately, there isn't a lot of [documentation][django-oauth2-provider--doc] currently on how to *dance* with this package on the client side. +`OAuth2Authentication` class provides only token verification for requests. The *oauth 2 dance* is taken care by the [`django-oaut2-provider`][django-oauth2-provider] dependency. The official [documentation][django-oauth2-provider--doc] is being [rewritten][django-oauth2-provider--rewritten-doc]. The Good news is, here is a minimal "How to start" because **OAuth 2** is dramatically simpler than **OAuth 1**, so no more headache with signature, cryptography on client side, and other complex things. @@ -228,13 +234,13 @@ Your client interface – I mean by that your iOS code, HTML code, or whatev --- -**Note:** Remember that you are **highly encourage** to use HTTPS for all your OAuth 2 requests. And by *highly encourage* I mean you SHOULD always use HTTPS otherwise you will expose user passwords for any person who can intercept the request (like a man in the middle attack). +**Note:** Remember that you should use HTTPS in production. --- You can use the command line to test that your local configuration is working : - $ curl -X POST -d "client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=password&username=YOUR_USERNAME&password=YOU_PASSWORD" http://localhost:8000/oauth2/access_token/ + $ curl -X POST -d "client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=password&username=YOUR_USERNAME&password=YOUR_PASSWORD" http://localhost:8000/oauth2/access_token/ Here is the response you should get : @@ -248,7 +254,7 @@ The command line to test the authentication looks like : $ curl -H "Authorization: Bearer " http://localhost:8000/api/?client_id=YOUR_CLIENT_ID\&client_secret=YOUR_CLIENT_SECRET -And hopefully, it will work like a charm. +And it will work like a charm. # Custom authentication @@ -305,4 +311,5 @@ HTTP digest authentication is a widely implemented scheme that was intended to r [djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth [django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider [django-oauth2-provider--doc]: https://django-oauth2-provider.readthedocs.org/en/latest/ +[django-oauth2-provider--rewritten-doc]: http://django-oauth2-provider-dulaccc.readthedocs.org/en/latest/ [rfc6749]: http://tools.ietf.org/html/rfc6749 From 8845c0be88bf68fa0e42d05c7196cd52d897623b Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sun, 3 Mar 2013 01:09:39 +0100 Subject: [PATCH 16/17] Fix import errors --- rest_framework/compat.py | 7 +++++++ rest_framework/runtests/settings.py | 2 +- rest_framework/tests/authentication.py | 8 ++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index e0a43f3f94..1e04e8f687 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -431,5 +431,12 @@ def apply_markdown(text): # OAuth 2 support is optional try: import provider.oauth2 as oauth2_provider + + # Hack to fix submodule import issues + submodules = ['backends', 'forms','managers','models','urls','views'] + for s in submodules: + mod = __import__('provider.oauth2.%s.*' % s) + setattr(oauth2_provider, s, mod) + except ImportError: oauth2_provider = None diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index b1501cf319..16ef1d2b6e 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -106,7 +106,7 @@ 'provider', 'provider.oauth2', ) -except ImportError, inst: +except ImportError: import logging logging.warning("django-oauth2-provider is not install, some tests will be skipped") diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 9d67a00504..a02aef5503 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -46,10 +46,14 @@ def put(self, request): (r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])), (r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])), (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'), - url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')), - url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])), ) +if oauth2_provider is not None: + urlpatterns += patterns('', + url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')), + url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])), + ) + class BasicAuthTests(TestCase): """Basic authentication""" From 5a56f92abf5f52ac153c4faa1b75af519c96a207 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sun, 3 Mar 2013 01:38:03 +0100 Subject: [PATCH 17/17] Update the package dependency url style in tox.ini --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index ea68e2668e..f3c19375f8 100644 --- a/tox.ini +++ b/tox.ini @@ -8,13 +8,13 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py3.3-django1.5] basepython = python3.3 deps = django==1.5 - https://github.com/alex/django-filter/archive/master.tar.gz + -egit+git://github.com/alex/django-filter.git#egg=django_filter defusedxml==0.3 [testenv:py3.2-django1.5] basepython = python3.2 deps = django==1.5 - https://github.com/alex/django-filter/archive/master.tar.gz + -egit+git://github.com/alex/django-filter.git#egg=django_filter defusedxml==0.3 [testenv:py2.7-django1.5]