Permalink
Browse files

Added the ``OAuthAuthentication`` class.

Thanks to zyegfryed for the original patch!
  • Loading branch information...
1 parent fda8257 commit 5799328a14b95912a8a470dabb357ea17640d59e @toastdriven toastdriven committed Sep 19, 2011
Showing with 155 additions and 14 deletions.
  1. +1 −0 AUTHORS
  2. +20 −10 docs/authentication_authorization.rst
  3. +80 −1 tastypie/authentication.py
  4. +53 −3 tests/core/tests/authentication.py
  5. +1 −0 tests/settings_core.py
View
@@ -34,6 +34,7 @@ Contributors:
* Evan Borgstrom (fatbox) for a documentation patch.
* Madis V (madisvain) for a README patch.
* Ed Summers (edsu) for a setup.py patch.
+* Sébastien Fievet (zyegfryed) for the initial OAuth implementation.
Thanks to Tav for providing validate_jsonp.py, placed in public domain.
@@ -23,8 +23,8 @@ Using these classes is simple. Simply provide them (or your own class) as a
from tastypie.authentication import BasicAuthentication
from tastypie.authorization import DjangoAuthorization
from tastypie.resources import ModelResource
-
-
+
+
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
@@ -74,7 +74,7 @@ objects. Hooking it up looks like::
from django.contrib.auth.models import User
from django.db import models
from tastypie.models import create_api_key
-
+
models.signals.post_save.connect(create_api_key, sender=User)
``DigestAuthentication``
@@ -94,6 +94,16 @@ should be included in ``INSTALLED_APPS``.
.. _`this post`: http://www.nerdydork.com/basic-authentication-on-mod_wsgi.html
+``OAuthAuthentication``
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Handles OAuth, which checks a user's credentials against a separate service.
+Currently verifies against OAuth 1.0a services.
+
+This does *NOT* provide OAuth authentication in your API, strictly
+consumption.
+
+
Authorization Options
=====================
@@ -134,31 +144,31 @@ required method and one optional method::
from tastypie.authentication import Authentication
from tastypie.authorization import Authorization
-
-
+
+
class SillyAuthentication(Authentication):
def is_authenticated(self, request, **kwargs):
if 'daniel' in request.user.username:
return True
-
+
return False
-
+
# Optional but recommended
def get_identifier(self, request):
return request.user.username
-
+
class SillyAuthorization(Authorization):
def is_authorized(self, request, object=None):
if request.user.date_joined.year == 2010:
return True
else:
return False
-
+
# Optional but useful for advanced limiting, such as per user.
def apply_limits(self, request, object_list):
if request and hasattr(request, 'user'):
return object_list.filter(author__username=request.user.username)
-
+
return object_list.none()
Under this scheme, only users with 'daniel' in their username will be allowed
View
@@ -6,6 +6,7 @@
from django.conf import settings
from django.contrib.auth import authenticate
from django.core.exceptions import ImproperlyConfigured
+from django.utils.translation import ugettext as _
from tastypie.http import HttpUnauthorized
try:
@@ -19,6 +20,16 @@
except ImportError:
python_digest = None
+try:
+ import oauth2
+except ImportError:
+ oauth2 = None
+
+try:
+ import oauth_provider
+except ImportError:
+ oauth_provider = None
+
class Authentication(object):
"""
@@ -156,7 +167,7 @@ def get_key(self, user, api_key):
from tastypie.models import ApiKey
try:
- key = ApiKey.objects.get(user=user, key=api_key)
+ ApiKey.objects.get(user=user, key=api_key)
except ApiKey.DoesNotExist:
return self._unauthorized()
@@ -281,3 +292,71 @@ def get_identifier(self, request):
return request.user.username
return 'nouser'
+
+
+class OAuthAuthentication(Authentication):
+ """
+ Handles OAuth, which checks a user's credentials against a separate service.
+ Currently verifies against OAuth 1.0a services.
+
+ This does *NOT* provide OAuth authentication in your API, strictly
+ consumption.
+ """
+ def __init__(self):
+ super(OAuthAuthentication, self).__init__()
+
+ 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 is_authenticated(self, request, **kwargs):
+ from oauth_provider.store import store, InvalidTokenError
+
+ if self.is_valid_request(request):
+ oauth_request = oauth_provider.utils.get_oauth_request(request)
+ consumer = store.get_consumer(request, oauth_request, oauth_request.get_parameter('oauth_consumer_key'))
+
+ try:
+ token = store.get_access_token(request, oauth_request, consumer, oauth_request.get_parameter('oauth_token'))
+ except oauth_provider.store.InvalidTokenError:
+ return oauth_provider.utils.send_oauth_error(oauth2.Error(_('Invalid access token: %s') % oauth_request.get_parameter('oauth_token')))
+
+ try:
+ self.validate_token(request, consumer, token)
+ except oauth2.Error, e:
+ return oauth_provider.utils.send_oauth_error(e)
+
+ if consumer and token:
+ request.user = token.user
+ return True
+
+ return oauth_provider.utils.send_oauth_error(oauth2.Error(_('You are not allowed to access this resource.')))
+
+ return oauth_provider.utils.send_oauth_error(oauth2.Error(_('Invalid request parameters.')))
+
+ 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)
@@ -1,18 +1,23 @@
import base64
+import time
+import warnings
from django.contrib.auth.models import User
from django.core import mail
from django.http import HttpRequest
from django.test import TestCase
-from tastypie.authentication import Authentication, BasicAuthentication, ApiKeyAuthentication, DigestAuthentication
+from tastypie.authentication import Authentication, BasicAuthentication, ApiKeyAuthentication, DigestAuthentication, OAuthAuthentication
from tastypie.http import HttpUnauthorized
from tastypie.models import ApiKey, create_api_key
# Be tricky.
-from tastypie.authentication import python_digest
+from tastypie.authentication import python_digest, oauth2, oauth_provider
if python_digest is None:
- import warnings
warnings.warn("Running tests without python_digest! Bad news!")
+if oauth2 is None:
+ warnings.warn("Running tests without oauth2! Bad news!")
+if oauth_provider is None:
+ warnings.warn("Running tests without oauth_provider! Bad news!")
class AuthenticationTestCase(TestCase):
@@ -164,3 +169,48 @@ def test_is_authenticated(self):
)
auth_request = auth.is_authenticated(request)
self.assertEqual(auth_request, True)
+
+
+class OAuthAuthenticationTestCase(TestCase):
+ fixtures = ['note_testdata.json']
+
+ def test_is_authenticated(self):
+ from oauth_provider.models import Consumer, Token, Resource
+ auth = OAuthAuthentication()
+ request = HttpRequest()
+ request.META['SERVER_NAME'] = 'testsuite'
+ request.META['SERVER_PORT'] = '8080'
+ request.REQUEST = request.GET = {}
+ request.method = "GET"
+
+ # Invalid request.
+ resp = auth.is_authenticated(request)
+ self.assertEqual(resp.status_code, 401)
+
+ # No username/api_key details should fail.
+ request.REQUEST = request.GET = {
+ 'oauth_consumer_key': '123',
+ 'oauth_nonce': 'abc',
+ 'oauth_signature': '&',
+ 'oauth_signature_method': 'PLAINTEXT',
+ 'oauth_timestamp': str(int(time.time())),
+ 'oauth_token': 'foo',
+ }
+ user = User.objects.create_user('daniel', 'test@example.com', 'password')
+ request.META['Authorization'] = 'OAuth ' + ','.join([key+'='+value for key, value in request.REQUEST.items()])
+ resource, _ = Resource.objects.get_or_create(url='test', defaults={
+ 'name': 'Test Resource'
+ })
+ consumer, _ = Consumer.objects.get_or_create(key='123', defaults={
+ 'name': 'Test',
+ 'description': 'Testing...'
+ })
+ token, _ = Token.objects.get_or_create(key='foo', token_type=Token.ACCESS, defaults={
+ 'consumer': consumer,
+ 'resource': resource,
+ 'secret': '',
+ 'user': user,
+ })
+ resp = auth.is_authenticated(request)
+ self.assertEqual(resp, True)
+ self.assertEqual(request.user.pk, user.pk)
View
@@ -1,5 +1,6 @@
from settings import *
INSTALLED_APPS.append('core')
+INSTALLED_APPS.append('oauth_provider')
ROOT_URLCONF = 'core.tests.api_urls'
MEDIA_URL = 'http://localhost:8080/media/'

0 comments on commit 5799328

Please sign in to comment.