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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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 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(-) (limited to 'rest_framework') 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 468b5e43e2582513c4ae862efa4511ea8313031e Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 02:06:20 +0100 Subject: Add tests for OAuth2 authentication --- rest_framework/tests/authentication.py | 110 ++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 2a2bfba9..3ceab808 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 HTTP_HEADER_ENCODING 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 @@ 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'), + url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')), + url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])), ) @@ -187,3 +197,99 @@ class TokenAuthTests(TestCase): {'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) -- cgit v1.2.3 From 4e1f77db1ae48b156cb163a0853776cc23e230b5 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 02:07:53 +0100 Subject: Add django-oauth2-provider to the installed apps --- rest_framework/runtests/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 03bfc216..67dc7fff 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', + 'provider', + 'provider.oauth2', ) STATIC_URL = '/static/' -- cgit v1.2.3 From da9d7fb8ec19f289d9d2777738a45007c41a1289 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 02:08:58 +0100 Subject: Add the OAuth2Authentication class --- rest_framework/authentication.py | 85 +++++++++++++++++++++++++++++++++++++++- rest_framework/compat.py | 11 ++++++ 2 files changed, 95 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 14b2136b..c20d9cb5 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -6,6 +6,7 @@ from django.contrib.auth import authenticate 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 @@ class TokenAuthentication(BaseAuthentication): 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 07fdddce..5bba0c86 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -426,3 +426,14 @@ try: 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 -- cgit v1.2.3 From 9d5c3060386cc8deb4ee55eda022f0a134e897c0 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 11:53:30 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index c20d9cb5..c94af405 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -6,7 +6,7 @@ from django.contrib.auth import authenticate 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 @@ class OAuth2Authentication(BaseAuthentication): """ # 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 @@ class OAuth2Authentication(BaseAuthentication): # 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 5bba0c86..e0a43f3f 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -430,10 +430,6 @@ except ImportError: # 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 c2c23bcc..1212f0aa 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -16,7 +16,6 @@ from rest_framework.authentication import ( 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 @@ class OAuth2Tests(TestCase): 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 @@ class OAuth2Tests(TestCase): 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 @@ class OAuth2Tests(TestCase): 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 @@ class OAuth2Tests(TestCase): 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 @@ class OAuth2Tests(TestCase): 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 @@ class OAuth2Tests(TestCase): 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 @@ class OAuth2Tests(TestCase): 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 @@ class OAuth2Tests(TestCase): 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 -- cgit v1.2.3 From d4c2267187128c60927931c685a5c41a95c300bd Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 12:08:28 +0100 Subject: Clean up some print and comments --- rest_framework/authentication.py | 6 ------ 1 file changed, 6 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index c94af405..c74078fc 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -175,7 +175,6 @@ class OAuth2Authentication(BaseAuthentication): 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 @@ class OAuth2Authentication(BaseAuthentication): 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) -- cgit v1.2.3 From 721dc519ecdb8435bdeed6aa67d99be6968c0972 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 1 Mar 2013 23:27:47 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 1212f0aa..0401ddd9 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse 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 @@ from rest_framework.views import APIView import json import base64 import datetime -import unittest factory = RequestFactory() -- cgit v1.2.3 From 8809c46ab5d2a09d5a956ccffcb2ae2db95c5c1b Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sat, 2 Mar 2013 20:16:18 +0100 Subject: Add new OAuth2 tests --- rest_framework/tests/authentication.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 0401ddd9..9d67a005 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -273,6 +273,36 @@ class OAuth2Tests(TestCase): 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""" -- cgit v1.2.3 From c449dd4f4d8c9602c826e906870a87c13d6689de Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sat, 2 Mar 2013 20:17:14 +0100 Subject: Properly fail to wrong Authorization token type --- rest_framework/authentication.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index c74078fc..d4ba7967 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -176,7 +176,7 @@ class OAuth2Authentication(BaseAuthentication): """ 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 @@ class OAuth2Authentication(BaseAuthentication): 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 -- cgit v1.2.3 From cda21a306e8fdb713d26726964be10e2862140c3 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sat, 2 Mar 2013 20:20:26 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 67dc7fff..b1501cf3 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', - '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 = ( -- cgit v1.2.3 From 8845c0be88bf68fa0e42d05c7196cd52d897623b Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sun, 3 Mar 2013 01:09:39 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/compat.py b/rest_framework/compat.py index e0a43f3f..1e04e8f6 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -431,5 +431,12 @@ except ImportError: # 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 b1501cf3..16ef1d2b 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -106,7 +106,7 @@ try: '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 9d67a005..a02aef55 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -46,10 +46,14 @@ urlpatterns = patterns('', (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""" -- cgit v1.2.3 From 44930f30915298cda8c1474ed9ec4415258c3e6f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 7 Mar 2013 09:15:05 +0000 Subject: Fix Py3k syntax errors --- rest_framework/authentication.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 24a8e336..460c1e53 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -185,7 +185,7 @@ class OAuthAuthentication(BaseAuthentication): try: consumer_key = oauth_request.get_parameter('oauth_consumer_key') consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key) - except oauth_provider_store.InvalidConsumerError, err: + except oauth_provider_store.InvalidConsumerError as err: raise exceptions.AuthenticationFailed(err) if consumer.status != oauth_provider.consts.ACCEPTED: @@ -201,8 +201,8 @@ class OAuthAuthentication(BaseAuthentication): try: self.validate_token(request, consumer, token) - except oauth.Error, e: - raise exceptions.AuthenticationFailed(e.message) + except oauth.Error as err: + raise exceptions.AuthenticationFailed(err.message) user = token.user -- cgit v1.2.3 From 1d62594fa9ed87545a312681f999bbfa0237491b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 7 Mar 2013 15:44:36 +0000 Subject: Clean ups. --- rest_framework/authentication.py | 63 ++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 28 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 460c1e53..8ee3a900 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -157,6 +157,7 @@ class OAuthAuthentication(BaseAuthentication): OAuth 1.0a authentication backend using `django-oauth-plus` and `oauth2`. Note: The `oauth2` package actually provides oauth1.0a support. Urg. + We import it from the `compat` module as `oauth`. """ www_authenticate_realm = 'api' @@ -164,23 +165,42 @@ class OAuthAuthentication(BaseAuthentication): super(OAuthAuthentication, self).__init__(**kwargs) if oauth is None: - raise ImproperlyConfigured("The 'oauth2' package could not be imported. It is required for use with the 'OAuthAuthentication' class.") + 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.") + 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, token) if authentication succeeds, or None otherwise. """ - if not self.is_valid_request(request): + try: + oauth_request = oauth_provider.utils.get_oauth_request(request) + except oauth.Error as err: + raise exceptions.AuthenticationFailed(err.message) + + oauth_params = oauth_provider.consts.OAUTH_PARAMETERS_NAMES + + found = any(param for param in oauth_params if param in oauth_request) + missing = list(param for param in oauth_params if param not in oauth_request) + + if not found: + # OAuth authentication was not attempted. return None - oauth_request = oauth_provider.utils.get_oauth_request(request) + if missing: + # OAuth was attempted but missing parameters. + msg = 'Missing parameters: %s' % (', '.join(missing)) + raise exceptions.AuthenticationFailed(msg) if not self.check_nonce(request, oauth_request): - raise exceptions.AuthenticationFailed("Nonce check failed") + msg = 'Nonce check failed' + raise exceptions.AuthenticationFailed(msg) try: consumer_key = oauth_request.get_parameter('oauth_consumer_key') @@ -207,40 +227,27 @@ class OAuthAuthentication(BaseAuthentication): user = token.user if not user.is_active: - raise exceptions.AuthenticationFailed('User inactive or deleted: %s' % user.username) + msg = 'User inactive or deleted: %s' % user.username + raise exceptions.AuthenticationFailed(msg) return (token.user, token) 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``. + If permission is denied, return a '401 Unauthorized' response, + with an appropraite 'WWW-Authenticate' header. """ - for param_name in oauth_provider.consts.OAUTH_PARAMETERS_NAMES: - if param_name not in params: - return False - - return True + return 'OAuth realm="%s"' % self.www_authenticate_realm - def is_valid_request(self, request): + def validate_token(self, request, consumer, token): """ - 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`. + Check the token and raise an `oauth.Error` exception if invalid. """ - 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) + oauth_server.verify_request(oauth_request, consumer, token) def check_nonce(self, request, oauth_request): """ - Checks nonce of request. + Checks nonce of request, and return True if valid. """ - return oauth_provider.store.store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) + return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) -- cgit v1.2.3 From 650d8e6a8ecd092e1bdd7269097044563f4ea449 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 8 Mar 2013 20:23:11 +0000 Subject: More bits of cleanup --- rest_framework/authentication.py | 125 +++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 59 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 4d6e3375..3000de3a 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -12,6 +12,19 @@ from rest_framework.authtoken.models import Token import base64 +def get_authorization_header(request): + """ + Return request's 'Authorization:' header, as a bytestring. + + Hide some test client ickyness where the header can be unicode. + """ + auth = request.META.get('HTTP_AUTHORIZATION', b'') + if type(auth) == type(''): + # Work around django test client oddness + auth = auth.encode(HTTP_HEADER_ENCODING) + return auth + + class BaseAuthentication(object): """ All authentication classes should extend BaseAuthentication. @@ -43,22 +56,22 @@ class BasicAuthentication(BaseAuthentication): Returns a `User` if a correct username and password have been supplied using HTTP Basic authentication. Otherwise returns `None`. """ - auth = request.META.get('HTTP_AUTHORIZATION', b'') - if type(auth) == type(''): - # Work around django test client oddness - auth = auth.encode(HTTP_HEADER_ENCODING) - auth = auth.split() + auth = get_authorization_header(request).split() if not auth or auth[0].lower() != b'basic': return None - if len(auth) != 2: - raise exceptions.AuthenticationFailed('Invalid basic header') + if len(auth) == 1: + msg = 'Invalid basic header. No credentials provided.' + if len(auth) > 2: + msg = 'Invalid basic header. Credentials string should not contain spaces.' + raise exceptions.AuthenticationFailed(msg) try: auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':') except (TypeError, UnicodeDecodeError): - raise exceptions.AuthenticationFailed('Invalid basic header') + msg = 'Invalid basic header. Credentials not correctly base64 encoded' + raise exceptions.AuthenticationFailed(msg) userid, password = auth_parts[0], auth_parts[2] return self.authenticate_credentials(userid, password) @@ -68,9 +81,9 @@ class BasicAuthentication(BaseAuthentication): Authenticate the userid and password against username and password. """ user = authenticate(username=userid, password=password) - if user is not None and user.is_active: - return (user, None) - raise exceptions.AuthenticationFailed('Invalid username/password') + if user is None or not user.is_active: + raise exceptions.AuthenticationFailed('Invalid username/password') + return (user, None) def authenticate_header(self, request): return 'Basic realm="%s"' % self.www_authenticate_realm @@ -129,13 +142,16 @@ class TokenAuthentication(BaseAuthentication): """ def authenticate(self, request): - auth = request.META.get('HTTP_AUTHORIZATION', '').split() + auth = get_authorization_header(request).split() if not auth or auth[0].lower() != "token": return None - if len(auth) != 2: - raise exceptions.AuthenticationFailed('Invalid token header') + if len(auth) == 1: + msg = 'Invalid token header. No credentials provided.' + if len(auth) > 2: + msg = 'Invalid token header. Token string should not contain spaces.' + raise exceptions.AuthenticationFailed(msg) return self.authenticate_credentials(auth[1]) @@ -145,9 +161,10 @@ class TokenAuthentication(BaseAuthentication): except self.model.DoesNotExist: raise exceptions.AuthenticationFailed('Invalid token') - if token.user.is_active: - return (token.user, token) - raise exceptions.AuthenticationFailed('User inactive or deleted') + if not token.user.is_active: + raise exceptions.AuthenticationFailed('User inactive or deleted') + + return (token.user, token) def authenticate_header(self, request): return 'Token' @@ -162,8 +179,8 @@ class OAuthAuthentication(BaseAuthentication): """ www_authenticate_realm = 'api' - def __init__(self, **kwargs): - super(OAuthAuthentication, self).__init__(**kwargs) + def __init__(self, *args, **kwargs): + super(OAuthAuthentication, self).__init__(*args, **kwargs) if oauth is None: raise ImproperlyConfigured( @@ -258,57 +275,59 @@ class OAuth2Authentication(BaseAuthentication): """ OAuth 2 authentication backend using `django-oauth2-provider` """ - require_active = True + www_authenticate_realm = 'api' + + def __init__(self, *args, **kwargs): + super(OAuth2Authentication, self).__init__(*args, **kwargs) - 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.") + 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 + Returns two-tuple of (user, token) if authentication succeeds, + or None otherwise. """ - auth = request.META.get('HTTP_AUTHORIZATION', '').split() - if not auth or auth[0].lower() != "bearer": - raise exceptions.AuthenticationFailed('Invalid Authorization token type') - if len(auth) != 2: - raise exceptions.AuthenticationFailed('Invalid token header') + auth = get_authorization_header(request).split() + + if not auth or auth[0].lower() != 'bearer': + return None + + if len(auth) == 1: + msg = 'Invalid bearer header. No credentials provided.' + if len(auth) > 2: + msg = 'Invalid bearer header. Token string should not contain spaces.' + raise exceptions.AuthenticationFailed(msg) 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 request, given the access token. """ - # authenticate the client + # Authenticate the client oauth2_client_form = oauth2_provider_forms.ClientAuthForm(request.REQUEST) if not oauth2_client_form.is_valid(): - raise exceptions.AuthenticationFailed("Client could not be validated") + raise exceptions.AuthenticationFailed('Client could not be validated') client = oauth2_client_form.cleaned_data.get('client') - # retrieve the `oauth2_provider.models.OAuth2AccessToken` instance from the access_token + # Retrieve the `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 - - # TODO check scope + raise exceptions.AuthenticationFailed('Invalid token') - if not self.check_active(token.user): - raise exceptions.AuthenticationFailed('User not active: %s' % token.user.username) + user = token.user - if client and token: - request.user = token.user - return (request.user, None) + if not user.is_active: + msg = 'User inactive or deleted: %s' % user.username + raise exceptions.AuthenticationFailed(msg) - raise exceptions.AuthenticationFailed( - 'You are not allowed to access this resource.') + return (token.user, token) def authenticate_header(self, request): """ @@ -316,16 +335,4 @@ class OAuth2Authentication(BaseAuthentication): 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 + return 'Bearer realm="%s"' % self.www_authenticate_realm -- cgit v1.2.3 From 2596c12a21003d230beb101aa93ddf83a1995305 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 8 Mar 2013 22:56:24 +0000 Subject: Fixes for auth header checking. --- rest_framework/authentication.py | 13 ++++++++----- rest_framework/tests/authentication.py | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 3000de3a..b4b73699 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -63,7 +63,8 @@ class BasicAuthentication(BaseAuthentication): if len(auth) == 1: msg = 'Invalid basic header. No credentials provided.' - if len(auth) > 2: + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: msg = 'Invalid basic header. Credentials string should not contain spaces.' raise exceptions.AuthenticationFailed(msg) @@ -144,12 +145,13 @@ class TokenAuthentication(BaseAuthentication): def authenticate(self, request): auth = get_authorization_header(request).split() - if not auth or auth[0].lower() != "token": + if not auth or auth[0].lower() != b'token': return None if len(auth) == 1: msg = 'Invalid token header. No credentials provided.' - if len(auth) > 2: + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: msg = 'Invalid token header. Token string should not contain spaces.' raise exceptions.AuthenticationFailed(msg) @@ -293,12 +295,13 @@ class OAuth2Authentication(BaseAuthentication): auth = get_authorization_header(request).split() - if not auth or auth[0].lower() != 'bearer': + if not auth or auth[0].lower() != b'bearer': return None if len(auth) == 1: msg = 'Invalid bearer header. No credentials provided.' - if len(auth) > 2: + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: msg = 'Invalid bearer header. Token string should not contain spaces.' raise exceptions.AuthenticationFailed(msg) diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index ddd61b63..9e86881a 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -159,7 +159,7 @@ class TokenAuthTests(TestCase): def test_post_form_passing_token_auth(self): """Ensure POSTing json over token auth with correct credentials passes and does not require CSRF""" - auth = "Token " + self.key + auth = 'Token ' + self.key response = self.csrf_client.post('/token/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_200_OK) -- cgit v1.2.3 From fd9d6c664be1273f4d2f4d56361345e630ce8b7b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 8 Mar 2013 23:08:55 +0000 Subject: Fix crazy typo. --- rest_framework/tests/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 54ab0c94..9e86881a 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -145,7 +145,7 @@ class SessionAuthTests(TestCase): class TokenAuthTests(TestCase): """Token authentication""" - urls = 'ยง.tests.authentication' + urls = 'rest_framework.tests.authentication' def setUp(self): self.csrf_client = Client(enforce_csrf_checks=True) -- cgit v1.2.3 From e03906a5c4101853b709403266b738911680c4b5 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sun, 10 Mar 2013 14:08:29 +0100 Subject: Add TokenHasReadWriteScope class for permissions based on scopes --- rest_framework/compat.py | 2 ++ rest_framework/permissions.py | 28 ++++++++++++++++++++++++++++ rest_framework/tests/authentication.py | 28 +++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 69be9543..e9570a08 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -453,9 +453,11 @@ try: from provider.oauth2 import backends as oauth2_provider_backends from provider.oauth2 import models as oauth2_provider_models from provider.oauth2 import forms as oauth2_provider_forms + from provider import scope as oauth2_provider_scope except ImportError: oauth2_provider = None oauth2_provider_backends = None oauth2_provider_models = None oauth2_provider_forms = None + oauth2_provider_scope = None diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 306f00ca..519a3691 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -7,6 +7,8 @@ import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] +from rest_framework.compat import oauth2_provider_scope + class BasePermission(object): """ @@ -125,3 +127,29 @@ class DjangoModelPermissions(BasePermission): request.user.has_perms(perms)): return True return False + + +class TokenHasReadWriteScope(BasePermission): + """ + The request is authenticated as a user and the token used has the right scope + """ + + def has_permission(self, request, view): + if not request.auth: + return False + + read_only = request.method in SAFE_METHODS + if hasattr(request.auth, 'resource'): # oauth 1 + pass + elif hasattr(request.auth, 'scope'): # oauth 2 + scope_valid = lambda scope_wanted_key, scope_had: oauth2_provider_scope.check( + oauth2_provider_scope.SCOPE_NAME_DICT[scope_wanted_key], scope_had) + + if (read_only and scope_valid('read', request.auth.scope)): + return True + elif scope_valid('write', request.auth.scope): + return True + return False + else: + # Improperly configured! + pass diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 9e86881a..693dbb4d 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -17,7 +17,7 @@ from rest_framework.authentication import ( ) from rest_framework.authtoken.models import Token from rest_framework.compat import patterns, url, include -from rest_framework.compat import oauth2_provider, oauth2_provider_models +from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope from rest_framework.compat import oauth, oauth_provider from rest_framework.tests.utils import RequestFactory from rest_framework.views import APIView @@ -54,6 +54,8 @@ 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])), + url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication], + permission_classes=[permissions.TokenHasReadWriteScope])), ) @@ -514,3 +516,27 @@ class OAuth2Tests(TestCase): 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) + + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_post_form_with_invalid_scope_failing_auth(self): + """Ensure POSTing with a readonly scope instead of a write scope fails""" + read_only_access_token = self.access_token + read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read'] + read_only_access_token.save() + auth = self._create_authorization_header(token=read_only_access_token.token) + params = self._client_credentials_params() + response = self.csrf_client.get('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + response = self.csrf_client.post('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_post_form_with_valid_scope_passing_auth(self): + """Ensure POSTing with a write scope succeed""" + read_write_access_token = self.access_token + read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write'] + read_write_access_token.save() + auth = self._create_authorization_header(token=read_write_access_token.token) + params = self._client_credentials_params() + response = self.csrf_client.post('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) -- cgit v1.2.3 From eec8efafc3eeacf00696208d2e1e55a11821257b Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Sun, 10 Mar 2013 14:40:20 +0100 Subject: Add the implementation for TokenHasReadWriteScope permissions w/ oauth 1 --- rest_framework/permissions.py | 8 ++++++-- rest_framework/tests/authentication.py | 37 +++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 519a3691..c477474c 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -140,12 +140,16 @@ class TokenHasReadWriteScope(BasePermission): read_only = request.method in SAFE_METHODS if hasattr(request.auth, 'resource'): # oauth 1 - pass + if read_only: + return True + elif request.auth.resource.is_readonly is False: + return True + return False elif hasattr(request.auth, 'scope'): # oauth 2 scope_valid = lambda scope_wanted_key, scope_had: oauth2_provider_scope.check( oauth2_provider_scope.SCOPE_NAME_DICT[scope_wanted_key], scope_had) - if (read_only and scope_valid('read', request.auth.scope)): + if read_only and scope_valid('read', request.auth.scope): return True elif scope_valid('write', request.auth.scope): return True diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 693dbb4d..b663ca48 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -47,7 +47,9 @@ urlpatterns = patterns('', (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])) + (r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])), + (r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication], + permission_classes=[permissions.TokenHasReadWriteScope])) ) if oauth2_provider is not None: @@ -391,6 +393,39 @@ class OAuthTests(TestCase): response = self.csrf_client.post('/oauth/', 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_get_form_with_readonly_resource_passing_auth(self): + """Ensure POSTing with a readonly resource instead of a write scope fails""" + read_only_access_token = self.token + read_only_access_token.resource.is_readonly = True + read_only_access_token.resource.save() + params = self._create_authorization_url_parameters() + response = self.csrf_client.get('/oauth-with-scope/', 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_form_with_readonly_resource_failing_auth(self): + """Ensure POSTing with a readonly resource instead of a write scope fails""" + read_only_access_token = self.token + read_only_access_token.resource.is_readonly = True + read_only_access_token.resource.save() + params = self._create_authorization_url_parameters() + response = self.csrf_client.post('/oauth-with-scope/', params) + 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_write_resource_passing_auth(self): + """Ensure POSTing with a write resource succeed""" + read_write_access_token = self.token + read_write_access_token.resource.is_readonly = False + read_write_access_token.resource.save() + params = self._create_authorization_url_parameters() + response = self.csrf_client.post('/oauth-with-scope/', params) + self.assertEqual(response.status_code, 200) + class OAuth2Tests(TestCase): """OAuth 2.0 authentication""" -- cgit v1.2.3 From f513db714db76849448bf2e2412428ee7121ebf6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Mar 2013 19:07:30 +0000 Subject: Clean up TokenHasReadWriteScope slightly --- rest_framework/compat.py | 8 ++------ rest_framework/permissions.py | 33 +++++++++++++-------------------- 2 files changed, 15 insertions(+), 26 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/compat.py b/rest_framework/compat.py index e9570a08..7b2ef738 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -445,19 +445,15 @@ except ImportError: # 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) from provider.oauth2 import backends as oauth2_provider_backends from provider.oauth2 import models as oauth2_provider_models from provider.oauth2 import forms as oauth2_provider_forms from provider import scope as oauth2_provider_scope - + from provider import constants as oauth2_constants except ImportError: oauth2_provider = None oauth2_provider_backends = None oauth2_provider_models = None oauth2_provider_forms = None oauth2_provider_scope = None + oauth2_constants = None diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 92f8215a..f026850a 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -7,7 +7,7 @@ import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] -from rest_framework.compat import oauth2_provider_scope +from rest_framework.compat import oauth2_provider_scope, oauth2_constants class BasePermission(object): @@ -142,25 +142,18 @@ class TokenHasReadWriteScope(BasePermission): """ def has_permission(self, request, view): - if not request.auth: - return False - + token = request.auth read_only = request.method in SAFE_METHODS - if hasattr(request.auth, 'resource'): # oauth 1 - if read_only: - return True - elif request.auth.resource.is_readonly is False: - return True - return False - elif hasattr(request.auth, 'scope'): # oauth 2 - scope_valid = lambda scope_wanted_key, scope_had: oauth2_provider_scope.check( - oauth2_provider_scope.SCOPE_NAME_DICT[scope_wanted_key], scope_had) - - if read_only and scope_valid('read', request.auth.scope): - return True - elif scope_valid('write', request.auth.scope): - return True + + if not token: return False + + if hasattr(token, 'resource'): # OAuth 1 + return read_only or not request.auth.resource.is_readonly + elif hasattr(token, 'scope'): # OAuth 2 + required = oauth2_constants.READ if read_only else oauth2_constants.WRITE + return oauth2_provider_scope.check(required, request.auth.scope) else: - # Improperly configured! - pass + assert False, ('TokenHasReadWriteScope requires either the' + '`OAuthAuthentication` or `OAuth2Authentication` authentication ' + 'class to be used.') -- cgit v1.2.3 From 043d748b539a6f5b4cfdf6de650b072541f1c6da Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Mar 2013 20:12:28 +0000 Subject: Tweak --- rest_framework/permissions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index f026850a..ae895f39 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -153,7 +153,7 @@ class TokenHasReadWriteScope(BasePermission): elif hasattr(token, 'scope'): # OAuth 2 required = oauth2_constants.READ if read_only else oauth2_constants.WRITE return oauth2_provider_scope.check(required, request.auth.scope) - else: - assert False, ('TokenHasReadWriteScope requires either the' - '`OAuthAuthentication` or `OAuth2Authentication` authentication ' - 'class to be used.') + + assert False, ('TokenHasReadWriteScope requires either the' + '`OAuthAuthentication` or `OAuth2Authentication` authentication ' + 'class to be used.') -- cgit v1.2.3