Commit de539d3e by James Henstridge

Rework the OpenID consumer code to be a bit clearer, and make it

possible to set a fixed OpenID server URL.
parent 0936a94a
from django import forms
from django.utils.translation import ugettext as _
from django.conf import settings
from openid.yadis import xri
class OpenIDLoginForm(forms.Form):
openid_url = forms.CharField(max_length=255, widget=forms.widgets.TextInput(attrs={'class': 'required openid'}))
def clean_openid_url(self):
if 'openid_url' in self.cleaned_data:
openid_url = self.cleaned_data['openid_url']
if xri.identifierScheme(openid_url) == 'XRI' and getattr(
settings, 'OPENID_DISALLOW_INAMES', False
):
raise forms.ValidationError(_('i-names are not supported'))
return self.cleaned_data['openid_url']
from django.contrib.auth.models import User
from django.db import models from django.db import models
class Nonce(models.Model): class Nonce(models.Model):
server_url = models.CharField(max_length=2047) server_url = models.CharField(max_length=2047)
timestamp = models.IntegerField() timestamp = models.IntegerField()
salt = models.CharField(max_length=40) salt = models.CharField(max_length=40)
def __unicode__(self): def __unicode__(self):
return u"Nonce: %s, %s" % (self.server_url, self.salt) return u"Nonce: %s, %s" % (self.server_url, self.salt)
class Association(models.Model): class Association(models.Model):
server_url = models.TextField(max_length=2047) server_url = models.TextField(max_length=2047)
handle = models.CharField(max_length=255) handle = models.CharField(max_length=255)
...@@ -16,7 +18,11 @@ class Association(models.Model): ...@@ -16,7 +18,11 @@ class Association(models.Model):
issued = models.IntegerField() issued = models.IntegerField()
lifetime = models.IntegerField() lifetime = models.IntegerField()
assoc_type = models.TextField(max_length=64) assoc_type = models.TextField(max_length=64)
def __unicode__(self): def __unicode__(self):
return u"Association: %s, %s" % (self.server_url, self.handle) return u"Association: %s, %s" % (self.server_url, self.handle)
class UserOpenID(models.Model):
user = models.ForeignKey(User)
claimed_id = models.TextField(max_length=2047)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Redirecting to your OpenID Provider</title>
</head>
<body>
<h1>Redirecting to your OpenID Provider</h1>
{{ form }}
</body>
</html>
...@@ -26,12 +26,15 @@ input.openid { ...@@ -26,12 +26,15 @@ input.openid {
</p> </p>
{% endif %} {% endif %}
<form name="fopenid" action="{{ action }}" method="post"> <form name="fopenid" action="{{ action }}" method="post">
{{ form.next }}
<fieldset> <fieldset>
<legend>{% trans "Sign In Using Your OpenID" %}</legend> <legend>{% trans "Sign In Using Your OpenID" %}</legend>
<div class="form-row"><label for="id_openid_ul">{% trans "OpenId URL :" %}</label><br />{{ form.openid_url }}</div> <div class="form-row"><label for="id_openid_ul">{% trans "OpenId URL :" %}</label><br />{{ form.openid_url }}</div>
<div class="submit-row "><input name="bsignin" type="submit" value="{% trans "Sign in with OPENID" %}"></div> <div class="submit-row "><input name="bsignin" type="submit" value="{% trans "Sign in with OPENID" %}"></div>
{% if next %}
<input type="hidden" name="next" value="{{ next }}" />
{% endif %}
</fieldset> </fieldset>
</form> </form>
</body> </body>
......
from django.conf.urls.defaults import patterns
urlpatterns = patterns('django_openidconsumer.views',
(r'^login$', 'login_begin'),
(r'^complete$', 'login_complete'),
)
import base64 import base64
import md5
import operator
import time import time
from django.db.models.query import Q from django.db.models.query import Q
from django.conf import settings
from openid.association import Association as OIDAssociation from openid.association import Association as OIDAssociation
from openid.store.interface import OpenIDStore from openid.store.interface import OpenIDStore
......
from django.http import HttpResponse, HttpResponseRedirect, get_host import re
from django.shortcuts import render_to_response as render import urllib
from django.template import RequestContext
from django.conf import settings from django.conf import settings
from django.utils.http import urlquote_plus, urlquote from django.contrib.auth import REDIRECT_FIELD_NAME
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.utils.safestring import SafeString
import md5, re, time, urllib from openid.consumer.consumer import (
from openid.consumer.consumer import Consumer, \ Consumer, SUCCESS, CANCEL, FAILURE)
SUCCESS, CANCEL, FAILURE, SETUP_NEEDED
from openid.consumer.discover import DiscoveryFailure from openid.consumer.discover import DiscoveryFailure
from openid.yadis import xri from openid.extensions import sreg
from util import OpenID, DjangoOpenIDStore, from_openid_response from util import DjangoOpenIDStore
from forms import OpenIDLoginForm
from forms import OpenidSigninForm
from django.utils.html import escape
def get_url_host(request):
if request.is_secure():
protocol = 'https'
else:
protocol = 'http'
host = escape(get_host(request))
return '%s://%s' % (protocol, host)
def get_full_url(request):
if request.is_secure():
protocol = 'https'
else:
protocol = 'http'
host = escape(request.META['HTTP_HOST'])
return get_url_host(request) + request.get_full_path()
next_url_re = re.compile('^/[-\w/]+$') next_url_re = re.compile('^/[-\w/]+$')
...@@ -42,122 +28,120 @@ def is_valid_next_url(next): ...@@ -42,122 +28,120 @@ def is_valid_next_url(next):
# path, not a complete URL. # path, not a complete URL.
return bool(next_url_re.match(next)) return bool(next_url_re.match(next))
def begin(request, sreg=None, extension_args=None, redirect_to=None,
on_failure=None): def sanitise_redirect_url(redirect_to):
"""Sanitise the redirection URL."""
on_failure = on_failure or default_on_failure # Light security check -- make sure redirect_to isn't garbage.
extension_args = extension_args or {} if not redirect_to or '//' in redirect_to or ' ' in redirect_to:
redirect_to = settings.LOGIN_REDIRECT_URL
next = '' return redirect_to
if request.GET.get('next'):
next = urllib.urlencode({
'next': request.GET['next'] def make_consumer(request):
}) """Create an OpenID Consumer object for the given Django request."""
# Give the OpenID library its own space in the session object.
session = request.session.setdefault('OPENID', {})
form_signin = OpenidSigninForm(initial={'next':next}) store = DjangoOpenIDStore()
if request.POST: return Consumer(session, store)
form_signin = OpenidSigninForm(request.POST)
if form_signin.is_valid():
consumer = Consumer(request.session, DjangoOpenIDStore()) def render_openid_request(request, openid_request, return_to, trust_root=None,
try: template_name='openid/auth-request.html'):
auth_request = consumer.begin(form_signin.cleaned_data['openid_url']) """Render an OpenID authentication request."""
except DiscoveryFailure: if trust_root is None:
return on_failure(request, "The OpenID was invalid") trust_root = getattr(settings, 'OPENID_TRUST_ROOT',
request.build_absolute_uri('/'))
if sreg:
extension_args['sreg.optional'] = sreg if openid_request.shouldSendRedirect():
redirect_url = openid_request.redirectURL(
trust_root = getattr( trust_root, return_to)
settings, 'OPENID_TRUST_ROOT', get_url_host(request) + '/' return HttpResponseRedirect(redirect_url)
) else:
form_html = openid_request.htmlMarkup(
redirect_to = redirect_to or getattr( trust_root, return_to, form_tag_attrs={'id': 'openid_message'})
settings, 'OPENID_REDIRECT_TO', return render_to_response(
# If not explicitly set, assume current URL with complete/ appended template_name, {'form': SafeString(form_html)},
get_full_url(request).split('?')[0] + 'complete/' context_instance=RequestContext(request))
)
# TODO: add redirect_to in form def parse_openid_response(request):
if not redirect_to.startswith('http://'): """Parse an OpenID response from a Django request."""
redirect_to = get_url_host(request) + redirect_to # Short cut if there is no request parameters.
#if len(request.REQUEST) == 0:
# return None
if 'next' in form_signin.cleaned_data and next != "":
if '?' in redirect_to: current_url = request.build_absolute_uri()
join = '&'
else: consumer = make_consumer(request)
join = '?' return consumer.complete(dict(request.REQUEST.items()), current_url)
redirect_to += join + urllib.urlencode({
'next': form_signin.cleaned_data['next']
}) def login_begin(request, template_name='openid/login.html',
redirect_field_name=REDIRECT_FIELD_NAME):
# Add extension args (for things like simple registration) """Begin an OpenID login request, possibly asking for an identity URL."""
for name, value in extension_args.items(): redirect_to = request.REQUEST.get(redirect_field_name, '')
namespace, key = name.split('.', 1)
auth_request.addExtensionArg(namespace, key, value) # Get the OpenID URL to try. First see if we've been configured
# to use a fixed server URL.
redirect_url = auth_request.redirectURL(trust_root, redirect_to) openid_url = getattr(settings, 'OPENID_SSO_SERVER_URL', None)
return HttpResponseRedirect(redirect_url)
if openid_url is None:
return render('openid_signin.html', { if request.POST:
'form': form_signin, login_form = OpenIDLoginForm(data=request.POST)
'action': request.path, if login_form.is_valid():
'logo': request.path + 'logo/', openid_url = login_form.cleaned_data['openid_url']
'openids': request.session.get('openids', []), else:
}) login_form = OpenIDLoginForm()
def complete(request, on_success=None, on_failure=None): # Invalid or no form data:
on_success = on_success or default_on_success if openid_url is None:
on_failure = on_failure or default_on_failure return render_to_response(template_name, {
'form': login_form,
redirect_field_name: redirect_to
redirect_to = getattr( }, context_instance=RequestContext(request))
settings, 'OPENID_REDIRECT_TO',
get_full_url(request).split('?')[0] error = None
) consumer = make_consumer(request)
try:
consumer = Consumer(request.session, DjangoOpenIDStore()) openid_request = consumer.begin(openid_url)
openid_response = consumer.complete(dict(request.GET.items()), redirect_to) except DiscoveryFailure, exc:
# XXX: make this a proper error page.
return HttpResponse("OpenID discovery error: %s" % (str(exc),))
# Request some user details.
openid_request.addExtension(
sreg.SRegRequest(optional=['email', 'fullname', 'nickname']))
# Construct the request completion URL, including the page we
# should redirect to.
return_to = request.build_absolute_uri(reverse(login_complete))
if redirect_to:
if '?' in return_to:
return_to += '&'
else:
return_to += '?'
return_to += urllib.urlencode({redirect_field_name: redirect_to})
return render_openid_request(request, openid_request, return_to)
def login_complete(request, redirect_field_name=REDIRECT_FIELD_NAME):
redirect_to = request.REQUEST.get(redirect_field_name, '')
openid_response = parse_openid_response(request)
if not openid_response:
return HttpResponseRedirect(sanitise_redirect_url(redirect_to))
if openid_response.status == SUCCESS: if openid_response.status == SUCCESS:
return on_success(request, openid_response.identity_url, openid_response) return HttpResponse("Success: %s")
elif openid_response.status == CANCEL:
return on_failure(request, 'The request was cancelled')
elif openid_response.status == FAILURE: elif openid_response.status == FAILURE:
return on_failure(request, openid_response.message) return HttpResponse("Failure: %s" % openid_response.message)
elif openid_response.status == SETUP_NEEDED: elif openid_response.status == CANCEL:
return on_failure(request, 'Setup needed') return HttpResponse("Cancel")
else: else:
assert False, "Bad openid status: %s" % openid_response.status assert False, (
"Unknown OpenID response type: %r" % openid_response.status)
def default_on_success(request, identity_url, openid_response):
if 'openids' not in request.session.keys():
request.session['openids'] = []
# Eliminate any duplicates
request.session['openids'] = [
o for o in request.session['openids'] if o.openid != identity_url
]
request.session['openids'].append(from_openid_response(openid_response))
next = request.GET.get('next', '').strip()
if not next or not is_valid_next_url(next):
next = getattr(settings, 'OPENID_REDIRECT_NEXT', '/')
return HttpResponseRedirect(next)
def default_on_failure(request, message):
return render('openid_failure.html', {
'message': message
})
def signout(request):
request.session['openids'] = []
next = request.GET.get('next', '/')
if not is_valid_next_url(next):
next = '/'
return HttpResponseRedirect(next)
def logo(request): def logo(request):
return HttpResponse( return HttpResponse(
......
...@@ -3,12 +3,12 @@ import views ...@@ -3,12 +3,12 @@ import views
urlpatterns = patterns('', urlpatterns = patterns('',
(r'^$', views.index), (r'^$', views.index),
(r'^openid/$', 'django_openidconsumer.views.begin'), (r'^openid/$', 'django_openidconsumer.views.login_begin'),
(r'^openid/with-sreg/$', 'django_openidconsumer.views.begin', { #(r'^openid/with-sreg/$', 'django_openidconsumer.views.begin', {
'sreg': 'email,nickname', # 'sreg': 'email,nickname',
'redirect_to': '/openid/complete/', # 'redirect_to': '/openid/complete/',
}), #}),
(r'^openid/complete/$', 'django_openidconsumer.views.complete'), (r'^openid/complete/$', 'django_openidconsumer.views.login_complete'),
(r'^openid/signout/$', 'django_openidconsumer.views.signout'), #(r'^openid/signout/$', 'django_openidconsumer.views.signout'),
(r'^next-works/$', views.next_works), (r'^next-works/$', views.next_works),
) )
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment