Re: [Snowy] OAuth in Snowy
- From: Sandy Armstrong <sanfordarmstrong gmail com>
- To: Stuart Langridge <stuart langridge canonical com>
- Cc: snowy-list gnome org
- Subject: Re: [Snowy] OAuth in Snowy
- Date: Sat, 6 Jun 2009 12:46:11 -0700
On Fri, Jun 5, 2009 at 3:26 AM, Stuart
Langridge<stuart langridge canonical com> wrote:
> OK, I've added OAuth support to Snowy. OAuth endpoints are at
> /oauth/{request_token,authenticate,access_token} and everything under
> api/ now requires OAuth (and does not work with basic auth). Two patches
> attached.
>
> I had to patch piston; the piston patches are required for OAuth support
> to actually work, and are taken from
> http://bitbucket.org/ephelon/django-piston/changeset/a7f81eae936c/ in
> consultation with jespern (who hasn't yet merged these patches upstream).
>
> Your comments invited. :-)
Hey Stuart,
So I'm trying to test this, and I admit I'm pretty new to django and
OAuth so I could be missing something obvious, but I'm unable to show
the authorization page. Here's what I've done:
1. python manage.py syncdb (to add new piston OAuth tables)
2. Add a "tomboy" consumer to the consumers table, with a key and all
that stuff (I did not specify a user, as I assume that's not needed)
3. I got a request token like so:
$ curl -d 'oauth_consumer_key=abcdefg&oauth_signature_method=PLAINTEXT&oauth_signature=1234567%26&oauth_timestamp=1244317000&oauth_nonce=hsu94j3884jdopsl&oauth_version=1.0'
'http://localhost:8000/oauth/request_token/'
oauth_token_secret=jdcAFxGWt7xMmmWN6HzR6eaPQF9j73jN&oauth_token=89MrNyNHtXwg9CNrdK[
So that worked, as I got back a token and secret.
4. Using the token, I tried to authenticate in my browser using this URL:
http://localhost:8000/oauth/authenticate?oauth_token=89MrNyNHtXwg9CNrdK
And I get "TemplateDoesNotExist at /oauth/authenticate/
piston/authorize_token.html"
I'm not sure what to do here. Any clues?
I'm attaching updated patches that apply against latest git master
(and have Stuart's correct email address). I suppose it's possible
that in updating the patches, I messed something up, so you might want
to review them.
Thanks,
Sandy
From b4bc5d4583a20f3ddbea083306896180e22ef538 Mon Sep 17 00:00:00 2001
From: Stuart Langridge <stuart langridge canonical com>
Date: Fri, 5 Jun 2009 11:17:43 +0100
Subject: [PATCH 1/2] Add OAuth endpoints, and require OAuth for accessing api/* URLs
---
notes/templates/notes/note_authorize_token.html | 13 +++++++++++++
notes/templates/oauth/challenge.html | 1 +
2 files changed, 14 insertions(+), 0 deletions(-)
create mode 100644 notes/templates/notes/note_authorize_token.html
create mode 100644 notes/templates/oauth/challenge.html
diff --git a/notes/templates/notes/note_authorize_token.html b/notes/templates/notes/note_authorize_token.html
new file mode 100644
index 0000000..49c5768
--- /dev/null
+++ b/notes/templates/notes/note_authorize_token.html
@@ -0,0 +1,13 @@
+{% extends 'notes/base.html' %}
+
+{% block title %}Notes | {{ block.super }}{% endblock %}
+{% block content %}
+<p>
+ <form action="{% url piston.authentication.oauth_user_auth %}" method="POST">
+ <table>
+ {{ form.as_table }}
+ </table>
+ <input type="submit">
+ </form>
+{% endblock %}
+
diff --git a/notes/templates/oauth/challenge.html b/notes/templates/oauth/challenge.html
new file mode 100644
index 0000000..71b948f
--- /dev/null
+++ b/notes/templates/oauth/challenge.html
@@ -0,0 +1 @@
+OAuth authentication required
--
1.6.2.4
From c85431521c3a8748e53f9aa3627d528b11a564b3 Mon Sep 17 00:00:00 2001
From: Stuart Langridge <stuart langridge canonical com>
Date: Fri, 5 Jun 2009 11:20:04 +0100
Subject: [PATCH 2/2] 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'),
+)
+
--
1.6.2.4
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]