[snowy] Require OAuth and patch piston to do OAuth properly



commit c85431521c3a8748e53f9aa3627d528b11a564b3
Author: Stuart Langridge <stuart langridge canonical com>
Date:   Fri Jun 5 11:20:04 2009 +0100

    Require OAuth and patch piston to do OAuth properly

 api/urls.py                  |    5 +-
 lib/piston/authentication.py |  116 ++++++++++++++++++++++++++----------------
 lib/piston/forms.py          |   44 ++++++++++++++++
 notes/views.py               |   10 ++++
 settings.py                  |    1 +
 urls.py                      |    8 +++
 6 files changed, 138 insertions(+), 46 deletions(-)
---
diff --git a/api/urls.py b/api/urls.py
index 060dd42..02df555 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -17,13 +17,14 @@
 
 from django.conf.urls.defaults import *
 
-from piston.authentication import HttpBasicAuthentication
+from piston.authentication import HttpBasicAuthentication, OAuthAuthentication
 from piston.resource import Resource
 
 from snowy.api.handlers import *
 
 auth = HttpBasicAuthentication(realm='Snowy')
-ad = {'authentication': auth}
+authoauth = OAuthAuthentication(realm='Snowy')
+ad = {'authentication': authoauth}
 
 user_handler = Resource(UserHandler)
 notes_handler = Resource(handler=NotesHandler, **ad)
diff --git a/lib/piston/authentication.py b/lib/piston/authentication.py
index e3fcdaf..2f325fa 100644
--- a/lib/piston/authentication.py
+++ b/lib/piston/authentication.py
@@ -1,29 +1,26 @@
 from django.http import HttpResponse, HttpResponseRedirect
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User, AnonymousUser
 from django.contrib.auth.decorators import login_required
 from django.template import loader
+from django.contrib.auth import authenticate
 from django.conf import settings
 from django.core.urlresolvers import get_callable
+from django.core.exceptions import ImproperlyConfigured
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.utils.importlib import import_module
 
 import oauth
-from store import DataStore
+from piston import forms
 
-def django_auth(username, password):
+class NoAuthentication(object):
     """
-    Basic callback for `HttpBasicAuthentication`
-    which checks the username and password up
-    against Djangos built-in authentication system.
-    
-    On success, returns the `User`, *not* boolean!
+    Authentication handler that always returns
+    True, so no authentication is needed, nor
+    initiated (`challenge` is missing.)
     """
-    try:
-        user = User.objects.get(username=username)
-        if user.check_password(password):
-            return user
-        else:
-            return False
-    except User.DoesNotExist:
-        return False
+    def is_authenticated(self, request):
+        return True
 
 class HttpBasicAuthentication(object):
     """
@@ -39,7 +36,7 @@ class HttpBasicAuthentication(object):
         This will usually be a `HttpResponse` object with
         some kind of challenge headers and 401 code on it.
     """
-    def __init__(self, auth_func=django_auth, realm='API'):
+    def __init__(self, auth_func=authenticate, realm='API'):
         self.auth_func = auth_func
         self.realm = realm
 
@@ -57,9 +54,10 @@ class HttpBasicAuthentication(object):
         auth = auth.strip().decode('base64')
         (username, password) = auth.split(':', 1)
         
-        request.user = self.auth_func(username, password)
+        request.user = self.auth_func(username=username, password=password) \
+            or AnonymousUser()
         
-        return not request.user is False
+        return not request.user in (False, None, AnonymousUser())
         
     def challenge(self):
         resp = HttpResponse("Authorization Required")
@@ -67,6 +65,28 @@ class HttpBasicAuthentication(object):
         resp.status_code = 401
         return resp
 
+DataStore = None
+
+def load_data_store():
+    '''Load data store for OAuth Consumers, Tokens, Nonces and Resources
+    '''
+    path = getattr(settings, 'OAUTH_DATA_STORE', 'piston.store.DataStore')
+
+    # stolen from django.contrib.auth.load_backend
+    i = path.rfind('.')
+    module, attr = path[:i], path[i+1:]
+    try:
+        mod = import_module(module)
+    except ImportError, e:
+        raise ImproperlyConfigured, 'Error importing OAuth data store %s: "%s"' % (module, e)
+
+    try:
+        cls = getattr(mod, attr)
+    except AttributeError:
+        raise ImproperlyConfigured, 'Module %s does not define a "%s" OAuth data store' % (module, attr)
+
+    return cls
+
 def initialize_server_request(request):
     """
     Shortcut for initialization.
@@ -77,6 +97,10 @@ def initialize_server_request(request):
         query_string=request.environ.get('QUERY_STRING', ''))
         
     if oauth_request:
+        global DataStore
+        if DataStore is None:
+            DataStore = load_data_store()
+
         oauth_server = oauth.OAuthServer(DataStore(oauth_request))
         oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
         oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
@@ -115,7 +139,12 @@ def oauth_request_token(request):
     return response
 
 def oauth_auth_view(request, token, callback, params):
-    return HttpResponse("Just a fake view for auth. %s, %s, %s" % (token, callback, params))
+    form = forms.OAuthAuthenticationForm(initial={
+        'oauth_token': token.key,
+        'oauth_callback': callback,
+        })
+    return render_to_response('piston/authorize_token.html',
+            { 'form': form }, RequestContext(request))
 
 @login_required
 def oauth_user_auth(request):
@@ -135,35 +164,34 @@ def oauth_user_auth(request):
         callback = None
         
     if request.method == "GET":
-        request.session['oauth'] = token.key
         params = oauth_request.get_normalized_parameters()
 
-        oauth_view = getattr(settings, 'OAUTH_AUTH_VIEW', 'oauth_auth_view')
-
-        return get_callable(oauth_view)(request, token, callback, params)
+        oauth_view = getattr(settings, 'OAUTH_AUTH_VIEW', None)
+        if oauth_view is None:
+            return oauth_auth_view(request, token, callback, params)
+        else:
+            return get_callable(oauth_view)(request, token, callback, params)
     elif request.method == "POST":
-        if request.session.get('oauth', '') == token.key:
-            request.session['oauth'] = ''
+        try:
+            form = forms.OAuthAuthenticationForm(request.POST)
+            if form.is_valid():
+                token = oauth_server.authorize_token(token, request.user)
+                args = '?'+token.to_string(only_key=True)
+            else:
+                args = '?error=%s' % 'Access not granted by user.'
             
-            try:
-                if int(request.POST.get('authorize_access', '0')):
-                    token = oauth_server.authorize_token(token, request.user)
-                    args = '?'+token.to_string(only_key=True)
-                else:
-                    args = '?error=%s' % 'Access not granted by user.'
+            if not callback:
+                callback = getattr(settings, 'OAUTH_CALLBACK_VIEW')
+                return get_callable(callback)(request, token)
                 
-                if not callback:
-                    callback = getattr(settings, 'OAUTH_CALLBACK_VIEW')
-                    return get_callable(callback)(request, token)
-                    
-                response = HttpResponseRedirect(callback+args)
-                    
-            except oauth.OAuthError, err:
-                response = send_oauth_error(err)
-        else:
-            response = HttpResponse('Action not allowed.')
+            response = HttpResponseRedirect(callback+args)
+                
+        except oauth.OAuthError, err:
+            response = send_oauth_error(err)
+    else:
+        response = HttpResponse('Action not allowed.')
             
-        return response
+    return response
 
 def oauth_access_token(request):
     oauth_server, oauth_request = initialize_server_request(request)
@@ -257,4 +285,4 @@ class OAuthAuthentication(object):
     def validate_token(request, check_timestamp=True, check_nonce=True):
         oauth_server, oauth_request = initialize_server_request(request)
         return oauth_server.verify_request(oauth_request)
-        
\ No newline at end of file
+
diff --git a/lib/piston/forms.py b/lib/piston/forms.py
index 727f997..8f1f1d7 100644
--- a/lib/piston/forms.py
+++ b/lib/piston/forms.py
@@ -1,4 +1,8 @@
+import hmac
+import base64
+
 from django import forms
+from django.conf import settings
 
 class Form(forms.Form):
     pass
@@ -17,3 +21,43 @@ class ModelForm(forms.ModelForm):
         for field in filter(filt, getattr(self.Meta, 'fields', ())):
             self.data[field] = self.initial.get(field, None)
 
+
+class OAuthAuthenticationForm(forms.Form):
+    oauth_token = forms.CharField(widget=forms.HiddenInput)
+    oauth_callback = forms.URLField(widget=forms.HiddenInput)
+    authorize_access = forms.BooleanField(required=True)
+    csrf_signature = forms.CharField(widget=forms.HiddenInput)
+
+    def __init__(self, *args, **kwargs):
+        forms.Form.__init__(self, *args, **kwargs)
+
+        self.fields['csrf_signature'].initial = self.initial_csrf_signature
+
+    def clean_csrf_signature(self):
+        sig = self.cleaned_data['csrf_signature']
+        token = self.cleaned_data['oauth_token']
+
+        sig1 = OAuthAuthenticationForm.get_csrf_signature(settings.SECRET_KEY, token)
+
+        if sig != sig1:
+            raise forms.ValidationError("CSRF signature is not valid")
+
+        return sig
+
+    def initial_csrf_signature(self):
+        token = self.initial['oauth_token']
+        return OAuthAuthenticationForm.get_csrf_signature(settings.SECRET_KEY, token)
+
+    @staticmethod
+    def get_csrf_signature(key, token):
+        # Check signature...
+        try:
+            import hashlib # 2.5
+            hashed = hmac.new(key, token, hashlib.sha1)
+        except:
+            import sha # deprecated
+            hashed = hmac.new(key, token, sha)
+
+        # calculate the digest base 64
+        return base64.b64encode(hashed.digest())
+
diff --git a/notes/views.py b/notes/views.py
index 68fb5a2..1cfcb2a 100644
--- a/notes/views.py
+++ b/notes/views.py
@@ -24,6 +24,8 @@ from snowy.notes.templates import CONTENT_TEMPLATES, DEFAULT_CONTENT_TEMPLATE
 from snowy.notes.models import *
 from snowy import settings
 
+from piston import forms as piston_forms
+
 def note_index(request, username,
                template_name='note/note_index.html'):
     author = get_object_or_404(User, username=username)
@@ -76,3 +78,11 @@ def note_detail(request, username, note_id, slug='',
                                'note': note, 'body': body,
                                'request': request, 'author': author},
                               context_instance=RequestContext(request))
+    
+def note_oauth_auth_view(request, token, callback, params):
+    form = piston_forms.OAuthAuthenticationForm(initial={
+        'oauth_token': token.key,
+        'oauth_callback': callback,
+        })
+    return render_to_response('notes/note_authorize_token.html',
+            { 'form': form }, RequestContext(request))
diff --git a/settings.py b/settings.py
index ef7ec1e..0b31b89 100644
--- a/settings.py
+++ b/settings.py
@@ -99,6 +99,7 @@ INSTALLED_APPS = (
     'reversion',
     'gravatar',
     'autoslug',
+    'piston',
 
     # Local apps
     'notes',
diff --git a/urls.py b/urls.py
index c39ce8c..cb797a0 100644
--- a/urls.py
+++ b/urls.py
@@ -47,3 +47,11 @@ if settings.DEBUG:
             'document_root': settings.MEDIA_ROOT,
             'show_indexes': True
         }))
+
+# OAuth
+urlpatterns += patterns('piston.authentication',
+    url(r'^oauth/request_token/$', 'oauth_request_token'),
+    url(r'^oauth/authenticate/$', 'oauth_user_auth'),
+    url(r'^oauth/access_token/$', 'oauth_access_token'),
+)
+



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]