Thomas Johansson avatar Thomas Johansson committed 9fda77a

Add new python-oauth2 based authentication. Also moves existing authentication classes into its own package.

Comments (0)

Files changed (9)

piston/authentication/__init__.py

+from piston.authentication.basic import HttpBasicAuthentication, HttpBasicSimple
+
+class NoAuthentication(object):
+    """
+    Authentication handler that always returns
+    True, so no authentication is needed, nor
+    initiated (`challenge` is missing.)
+    """
+    def is_authenticated(self, request):
+        return True

piston/authentication/basic.py

+import binascii
+
+from django.contrib.auth import authenticate
+from django.contrib.auth.models import User, AnonymousUser
+from django.http import HttpResponse
+
+
+class HttpBasicAuthentication(object):
+    """
+    Basic HTTP authenticater. Synopsis:
+    
+    Authentication handlers must implement two methods:
+     - `is_authenticated`: Will be called when checking for
+        authentication. Receives a `request` object, please
+        set your `User` object on `request.user`, otherwise
+        return False (or something that evaluates to False.)
+     - `challenge`: In cases where `is_authenticated` returns
+        False, the result of this method will be returned.
+        This will usually be a `HttpResponse` object with
+        some kind of challenge headers and 401 code on it.
+    """
+    def __init__(self, auth_func=authenticate, realm='API'):
+        self.auth_func = auth_func
+        self.realm = realm
+
+    def is_authenticated(self, request):
+        auth_string = request.META.get('HTTP_AUTHORIZATION', None)
+
+        if not auth_string:
+            return False
+            
+        try:
+            (authmeth, auth) = auth_string.split(" ", 1)
+
+            if not authmeth.lower() == 'basic':
+                return False
+
+            auth = auth.strip().decode('base64')
+            (username, password) = auth.split(':', 1)
+        except (ValueError, binascii.Error):
+            return False
+        
+        request.user = self.auth_func(username=username, password=password) \
+            or AnonymousUser()
+                
+        return not request.user in (False, None, AnonymousUser())
+        
+    def challenge(self):
+        resp = HttpResponse("Authorization Required")
+        resp['WWW-Authenticate'] = 'Basic realm="%s"' % self.realm
+        resp.status_code = 401
+        return resp
+
+    def __repr__(self):
+        return u'<HTTPBasic: realm=%s>' % self.realm
+
+
+class HttpBasicSimple(HttpBasicAuthentication):
+    def __init__(self, realm, username, password):
+        self.user = User.objects.get(username=username)
+        self.password = password
+
+        super(HttpBasicSimple, self).__init__(auth_func=self.hash, realm=realm)
+    
+    def hash(self, username, password):
+        if username == self.user.username and password == self.password:
+            return self.user

piston/authentication/oauth/__init__.py

+import oauth2 as oauth
+from django.conf import settings
+from django.http import HttpResponse
+from django.template import loader
+
+from piston.authentication.oauth.store import store, InvalidAccessToken, InvalidConsumer
+from piston.authentication.oauth.utils import get_oauth_request, verify_oauth_request
+
+
+class OAuthAuthentication(object):
+    def __init__(self, realm='API'):
+        self.realm = realm
+
+    def is_authenticated(self, request):
+        oauth_request = get_oauth_request(request)
+
+        try:
+            consumer = store.get_consumer(request, oauth_request, oauth_request['oauth_consumer_key'])
+            access_token = store.get_access_token(request, oauth_request, consumer, oauth_request['oauth_token'])
+        except (InvalidConsumer, InvalidAccessToken):
+            return False
+    
+        if not verify_oauth_request(request, oauth_request, consumer, access_token):
+            return False
+
+        request.user = store.get_user_from_access_token(request, oauth_request, access_token)
+        request.consumer = store.get_consumer_from_access_token(request, oauth_request, access_token)
+        request.throttle_extra = request.consumer.key
+
+        return True
+        
+    def challenge(self):
+        """
+        Returns a 401 response with a small bit on
+        what OAuth is, and where to learn more about it.
+        
+        When this was written, browsers did not understand
+        OAuth authentication on the browser side, and hence
+        the helpful template we render. Maybe some day in the
+        future, browsers will take care of this stuff for us
+        and understand the 401 with the realm we give it.
+        """
+        response = HttpResponse()
+        response.status_code = 401
+
+        for k, v in oauth.build_authenticate_header(realm=self.realm).iteritems():
+            response[k] = v
+
+        tmpl = loader.render_to_string('piston/oauth/challenge.html',
+            { 'MEDIA_URL': settings.MEDIA_URL })
+
+        response.content = tmpl
+
+        return response

piston/authentication/oauth/forms.py

+from django import forms
+
+
+class AuthorizeRequestTokenForm(forms.Form):
+    oauth_token = forms.CharField(widget=forms.HiddenInput)
+    authorize_access = forms.BooleanField(required=True)

piston/authentication/oauth/store/__init__.py

+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils import importlib
+
+
+class InvalidConsumer(RuntimeError):
+    pass
+
+
+class InvalidToken(RuntimeError):
+    pass
+
+
+class InvalidRequestToken(InvalidToken):
+    pass
+
+
+class InvalidAccessToken(InvalidToken):
+    pass
+
+
+class Store(object):
+    def get_consumer(self, request, oauth_request, consumer_key):
+        """
+        Return an `oauth2.Consumer` (or compatible) instance for `consumer_key`
+        or raise `InvalidConsumer`.
+        
+        `request`: The Django request object.
+        `consumer_key`: The consumer key.
+        """
+        raise NotImplementedError
+    
+    def get_consumer_from_request_token(self, request, oauth_request, request_token):
+        """
+        Return the `oauth2.Consumer` (or compatible) instance associated with
+        the `request_token` request token, or raise `InvalidConsumer`.
+        
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `request_token`: The request token to get the consumer for.
+        """
+        raise NotImplementedError
+    
+    def get_consumer_from_access_token(self, request, oauth_request, access_token):
+        """
+        Return the `oauth2.Consumer` (or compatible) instance associated with
+        the `access_token` access token, or raise `InvalidConsumer`.
+        
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `access_token`: The access token to get the consumer for.
+        """
+        raise NotImplementedError
+        
+    def create_request_token(self, request, oauth_request, consumer, callback):
+        """
+        Generate and return an `oauth2.Token` (or compatible) instance.
+
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer`: The consumer that made the request.
+        """
+        raise NotImplementedError
+
+    def get_request_token(self, request, oauth_request, request_token_key):
+        """
+        Return an `oauth2.Token` (or compatible) instance for
+        `request_token_key` or raise `InvalidRequestToken`.
+        
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer`: The consumer that made the request.
+        `request_token_key`: The request token key.
+        """
+        raise NotImplementedError
+
+    def authorize_request_token(self, request, oauth_request, request_token):
+        """ 
+        Authorize the request token and return it, or raise
+        `InvalidRequestToken`.
+        
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `request_token`: The request token.
+        """
+        raise NotImplementedError
+
+    def create_access_token(self, request, oauth_request, consumer, request_token):
+        """
+        Generate and return a `oauth2.Token` (or compatible) instance.
+        
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer`: The consumer that made the request.
+        `request_token`: The request token used to request the access token.
+        """
+        raise NotImplementedError
+
+    def get_access_token(self, request, oauth_request, consumer, access_token_key):
+        """
+        Return the appropriate `oauth2.Token` (or compatible) instance for
+        `access_token_key` or raise `InvalidAccessToken`.
+        
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `consumer`: The consumer that made the request.
+        `access_token_key`: The access token key used to make the request.
+        """
+        raise NotImplementedError
+
+    def check_nonce(self, request, oauth_request, nonce):
+        """
+        Return `True` if the nonce has not yet been used.
+        
+        `request`: The Django request object.
+        `oauth_request`: The `oauth2.Request` object.
+        `nonce`: The nonce to check.
+        """
+        raise NotImplementedError
+
+
+def get_store(path='piston.authentication.oauth.store.db.ModelStore'):
+    """
+    Load the piston oauth store. Should not be called directly unless testing.
+    """
+    path = getattr(settings, 'PISTON_OAUTH_STORE', path)
+
+    try:
+        module, attr = path.rsplit('.', 1)
+        store_class = getattr(importlib.import_module(module), attr)
+    except ValueError:
+        raise ImproperlyConfigured('Invalid piston oauth store string: "%s"' % path)
+    except ImportError, e:
+        raise ImproperlyConfigured('Error loading piston oauth store module "%s": "%s"' % (module, e))
+    except AttributeError:
+        raise ImproperlyConfigured('Module "%s" does not define a piston oauth store named "%s"' % (module, attr))
+
+    return store_class()
+
+
+store = get_store()

piston/authentication/oauth/store/db.py

+import oauth2 as oauth
+
+from piston.authentication.oauth.store import InvalidAccessToken, InvalidConsumer, InvalidRequestToken, Store
+from piston.authentication.oauth.utils import generate_random
+from piston.models import Nonce, Token, Consumer
+
+
+class ModelStore(Store):    
+    def get_consumer(self, request, oauth_request, consumer_key):
+        try:
+            consumer = Consumer.objects.get(key=consumer_key)
+            return oauth.Consumer(consumer.key, consumer.secret)
+        except Consumer.DoesNotExist:
+            raise InvalidConsumer
+
+    def get_consumer_from_request_token(self, request, oauth_request, request_token):
+        try:
+            return Token.objects.get(key=request_token.key, token_type=Token.REQUEST).consumer
+        except Token.DoesNotExist:
+            raise InvalidConsumer
+
+    def get_consumer_from_access_token(self, request, oauth_request, access_token):
+        try:
+            return Token.objects.get(key=access_token.key, token_type=Token.ACCESS).consumer
+        except Token.DoesNotExist:
+            raise InvalidConsumer
+
+    def create_request_token(self, request, oauth_request, consumer, callback):
+        token = Token.objects.create_token(
+            token_type=Token.REQUEST,
+            consumer=Consumer.objects.get(key=oauth_request['oauth_consumer_key']),
+            timestamp=oauth_request['oauth_timestamp']
+        )
+        token.set_callback(callback)
+        token.save()    
+
+        return token
+
+    def get_request_token(self, request, oauth_request, request_token_key):
+        try:
+            return Token.objects.get(key=request_token_key, token_type=Token.REQUEST)
+        except Token.DoesNotExist:
+            raise InvalidRequestToken
+
+    def authorize_request_token(self, request, oauth_request, request_token):    
+        try:
+            token = Token.objects.get(key=request_token.key, token_type=Token.REQUEST)
+            token.is_approved = True
+            token.user = request.user
+            token.verifier = oauth.generate_verifier()
+            token.save()
+            return token
+        except Token.DoesNotExist:
+            raise InvalidRequestToken
+
+    def create_access_token(self, request, oauth_request, consumer, request_token):
+        request_token = Token.objects.get(key=request_token.key, token_type=Token.REQUEST)
+        access_token = Token.objects.create_token(
+            token_type=Token.ACCESS,
+            timestamp=oauth_request['oauth_timestamp'],
+            consumer=Consumer.objects.get(key=consumer.key),
+            user=request_token.user,
+        )
+        request_token.delete()
+        return access_token
+
+    def get_access_token(self, request, oauth_request, consumer, access_token_key):
+        try:
+            return Token.objects.get(key=access_token_key, token_type=Token.ACCESS)
+        except Token.DoesNotExist:
+            raise InvalidAccessToken
+
+    def get_user_from_access_token(self, request, oauth_request, access_token):
+        try:
+            return Token.objects.get(key=access_token.key, token_type=Token.ACCESS).user
+        except Token.DoesNotExist:
+            raise InvalidConsumer
+
+    def check_nonce(self, request, oauth_request, nonce):
+        nonce, created = Nonce.objects.get_or_create(
+            consumer_key=oauth_request['oauth_consumer_key'],
+            token_key=oauth_request.get('oauth_token', ''),
+            key=nonce
+        )
+        return created

piston/authentication/oauth/urls.py

+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns('piston.authentication.oauth.views',
+    (r'^get_request_token', 'get_request_token'),
+    (r'^authorize_request_token', 'authorize_request_token'),
+    (r'^get_access_token', 'get_access_token'),
+)

piston/authentication/oauth/utils.py

+import oauth2 as oauth
+from django.contrib.auth.models import User
+
+
+def generate_random(length=8):
+    return User.objects.make_random_password(length=length)
+
+
+def get_oauth_request(request):
+    """ Converts a Django request object into an `oauth2.Request` object. """
+    headers = {}
+    if 'HTTP_AUTHORIZATION' in request.META:
+        headers['Authorization'] = request.META['HTTP_AUTHORIZATION']
+    return oauth.Request.from_request(request.method, request.build_absolute_uri(request.path), headers, dict(request.REQUEST))
+
+
+def verify_oauth_request(request, oauth_request, consumer, token=None):
+    """ Helper function to verify requests. """
+    from piston.authentication.oauth.store import store
+
+    # Check nonce
+    if not store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']):
+        return False
+
+    # Verify request
+    try:
+        oauth_server = oauth.Server()
+        oauth_server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+        oauth_server.add_signature_method(oauth.SignatureMethod_PLAINTEXT())
+
+        consumer = oauth.Consumer(consumer.key.encode('ascii', 'ignore'), consumer.secret.encode('ascii', 'ignore'))
+        oauth_server.verify_request(oauth_request, consumer, token)
+    except Exception, e:
+        return False
+    
+    return True

piston/authentication/oauth/views.py

+import oauth2 as oauth
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.views.decorators.csrf import csrf_exempt
+
+from piston.authentication.oauth.forms import AuthorizeRequestTokenForm
+from piston.authentication.oauth.store import store
+from piston.authentication.oauth.utils import verify_oauth_request, get_oauth_request
+
+
+@csrf_exempt
+def get_request_token(request):
+    oauth_request = get_oauth_request(request)
+    consumer = store.get_consumer(request, oauth_request, oauth_request['oauth_consumer_key'])
+
+    # Ensure the client is using 1.0a
+    if 'oauth_callback' not in oauth_request:
+        return HttpResponseForbidden('OAuth 1.0 is not supported, you must use OAuth 1.0a.')
+
+    if not verify_oauth_request(request, oauth_request, consumer):
+        return HttpResponseForbidden('Invalid request')
+
+    request_token = store.create_request_token(request, oauth_request, consumer, oauth_request['oauth_callback'])
+    ret = 'oauth_token=%s&oauth_token_secret=%s&callback_confirmed=true' % (request_token.key, request_token.secret)
+    return HttpResponse(ret, content_type='application/x-www-form-urlencoded')
+
+
+@login_required
+def authorize_request_token(request, form_class=AuthorizeRequestTokenForm, template_name='piston/oauth/authorize.html', verification_template_name='piston/oauth/authorize_verification_code.html'):
+    if 'oauth_token' not in request.REQUEST:
+        return HttpResponse('No token specified.')
+
+    oauth_request = get_oauth_request(request)
+    request_token = store.get_request_token(request, oauth_request, request.REQUEST['oauth_token'])
+    consumer = store.get_consumer_from_request_token(request, oauth_request, request_token)
+    
+    if request.method == 'POST':
+        form = form_class(request.POST)
+        if form.is_valid() and form.cleaned_data['authorize_access']:
+            request_token = store.authorize_request_token(request, oauth_request, request_token)            
+            if request_token.callback is not None and request_token.callback != 'oob':
+                return HttpResponseRedirect('%s&oauth_token=%s' % (request_token.get_callback_url(), request_token.key))
+            else:
+                return render_to_response(verification_template_name, {'consumer': consumer, 'verification_code': request_token.verifier}, RequestContext(request))
+    else:
+        form = form_class(initial={'oauth_token': request_token.key})
+
+    return render_to_response(template_name, {'consumer': consumer, 'form': form}, RequestContext(request))
+
+
+@csrf_exempt
+def get_access_token(request):
+    oauth_request = get_oauth_request(request)
+    consumer = store.get_consumer(request, oauth_request, oauth_request['oauth_consumer_key'])
+    request_token = store.get_request_token(request, oauth_request, oauth_request['oauth_token'])
+
+    if not verify_oauth_request(request, oauth_request, consumer, request_token):
+        return HttpResponseForbidden('Invalid request')
+        
+    if oauth_request.get('oauth_verifier', None) != request_token.verifier:
+        return False
+
+    access_token = store.create_access_token(request, oauth_request, consumer, request_token)
+    ret = 'oauth_token=%s&oauth_token_secret=%s' % (access_token.key, access_token.secret)
+    return HttpResponse(ret, content_type='application/x-www-form-urlencoded')
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.