From 1062d71f8be929f1f7e6910a8d573ac643082bae Mon Sep 17 00:00:00 2001 From: swistakm Date: Mon, 25 Feb 2013 16:56:14 +0100 Subject: add tests for OAuth authentication --- rest_framework/tests/authentication.py | 145 ++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index c9df1733..88b6fd16 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -2,15 +2,19 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.http import HttpResponse from django.test import Client, TestCase -from rest_framework import HTTP_HEADER_ENCODING +import time +from rest_framework import HTTP_HEADER_ENCODING, status from rest_framework import permissions from rest_framework.authtoken.models import Token -from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication +from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication, OAuthAuthentication from rest_framework.compat import patterns from rest_framework.views import APIView import json import base64 - +from oauth_provider.models import Consumer, Resource +from oauth_provider.models import Token as OAuthToken +from oauth_provider import consts as oauth_consts +import oauth2 as oauth class MockView(APIView): permission_classes = (permissions.IsAuthenticated,) @@ -21,11 +25,15 @@ class MockView(APIView): 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'), + (r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])) ) @@ -186,3 +194,134 @@ class TokenAuthTests(TestCase): {'username': self.username, 'password': self.password}) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key) + +class OAuthTests(TestCase): + """OAuth 1.0a 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.CONSUMER_KEY = 'consumer_key' + self.CONSUMER_SECRET = 'consumer_secret' + self.TOKEN_KEY = "token_key" + self.TOKEN_SECRET = "token_secret" + + self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, + name='example', user=self.user, status=oauth_consts.ACCEPTED) + + + self.resource = Resource.objects.create(name="resource name", url="api/") + self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource, + token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True + ) + + + def _create_authorization_header(self): + params = { + 'oauth_version': "1.0", + 'oauth_nonce': oauth.generate_nonce(), + 'oauth_timestamp': int(time.time()), + 'oauth_token': self.token.key, + 'oauth_consumer_key': self.consumer.key + } + + req = oauth.Request(method="GET", url="http://example.com", parameters=params) + + signature_method = oauth.SignatureMethod_PLAINTEXT() + req.sign_request(signature_method, self.consumer, self.token) + + return req.to_header()["Authorization"] + + def _create_authorization_url_parameters(self): + params = { + 'oauth_version': "1.0", + 'oauth_nonce': oauth.generate_nonce(), + 'oauth_timestamp': int(time.time()), + 'oauth_token': self.token.key, + 'oauth_consumer_key': self.consumer.key + } + + req = oauth.Request(method="GET", url="http://example.com", parameters=params) + + signature_method = oauth.SignatureMethod_PLAINTEXT() + req.sign_request(signature_method, self.consumer, self.token) + return dict(req) + + def test_post_form_passing_oauth(self): + """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF""" + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + + def test_post_form_repeated_nonce_failing_oauth(self): + """Ensure POSTing form over OAuth with repeated auth (same nonces and timestamp) credentials fails""" + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + + # simulate reply attack auth header containes already used (nonce, timestamp) pair + response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) + self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + + def test_post_form_token_removed_failing_oauth(self): + """Ensure POSTing when there is no OAuth access token in db fails""" + self.token.delete() + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) + self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + + def test_post_form_consumer_status_not_accepted_failing_oauth(self): + """Ensure POSTing when consumer status is anything other than ACCEPTED fails""" + for consumer_status in (oauth_consts.CANCELED, oauth_consts.PENDING, oauth_consts.REJECTED): + self.consumer.status = consumer_status + self.consumer.save() + + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) + self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + + def test_post_form_with_request_token_failing_oauth(self): + """Ensure POSTing with unauthorized request token instead of access token fails""" + self.token.token_type = OAuthToken.REQUEST + self.token.save() + + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) + self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + + def test_post_form_with_urlencoded_parameters(self): + """Ensure POSTing with x-www-form-urlencoded auth parameters passes""" + params = self._create_authorization_url_parameters() + response = self.csrf_client.post('/oauth/', params) + self.assertEqual(response.status_code, 200) + + def test_get_form_with_url_parameters(self): + """Ensure GETing with auth in url parameters passes""" + params = self._create_authorization_url_parameters() + response = self.csrf_client.get('/oauth/', params) + self.assertEqual(response.status_code, 200) + + def test_post_hmac_sha1_signature_passes(self): + """Ensure POSTing using HMAC_SHA1 signature method passes""" + params = { + 'oauth_version': "1.0", + 'oauth_nonce': oauth.generate_nonce(), + 'oauth_timestamp': int(time.time()), + 'oauth_token': self.token.key, + 'oauth_consumer_key': self.consumer.key + } + + req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params) + + signature_method = oauth.SignatureMethod_HMAC_SHA1() + req.sign_request(signature_method, self.consumer, self.token) + auth = req.to_header()["Authorization"] + + response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + -- cgit v1.2.3 From ced22db7cfe83d658283257bd898e7e11d125ad4 Mon Sep 17 00:00:00 2001 From: swistakm Date: Mon, 25 Feb 2013 16:57:45 +0100 Subject: add django-oauth-plus & oauth2 to installed apps in runtests settings.py --- 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 03bfc216..683669ce 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -97,7 +97,9 @@ INSTALLED_APPS = ( # 'django.contrib.admindocs', 'rest_framework', 'rest_framework.authtoken', - 'rest_framework.tests' + 'rest_framework.tests', + 'oauth_provider', + ) STATIC_URL = '/static/' -- cgit v1.2.3 From 1aed9c1604be3db8f3f2d2de748eb6f7c574637a Mon Sep 17 00:00:00 2001 From: swistakm Date: Mon, 25 Feb 2013 16:58:16 +0100 Subject: add OAuthAuthentication class --- rest_framework/authentication.py | 119 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 14b2136b..2bd0767e 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -9,6 +9,18 @@ from rest_framework.compat import CsrfViewMiddleware from rest_framework.authtoken.models import Token import base64 +from django.core.exceptions import ImproperlyConfigured +try: + import oauth2 +except ImportError: + oauth2 = None + +try: + import oauth_provider + from oauth_provider.store import store +except ImportError: + oauth_provider = None + class BaseAuthentication(object): """ @@ -155,4 +167,109 @@ class TokenAuthentication(BaseAuthentication): return 'Token' -# TODO: OAuthAuthentication +class OAuthAuthentication(BaseAuthentication): + """rest_framework OAuth authentication backend using + django-oath-plus""" + www_authenticate_realm = 'api' + require_active = True + + def __init__(self, **kwargs): + super(OAuthAuthentication, self).__init__(**kwargs) + + if oauth2 is None: + raise ImproperlyConfigured("The 'python-oauth2' package could not be imported. It is required for use with the 'OAuthAuthentication' class.") + + if oauth_provider is None: + raise ImproperlyConfigured("The 'django-oauth-plus' package could not be imported. It is required for use with the 'OAuthAuthentication' class.") + + + def authenticate(self, request): + """ + :returns: two-tuple of (user, auth) if authentication succeeds, or None otherwise. + """ + from oauth_provider.store import store + if self.is_valid_request(request): + oauth_request = oauth_provider.utils.get_oauth_request(request) + + if not self.check_nonce(request, oauth_request): + raise exceptions.AuthenticationFailed("Nonce check failed") + + try: + consumer = store.get_consumer(request, oauth_request, + oauth_request.get_parameter('oauth_consumer_key')) + except oauth_provider.store.InvalidConsumerError, e: + raise exceptions.AuthenticationFailed(e) + + if consumer.status != oauth_provider.consts.ACCEPTED: + raise exceptions.AuthenticationFailed('Invalid consumer key status: %s' % consumer.get_status_display()) + + try: + token = store.get_access_token(request, oauth_request, + consumer, oauth_request.get_parameter('oauth_token')) + + except oauth_provider.store.InvalidTokenError: + raise exceptions.AuthenticationFailed( + 'Invalid access token: %s' % oauth_request.get_parameter('oauth_token')) + + 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 consumer 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): + return 'OAuth realm="%s"' % self.www_authenticate_realm + + def is_in(self, params): + """ + Checks to ensure that all the OAuth parameter names are in the + provided ``params``. + """ + from oauth_provider.consts import OAUTH_PARAMETERS_NAMES + + for param_name in OAUTH_PARAMETERS_NAMES: + if param_name not in params: + return False + + return True + + def is_valid_request(self, request): + """ + Checks whether the required parameters are either in the HTTP + ``Authorization`` header sent by some clients (the preferred method + according to OAuth spec) or fall back to ``GET/POST``. + """ + auth_params = request.META.get("HTTP_AUTHORIZATION", []) + return self.is_in(auth_params) or self.is_in(request.REQUEST) + + def validate_token(self, request, consumer, token): + oauth_server, oauth_request = oauth_provider.utils.initialize_server_request(request) + return oauth_server.verify_request(oauth_request, consumer, token) + + 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 + + def check_nonce(self, request, oauth_request): + """Checks nonce of request""" + return store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) -- cgit v1.2.3 From e2b11a29d129816c1d05cdd90838f64f07a12bb5 Mon Sep 17 00:00:00 2001 From: swistakm Date: Mon, 25 Feb 2013 17:00:09 +0100 Subject: add django-oauth-plus & oauth2 to .travis.yml --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3787e517..046b6d9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,8 @@ env: install: - pip install $DJANGO - pip install defusedxml==0.3 + - pip install oauth2 + - pip install django-oauth-plus - "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=. -- cgit v1.2.3 From cfce4555d7903720806d8acc9febc3b9fc3ed0a6 Mon Sep 17 00:00:00 2001 From: swistakm Date: Mon, 25 Feb 2013 17:02:13 +0100 Subject: add django-oauth-plus & oauth2 to optionals.txt --- optionals.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/optionals.txt b/optionals.txt index 3d98cc0e..d0bc6a70 100644 --- a/optionals.txt +++ b/optionals.txt @@ -2,3 +2,5 @@ markdown>=2.1.0 PyYAML>=3.10 defusedxml>=0.3 django-filter>=0.5.4 +django-oauth-plus>=2.0 +oauth2>=1.5.211 -- cgit v1.2.3 From 5d9ed34e4115f683294082faf9c9584a53bb7e49 Mon Sep 17 00:00:00 2001 From: swistakm Date: Mon, 25 Feb 2013 17:41:34 +0100 Subject: add OAuthAuthentication documentation stub --- docs/api-guide/authentication.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 8c1d1185..81b35909 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -182,6 +182,20 @@ 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. +## OAuthAuthentication + +This authentication uses [OAuth 1.0](http://tools.ietf.org/html/rfc5849) authentication scheme. It depends on optional `django-oauth-plus` and `oauth2` packages. In order to make it work you must istall these packages and add `oauth_provider` (from `django-oauth-plus`) to your `INSTALLED_APPS`: + + INSTALLED_APPS = ( + #(...) + `oauth_provider`, + ) + +OAuthAuthentication class provides only token verification and signature validation for requests. It doesn't provide authorization flow for your clients. You still need to implement your own views for accessing and authorizing Reqest/Access Tokens. This is because there are many different OAuth flows in use. Almost always they require end-user interaction, and most likely this is what you want to design yourself. + +Luckily `django-oauth-plus` provides simple foundation for classic 'three-legged' oauth flow, so if it is what you need please refer to [its documentation](http://code.larlet.fr/django-oauth-plus/wiki/Home). This documentation will provide you also information about how to work with supplied models and change basic settings. + + # 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. -- cgit v1.2.3 From 59a6f5f463472656518cb0680b9da5f22a724882 Mon Sep 17 00:00:00 2001 From: swistakm Date: Tue, 26 Feb 2013 11:22:21 +0100 Subject: Move oauth2 and django-oauth-plus imports to compat and fix some minor issues - alias oauth2 as oauth - remove rouge print - remove docstring markups - OAuthAuthentication.authenticate() now returns (user, token) two-tuple on success - don't set request.user because it's already set --- rest_framework/authentication.py | 31 ++++++++++--------------------- rest_framework/compat.py | 12 ++++++++++++ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 2bd0767e..b507c5e1 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -4,23 +4,14 @@ Provides a set of pluggable authentication policies. from __future__ import unicode_literals from django.contrib.auth import authenticate from django.utils.encoding import DjangoUnicodeDecodeError +from django.core.exceptions import ImproperlyConfigured from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import CsrfViewMiddleware +from rest_framework.compat import oauth +from rest_framework.compat import oauth_provider from rest_framework.authtoken.models import Token import base64 -from django.core.exceptions import ImproperlyConfigured -try: - import oauth2 -except ImportError: - oauth2 = None - -try: - import oauth_provider - from oauth_provider.store import store -except ImportError: - oauth_provider = None - class BaseAuthentication(object): """ @@ -169,15 +160,15 @@ class TokenAuthentication(BaseAuthentication): class OAuthAuthentication(BaseAuthentication): """rest_framework OAuth authentication backend using - django-oath-plus""" + django-oath-plus and oauth2""" www_authenticate_realm = 'api' require_active = True def __init__(self, **kwargs): super(OAuthAuthentication, self).__init__(**kwargs) - if oauth2 is None: - raise ImproperlyConfigured("The 'python-oauth2' package could not be imported. It is required for use with the 'OAuthAuthentication' class.") + if oauth is None: + raise ImproperlyConfigured("The 'oauth2' package could not be imported. It is required for use with the 'OAuthAuthentication' class.") if oauth_provider is None: raise ImproperlyConfigured("The 'django-oauth-plus' package could not be imported. It is required for use with the 'OAuthAuthentication' class.") @@ -185,7 +176,7 @@ class OAuthAuthentication(BaseAuthentication): def authenticate(self, request): """ - :returns: two-tuple of (user, auth) if authentication succeeds, or None otherwise. + Returns two-tuple of (user, auth token) if authentication succeeds, or None otherwise. """ from oauth_provider.store import store if self.is_valid_request(request): @@ -213,16 +204,14 @@ class OAuthAuthentication(BaseAuthentication): try: self.validate_token(request, consumer, token) - except oauth2.Error, e: - print "got e" + except oauth.Error, e: raise exceptions.AuthenticationFailed(e.message) if not self.check_active(token.user): raise exceptions.AuthenticationFailed('User not active: %s' % token.user.username) if consumer and token: - request.user = token.user - return (request.user, None) + return (token.user, token) raise exceptions.AuthenticationFailed( 'You are not allowed to access this resource.') @@ -272,4 +261,4 @@ class OAuthAuthentication(BaseAuthentication): def check_nonce(self, request, oauth_request): """Checks nonce of request""" - return store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) + return oauth_provider.store.store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 07fdddce..e4bad0cb 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -426,3 +426,15 @@ try: import defusedxml.ElementTree as etree except ImportError: etree = None + +# OAuth is optional +try: + import oauth2 as oauth +except ImportError: + oauth = None + +# OAuth is optional +try: + import oauth_provider +except ImportError: + oauth_provider = None \ No newline at end of file -- cgit v1.2.3 From d84c2cf2752467c835200a54601f77ad1cee6b38 Mon Sep 17 00:00:00 2001 From: swistakm Date: Tue, 26 Feb 2013 11:25:27 +0100 Subject: OAuth tests now are skipped unless django-oauth-plus and oauth2 are installed. --- rest_framework/tests/authentication.py | 37 +++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 88b6fd16..d1c978fc 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -9,12 +9,11 @@ from rest_framework.authtoken.models import Token from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication, OAuthAuthentication from rest_framework.compat import patterns from rest_framework.views import APIView +from rest_framework.compat import oauth +from rest_framework.compat import oauth_provider import json import base64 -from oauth_provider.models import Consumer, Resource -from oauth_provider.models import Token as OAuthToken -from oauth_provider import consts as oauth_consts -import oauth2 as oauth +import unittest class MockView(APIView): permission_classes = (permissions.IsAuthenticated,) @@ -200,6 +199,14 @@ class OAuthTests(TestCase): urls = 'rest_framework.tests.authentication' def setUp(self): + # these imports are here because oauth is optional and hiding them in try..except block or compat + # could obscure problems if something breaks + from oauth_provider.models import Consumer, Resource + from oauth_provider.models import Token as OAuthToken + from oauth_provider import consts + + self.consts = consts + self.csrf_client = Client(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' @@ -212,7 +219,7 @@ class OAuthTests(TestCase): self.TOKEN_SECRET = "token_secret" self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, - name='example', user=self.user, status=oauth_consts.ACCEPTED) + name='example', user=self.user, status=self.consts.ACCEPTED) self.resource = Resource.objects.create(name="resource name", url="api/") @@ -252,12 +259,16 @@ class OAuthTests(TestCase): req.sign_request(signature_method, self.consumer, self.token) return dict(req) + @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') + @unittest.skipUnless(oauth, 'oauth2 not installed') def test_post_form_passing_oauth(self): """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF""" auth = self._create_authorization_header() response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) + @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') + @unittest.skipUnless(oauth, 'oauth2 not installed') def test_post_form_repeated_nonce_failing_oauth(self): """Ensure POSTing form over OAuth with repeated auth (same nonces and timestamp) credentials fails""" auth = self._create_authorization_header() @@ -268,6 +279,8 @@ class OAuthTests(TestCase): response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') + @unittest.skipUnless(oauth, 'oauth2 not installed') def test_post_form_token_removed_failing_oauth(self): """Ensure POSTing when there is no OAuth access token in db fails""" self.token.delete() @@ -275,9 +288,11 @@ class OAuthTests(TestCase): response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') + @unittest.skipUnless(oauth, 'oauth2 not installed') def test_post_form_consumer_status_not_accepted_failing_oauth(self): """Ensure POSTing when consumer status is anything other than ACCEPTED fails""" - for consumer_status in (oauth_consts.CANCELED, oauth_consts.PENDING, oauth_consts.REJECTED): + for consumer_status in (self.consts.CANCELED, self.consts.PENDING, self.consts.REJECTED): self.consumer.status = consumer_status self.consumer.save() @@ -285,27 +300,35 @@ class OAuthTests(TestCase): response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') + @unittest.skipUnless(oauth, 'oauth2 not installed') def test_post_form_with_request_token_failing_oauth(self): """Ensure POSTing with unauthorized request token instead of access token fails""" - self.token.token_type = OAuthToken.REQUEST + self.token.token_type = self.token.REQUEST self.token.save() auth = self._create_authorization_header() response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') + @unittest.skipUnless(oauth, 'oauth2 not installed') def test_post_form_with_urlencoded_parameters(self): """Ensure POSTing with x-www-form-urlencoded auth parameters passes""" params = self._create_authorization_url_parameters() response = self.csrf_client.post('/oauth/', params) self.assertEqual(response.status_code, 200) + @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') + @unittest.skipUnless(oauth, 'oauth2 not installed') def test_get_form_with_url_parameters(self): """Ensure GETing with auth in url parameters passes""" params = self._create_authorization_url_parameters() response = self.csrf_client.get('/oauth/', params) self.assertEqual(response.status_code, 200) + @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') + @unittest.skipUnless(oauth, 'oauth2 not installed') def test_post_hmac_sha1_signature_passes(self): """Ensure POSTing using HMAC_SHA1 signature method passes""" params = { -- cgit v1.2.3 From a4304458f5a07acc400b7630a59a4a0996e166d9 Mon Sep 17 00:00:00 2001 From: swistakm Date: Tue, 26 Feb 2013 11:27:06 +0100 Subject: runtest.settings fixed if django-oauth-plus or oauth2 are not installed oauth_provider can be added to INSTALLED_APPS only if these packages are installed --- 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 683669ce..eb3f1115 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -98,10 +98,18 @@ INSTALLED_APPS = ( 'rest_framework', 'rest_framework.authtoken', 'rest_framework.tests', - 'oauth_provider', - ) +# OAuth is optional and won't work if there is no oauth_provider & oauth2 +try: + import oauth_provider + import oauth2 +except ImportError: + pass +else: + INSTALLED_APPS += ('oauth_provider',) + + STATIC_URL = '/static/' PASSWORD_HASHERS = ( -- cgit v1.2.3 From dd355d511c2e1bf4b26be80955a4f53b20bfed1b Mon Sep 17 00:00:00 2001 From: swistakm Date: Wed, 27 Feb 2013 10:56:04 +0100 Subject: oauth2 & django-oauth-plus installed only on 2.x --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 046b6d9c..52ed562c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,8 @@ env: install: - pip install $DJANGO - pip install defusedxml==0.3 - - pip install oauth2 - - pip install django-oauth-plus + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus; 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=. -- cgit v1.2.3 From 55ea5b9460842f6b8aefa67575a6d25b2d479593 Mon Sep 17 00:00:00 2001 From: swistakm Date: Wed, 27 Feb 2013 10:58:13 +0100 Subject: import compat version of unittest --- rest_framework/tests/authentication.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index d1c978fc..8ef9d3ff 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.http import HttpResponse from django.test import Client, TestCase +from django.utils import unittest import time from rest_framework import HTTP_HEADER_ENCODING, status from rest_framework import permissions @@ -13,7 +14,7 @@ from rest_framework.compat import oauth from rest_framework.compat import oauth_provider import json import base64 -import unittest + class MockView(APIView): permission_classes = (permissions.IsAuthenticated,) -- cgit v1.2.3 From 2eabc5c2b46d9f4cc7a467af849ff31397b9d7bf Mon Sep 17 00:00:00 2001 From: swistakm Date: Wed, 27 Feb 2013 11:02:50 +0100 Subject: rfc5849 link with anchor --- docs/api-guide/authentication.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 81b35909..d7918e10 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 ## OAuthAuthentication -This authentication uses [OAuth 1.0](http://tools.ietf.org/html/rfc5849) authentication scheme. It depends on optional `django-oauth-plus` and `oauth2` packages. In order to make it work you must istall these packages and add `oauth_provider` (from `django-oauth-plus`) to your `INSTALLED_APPS`: +This authentication uses [OAuth 1.0][rfc5849] authentication scheme. It depends on optional `django-oauth-plus` and `oauth2` packages. In order to make it work you must istall these packages and add `oauth_provider` (from `django-oauth-plus`) to your `INSTALLED_APPS`: INSTALLED_APPS = ( #(...) @@ -249,3 +249,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 +[rfc5849] : http://tools.ietf.org/html/rfc5849 \ No newline at end of file -- cgit v1.2.3