Permalink
Browse files

add in optional oauth (bug 849075)

  • Loading branch information...
1 parent fbbd0f6 commit 0ff08ffbff9529e8fc9c4e32f91182288d84c4b6 @andymckay andymckay committed Mar 8, 2013
@@ -7,6 +7,7 @@ cef==0.5
celery==2.5.1
celery-tasktree==0.3.2
commonware==0.4.0
+curling==0.2.2
dj-database-url==0.2.1
django-aesfield==0.1
django-celery==2.2.4
@@ -21,6 +22,7 @@ django-statsd-mozilla==0.3.8.3
django-tastypie==0.9.11
funfactory==2.1.1
gunicorn==0.17.2
+httplib2==0.7.6
kombu==2.1.2
lxml==2.2.6
m2secret==0.1.1
@@ -29,6 +31,7 @@ metlog-py==0.9.8
mimeparse==0.1.3
mock==1.0b1
nose==1.2.1
+oauth2==1.5.211
ordereddict==1.1
py-bcrypt==0.2
python-dateutil==1.5
@@ -39,6 +42,7 @@ requests==0.14.1
schematic==0.2
simplejson==2.6.2
six==1.2.0
+slumber==0.5.3
statsd==1.0.0
suds==0.4
tastypie-services==0.1.1
View
@@ -46,3 +46,5 @@
# We don't want to hit the live bango in tests.
BANGO_MOCK = True
+
+SITE_URL = 'http://localhost/'
View
@@ -0,0 +1,112 @@
+import logging
+from urlparse import urljoin
+
+
+from django.conf import settings
+import oauth2
+
+from tastypie.authentication import Authentication
+
+log = logging.getLogger('s.auth')
+
+
+class Consumer(object):
+
+ def __init__(self, key, secret=None):
+ self.key = key
+ self.secret = secret or settings.CLIENT_OAUTH_KEYS[key]
+
+
+class OAuthError(RuntimeError):
+ def __init__(self, message='OAuth error occured.'):
+ self.message = message
+
+
+class OAuthAuthentication(Authentication):
+ """
+ This is based on https://github.com/amrox/django-tastypie-two-legged-oauth
+ with permission.
+ """
+
+ def __init__(self, realm='', consumer=None):
+ self.realm = realm
+ self.consumer = consumer
+
+ def _header(self, request):
+ return request.META.get('HTTP_AUTHORIZATION', None)
+
+ def is_authenticated(self, request, **kwargs):
+ auth_header_value = self._header(request)
+ oauth_server, oauth_request = initialize_oauth_server_request(request)
+ try:
+ key = get_oauth_consumer_key_from_header(auth_header_value)
+ if not key:
+ if settings.REQUIRE_OAUTH:
+ return False
+ return True
+ oauth_server.verify_request(oauth_request, Consumer(key), None)
+ log.error(u'Access granted: %s' % key)
+ return True
+
+ except KeyError:
+ log.error(u'No key: %s' % key)
+ return False
+
+ except:
+ log.error(u'Access failed: %s' % key, exc_info=True)
+ return False
+
+
+def initialize_oauth_server_request(request):
+ """
+ OAuth initialization.
+ """
+
+ # Since 'Authorization' header comes through as 'HTTP_AUTHORIZATION',
+ # convert it back.
+ auth_header = {}
+ if 'HTTP_AUTHORIZATION' in request.META:
+ auth_header = {'Authorization': request.META.get('HTTP_AUTHORIZATION')}
+
+ url = urljoin(settings.SITE_URL, request.path)
+
+ # Note: we are only signing using the QUERY STRING. We are not signing the
+ # body yet. According to the spec we should be including an oauth_body_hash
+ # as per:
+ #
+ # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/1/spec.html
+ #
+ # There is no support in python-oauth2 for this yet. There is an
+ # outstanding pull request for this:
+ #
+ # https://github.com/simplegeo/python-oauth2/pull/110
+ #
+ # Or time to move to a better OAuth implementation.
+ method = getattr(request, 'signed_method', request.method)
+ oauth_request = oauth2.Request.from_request(
+ method, url, headers=auth_header,
+ query_string=request.META['QUERY_STRING'])
+ oauth_server = oauth2.Server(signature_methods={
+ 'HMAC-SHA1': oauth2.SignatureMethod_HMAC_SHA1()
+ })
+ return oauth_server, oauth_request
+
+
+def get_oauth_consumer_key_from_header(auth_header_value):
+ key = None
+
+ # Process Auth Header
+ if not auth_header_value:
+ return None
+ # Check that the authorization header is OAuth.
+ if auth_header_value[:6] == 'OAuth ':
+ auth_header = auth_header_value[6:]
+ try:
+ # Get the parameters from the header.
+ header_params = oauth2.Request._split_header(auth_header)
+ if 'oauth_consumer_key' in header_params:
+ key = header_params['oauth_consumer_key']
+ except:
+ raise OAuthError('Unable to parse OAuth parameters from '
+ 'Authorization header.')
+ return key
View
@@ -21,20 +21,19 @@
pass
from cef import log_cef as _log_cef
-import jwt
from tastypie import http
-from tastypie.authentication import Authentication
from tastypie.authorization import Authorization
from tastypie.exceptions import ImmediateHttpResponse, InvalidFilterError
from tastypie.resources import (ModelResource as TastyPieModelResource,
Resource as TastyPieResource)
-from tastypie.serializers import Serializer
from tastypie.utils import dict_strip_unicode_keys
from tastypie.validation import FormValidation
import test_utils
from lib.delayable.tasks import delayable
+from authentication import OAuthAuthentication
+
log = logging.getLogger('s')
tasty_log = logging.getLogger('django.request.tastypie')
@@ -143,11 +142,6 @@ def get_errors(self, content, field):
return json.loads(content)[field]
-class Authentication(Authentication):
- # TODO(andym): add in authentication here.
- pass
-
-
class Authorization(Authorization):
pass
@@ -202,15 +196,6 @@ def dehydrate(self, bundle):
return super(BaseResource, self).dehydrate(bundle)
def _handle_500(self, request, exception):
- # I'd prefer it if JWT errors went back as unauth errors, not
- # 500 errors.
- if isinstance(exception, JWTDecodeError):
- # Let's log these with a higher severity.
- log_cef(str(exception), request, severity=10)
- return http.HttpUnauthorized(
- content=json.dumps({'reason': str(exception)}),
- content_type='application/json; charset=utf-8')
-
# Print some nice 500 errors back to the clients if not in debug mode.
tb = traceback.format_tb(sys.exc_traceback)
tasty_log.error('%s: %s %s\n%s' % (request.path,
@@ -365,77 +350,20 @@ def is_valid(self, bundle, request=None):
return form.errors
-class JWTDecodeError(Exception):
- pass
-
-
-class JWTSerializer(Serializer):
- jwt_key = None
- formats = ['json', 'jwt']
- content_types = {
- 'jwt': 'application/jwt',
- 'json': 'application/json',
- }
-
- def _error(self, msg, error='none'):
- log.error('%s (%s)' % (msg, error), exc_info=True)
- return JWTDecodeError(msg)
-
- def from_json(self, content):
- if settings.REQUIRE_JWT:
- raise self._error('JWT is required', None)
-
- return super(JWTSerializer, self).from_json(content)
-
- def from_jwt(self, content):
- try:
- key = jwt.decode(content, verify=False).get('jwt-encode-key', '')
- except jwt.DecodeError as err:
- raise self._error('Error decoding JWT', error=err)
-
- if not key:
- raise self._error('No JWT key')
-
- secret = settings.CLIENT_JWT_KEYS.get(key, '')
- if not secret:
- raise self._error('No JWT secret for that key')
-
- try:
- content = jwt.decode(content, secret, verify=True)
- except jwt.DecodeError as err:
- raise self._error('Error decoding JWT', err)
-
- # We don't need this key anymore, delete it so that solitude
- # doesn't trigger a warning on it.
- del content['jwt-encode-key']
- # Store the key and secret on the serializer, so that we can
- # return the same data. Assuming it's always going to be the
- # same object.
- self.jwt_key = {'key': key, 'secret': secret}
- return content
-
- def to_jwt(self, content, options=None):
- assert self.jwt_key
- content['jwt-encode-key'] = self.jwt_key['key']
- return jwt.encode(content, self.jwt_key['secret'])
-
-
class Resource(BaseResource, TastyPieResource):
class Meta:
always_return_data = True
- authentication = Authentication()
+ authentication = OAuthAuthentication()
authorization = Authorization()
- serializer = JWTSerializer()
class ModelResource(BaseResource, TastyPieModelResource):
class Meta:
always_return_data = True
- authentication = Authentication()
+ authentication = OAuthAuthentication()
authorization = Authorization()
- serializer = JWTSerializer()
class Cached(object):
View
@@ -1,7 +1,6 @@
import dj_database_url
import logging.handlers
import os
-import urlparse
from funfactory.settings_base import *
@@ -156,14 +155,13 @@
DUMP_REQUESTS = False
-# If this flag is set, any communication will require JWT encoding of the
-# data using a key set in CLIENT_JWT_KEYS. Note: this does not require JWT for
-# all things, eg: nagios checks.
-REQUIRE_JWT = False
+# If this flag is set, any communication will require OAuth signing of the
+# request. Without this, OAuth is optional. This should be True for production.
+REQUIRE_OAUTH = False
-# A mapping of the keys and secrets that will be used to encode the JWT
+# A mapping of the keys and secrets that will be used to sign OAuth
# for any server talking to this server.
-CLIENT_JWT_KEYS = {}
+CLIENT_OAUTH_KEYS = {}
# Bango API settings.
BANGO_AUTH = {'USER': 'Mozilla', 'PASSWORD': ''}
@@ -201,3 +199,6 @@
}
# Sensitive keys.
SENSITIVE_DATA_KEYS = ['bankAccountNumber', 'pin', 'secret']
+
+# Set this for OAuth.
+SITE_URL = ''
Oops, something went wrong.

0 comments on commit 0ff08ff

Please sign in to comment.