[snowy: 1/26] Add django_openid_auth library
- From: Sanford Armstrong <sharm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [snowy: 1/26] Add django_openid_auth library
- Date: Tue, 22 Jun 2010 20:57:05 +0000 (UTC)
commit 903abf482dd124e93091c52d1acbd082cb490e14
Author: Leon Handreke <leon handreke gmail com>
Date: Wed Mar 17 20:42:35 2010 +0100
Add django_openid_auth library
lib/django_openid_auth/LICENSE.txt | 23 +
lib/django_openid_auth/__init__.py | 29 ++
lib/django_openid_auth/admin.py | 90 +++++
lib/django_openid_auth/auth.py | 183 +++++++++
lib/django_openid_auth/forms.py | 87 ++++
lib/django_openid_auth/management/__init__.py | 27 ++
.../management/commands/__init__.py | 27 ++
.../management/commands/openid_cleanup.py | 39 ++
lib/django_openid_auth/models.py | 58 +++
lib/django_openid_auth/store.py | 131 ++++++
lib/django_openid_auth/teams.py | 411 +++++++++++++++++++
.../templates/openid/failure.html | 13 +
lib/django_openid_auth/templates/openid/login.html | 44 ++
lib/django_openid_auth/tests/__init__.py | 37 ++
lib/django_openid_auth/tests/test_store.py | 193 +++++++++
lib/django_openid_auth/tests/test_views.py | 423 ++++++++++++++++++++
lib/django_openid_auth/tests/urls.py | 39 ++
lib/django_openid_auth/urls.py | 36 ++
lib/django_openid_auth/views.py | 239 +++++++++++
settings.py | 5 +
urls.py | 2 +-
21 files changed, 2135 insertions(+), 1 deletions(-)
---
diff --git a/lib/django_openid_auth/LICENSE.txt b/lib/django_openid_auth/LICENSE.txt
new file mode 100644
index 0000000..0e67faf
--- /dev/null
+++ b/lib/django_openid_auth/LICENSE.txt
@@ -0,0 +1,23 @@
+Copyright (C) 2007 Simon Willison
+Copyright (C) 2008-2009 Canonical Ltd.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/django_openid_auth/__init__.py b/lib/django_openid_auth/__init__.py
new file mode 100644
index 0000000..4e5da8c
--- /dev/null
+++ b/lib/django_openid_auth/__init__.py
@@ -0,0 +1,29 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/lib/django_openid_auth/admin.py b/lib/django_openid_auth/admin.py
new file mode 100644
index 0000000..ac7906c
--- /dev/null
+++ b/lib/django_openid_auth/admin.py
@@ -0,0 +1,90 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2008-2009 Canonical Ltd.
+# Copyright (C) 2010 Dave Walker
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.conf import settings
+from django.contrib import admin
+from django_openid_auth.models import Nonce, Association, UserOpenID
+from django_openid_auth.store import DjangoOpenIDStore
+
+
+class NonceAdmin(admin.ModelAdmin):
+ list_display = ('server_url', 'timestamp')
+ actions = ['cleanup_nonces']
+
+ def cleanup_nonces(self, request, queryset):
+ store = DjangoOpenIDStore()
+ count = store.cleanupNonces()
+ self.message_user(request, "%d expired nonces removed" % count)
+ cleanup_nonces.short_description = "Clean up expired nonces"
+
+admin.site.register(Nonce, NonceAdmin)
+
+
+class AssociationAdmin(admin.ModelAdmin):
+ list_display = ('server_url', 'assoc_type')
+ list_filter = ('assoc_type',)
+ search_fields = ('server_url',)
+ actions = ['cleanup_associations']
+
+ def cleanup_associations(self, request, queryset):
+ store = DjangoOpenIDStore()
+ count = store.cleanupAssociations()
+ self.message_user(request, "%d expired associations removed" % count)
+ cleanup_associations.short_description = "Clean up expired associations"
+
+admin.site.register(Association, AssociationAdmin)
+
+
+class UserOpenIDAdmin(admin.ModelAdmin):
+ list_display = ('user', 'claimed_id')
+ search_fields = ('claimed_id',)
+
+admin.site.register(UserOpenID, UserOpenIDAdmin)
+
+
+# Support for allowing openid authentication for /admin (django.contrib.admin)
+if getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False):
+ from django.http import HttpResponseRedirect
+ from django_openid_auth import views
+
+ def _openid_login(self, request, error_message='', extra_context=None):
+ if request.user.is_authenticated():
+ if not request.user.is_staff:
+ return views.render_failure(
+ request, "User %s does not have admin access."
+ % request.user.username)
+ return views.render_failure(
+ request, "Unknown Error: %s" % error_message)
+ else:
+ # Redirect to openid login path,
+ return HttpResponseRedirect(
+ settings.LOGIN_URL + "?next=" + request.get_full_path())
+
+ # Overide the standard admin login form.
+ admin.sites.AdminSite.display_login_form = _openid_login
diff --git a/lib/django_openid_auth/auth.py b/lib/django_openid_auth/auth.py
new file mode 100644
index 0000000..047c63a
--- /dev/null
+++ b/lib/django_openid_auth/auth.py
@@ -0,0 +1,183 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2008-2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""Glue between OpenID and django.contrib.auth."""
+
+__metaclass__ = type
+
+from django.conf import settings
+from django.contrib.auth.models import User, Group
+from openid.consumer.consumer import SUCCESS
+from openid.extensions import sreg
+
+from django_openid_auth import teams
+from django_openid_auth.models import UserOpenID
+
+
+class IdentityAlreadyClaimed(Exception):
+ pass
+
+
+class OpenIDBackend:
+ """A django.contrib.auth backend that authenticates the user based on
+ an OpenID response."""
+
+ def get_user(self, user_id):
+ try:
+ return User.objects.get(pk=user_id)
+ except User.DoesNotExist:
+ return None
+
+ def authenticate(self, **kwargs):
+ """Authenticate the user based on an OpenID response."""
+ # Require that the OpenID response be passed in as a keyword
+ # argument, to make sure we don't match the username/password
+ # calling conventions of authenticate.
+
+ openid_response = kwargs.get('openid_response')
+ if openid_response is None:
+ return None
+
+ if openid_response.status != SUCCESS:
+ return None
+
+ user = None
+ try:
+ user_openid = UserOpenID.objects.get(
+ claimed_id__exact=openid_response.identity_url)
+ except UserOpenID.DoesNotExist:
+ if getattr(settings, 'OPENID_CREATE_USERS', False):
+ user = self.create_user_from_openid(openid_response)
+ else:
+ user = user_openid.user
+
+ if user is None:
+ return None
+
+ if getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False):
+ sreg_response = sreg.SRegResponse.fromSuccessResponse(
+ openid_response)
+ if sreg_response:
+ self.update_user_details_from_sreg(user, sreg_response)
+
+ teams_response = teams.TeamsResponse.fromSuccessResponse(
+ openid_response)
+ if teams_response:
+ self.update_groups_from_teams(user, teams_response)
+
+ return user
+
+ def create_user_from_openid(self, openid_response):
+ sreg_response = sreg.SRegResponse.fromSuccessResponse(openid_response)
+ if sreg_response:
+ nickname = sreg_response.get('nickname', 'openiduser')
+ email = sreg_response.get('email', '')
+ else:
+ nickname = 'openiduser'
+ email = ''
+
+ # Pick a username for the user based on their nickname,
+ # checking for conflicts.
+ i = 1
+ while True:
+ username = nickname
+ if i > 1:
+ username += str(i)
+ try:
+ User.objects.get(username__exact=username)
+ except User.DoesNotExist:
+ break
+ i += 1
+
+ user = User.objects.create_user(username, email, password=None)
+
+ if sreg_response:
+ self.update_user_details_from_sreg(user, sreg_response)
+
+ self.associate_openid(user, openid_response)
+ return user
+
+ def associate_openid(self, user, openid_response):
+ """Associate an OpenID with a user account."""
+ # Check to see if this OpenID has already been claimed.
+ try:
+ user_openid = UserOpenID.objects.get(
+ claimed_id__exact=openid_response.identity_url)
+ except UserOpenID.DoesNotExist:
+ user_openid = UserOpenID(
+ user=user,
+ claimed_id=openid_response.identity_url,
+ display_id=openid_response.endpoint.getDisplayIdentifier())
+ user_openid.save()
+ else:
+ if user_openid.user != user:
+ raise IdentityAlreadyClaimed(
+ "The identity %s has already been claimed"
+ % openid_response.identity_url)
+
+ return user_openid
+
+ def update_user_details_from_sreg(self, user, sreg_response):
+ fullname = sreg_response.get('fullname')
+ if fullname:
+ # Do our best here ...
+ if ' ' in fullname:
+ user.first_name, user.last_name = fullname.rsplit(None, 1)
+ else:
+ user.first_name = u''
+ user.last_name = fullname
+
+ email = sreg_response.get('email')
+ if email:
+ user.email = email
+ user.save()
+
+ def update_groups_from_teams(self, user, teams_response):
+ teams_mapping_auto = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO', False)
+ teams_mapping_auto_blacklist = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST', [])
+ teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
+ if teams_mapping_auto:
+ #ignore teams_mapping. use all django-groups
+ teams_mapping = dict()
+ all_groups = Group.objects.exclude(name__in=teams_mapping_auto_blacklist)
+ for group in all_groups:
+ teams_mapping[group.name] = group.name
+
+ if len(teams_mapping) == 0:
+ return
+
+ current_groups = set(user.groups.filter(
+ name__in=teams_mapping.values()))
+ desired_groups = set(Group.objects.filter(
+ name__in=[teams_mapping[lp_team]
+ for lp_team in teams_response.is_member
+ if lp_team in teams_mapping]))
+ for group in current_groups - desired_groups:
+ user.groups.remove(group)
+ for group in desired_groups - current_groups:
+ user.groups.add(group)
diff --git a/lib/django_openid_auth/forms.py b/lib/django_openid_auth/forms.py
new file mode 100644
index 0000000..d3fd112
--- /dev/null
+++ b/lib/django_openid_auth/forms.py
@@ -0,0 +1,87 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django import forms
+from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.forms import UserChangeForm
+from django.contrib.auth.models import Group
+from django.utils.translation import ugettext as _
+from django.conf import settings
+
+from openid.yadis import xri
+
+
+def teams_new_unicode(self):
+ """
+ Replacement for Group.__unicode__()
+ Calls original method to chain results
+ """
+ name = self.unicode_before_teams()
+ teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
+ group_teams = [t for t in teams_mapping if teams_mapping[t] == self.name]
+ if len(group_teams) > 0:
+ return "%s -> %s" % (name, ", ".join(group_teams))
+ else:
+ return name
+Group.unicode_before_teams = Group.__unicode__
+Group.__unicode__ = teams_new_unicode
+
+
+class UserChangeFormWithTeamRestriction(UserChangeForm):
+ """
+ Extends UserChangeForm to add teams awareness to the user admin form
+ """
+ def clean_groups(self):
+ data = self.cleaned_data['groups']
+ teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
+ known_teams = teams_mapping.values()
+ user_groups = self.instance.groups.all()
+ for group in data:
+ if group.name in known_teams and group not in user_groups:
+ raise forms.ValidationError("""The group %s is mapped to an
+ external team. You cannot assign it manually.""" % group.name)
+ return data
+UserAdmin.form = UserChangeFormWithTeamRestriction
+
+
+class OpenIDLoginForm(forms.Form):
+ openid_identifier = forms.CharField(
+ max_length=255,
+ widget=forms.TextInput(attrs={'class': 'required openid'}))
+
+ def clean_openid_identifier(self):
+ if 'openid_identifier' in self.cleaned_data:
+ openid_identifier = self.cleaned_data['openid_identifier']
+ if xri.identifierScheme(openid_identifier) == 'XRI' and getattr(
+ settings, 'OPENID_DISALLOW_INAMES', False
+ ):
+ raise forms.ValidationError(_('i-names are not supported'))
+ return self.cleaned_data['openid_identifier']
+
+
diff --git a/lib/django_openid_auth/management/__init__.py b/lib/django_openid_auth/management/__init__.py
new file mode 100644
index 0000000..cb7e18f
--- /dev/null
+++ b/lib/django_openid_auth/management/__init__.py
@@ -0,0 +1,27 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/django_openid_auth/management/commands/__init__.py b/lib/django_openid_auth/management/commands/__init__.py
new file mode 100644
index 0000000..cb7e18f
--- /dev/null
+++ b/lib/django_openid_auth/management/commands/__init__.py
@@ -0,0 +1,27 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/django_openid_auth/management/commands/openid_cleanup.py b/lib/django_openid_auth/management/commands/openid_cleanup.py
new file mode 100644
index 0000000..f717dff
--- /dev/null
+++ b/lib/django_openid_auth/management/commands/openid_cleanup.py
@@ -0,0 +1,39 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.core.management.base import NoArgsCommand
+
+from django_openid_auth.store import DjangoOpenIDStore
+
+
+class Command(NoArgsCommand):
+ help = 'Clean up stale OpenID associations and nonces'
+
+ def handle_noargs(self, **options):
+ store = DjangoOpenIDStore()
+ store.cleanup()
diff --git a/lib/django_openid_auth/models.py b/lib/django_openid_auth/models.py
new file mode 100644
index 0000000..a9af98f
--- /dev/null
+++ b/lib/django_openid_auth/models.py
@@ -0,0 +1,58 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.contrib.auth.models import User
+from django.db import models
+
+
+class Nonce(models.Model):
+ server_url = models.CharField(max_length=2047)
+ timestamp = models.IntegerField()
+ salt = models.CharField(max_length=40)
+
+ def __unicode__(self):
+ return u"Nonce: %s, %s" % (self.server_url, self.salt)
+
+
+class Association(models.Model):
+ server_url = models.TextField(max_length=2047)
+ handle = models.CharField(max_length=255)
+ secret = models.TextField(max_length=255) # Stored base64 encoded
+ issued = models.IntegerField()
+ lifetime = models.IntegerField()
+ assoc_type = models.TextField(max_length=64)
+
+ def __unicode__(self):
+ 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, unique=True)
+ display_id = models.TextField(max_length=2047)
diff --git a/lib/django_openid_auth/store.py b/lib/django_openid_auth/store.py
new file mode 100644
index 0000000..dab547f
--- /dev/null
+++ b/lib/django_openid_auth/store.py
@@ -0,0 +1,131 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import base64
+import time
+
+from openid.association import Association as OIDAssociation
+from openid.store.interface import OpenIDStore
+from openid.store.nonce import SKEW
+
+from django_openid_auth.models import Association, Nonce
+
+
+class DjangoOpenIDStore(OpenIDStore):
+ def __init__(self):
+ self.max_nonce_age = 6 * 60 * 60 # Six hours
+
+ def storeAssociation(self, server_url, association):
+ try:
+ assoc = Association.objects.get(
+ server_url=server_url, handle=association.handle)
+ except Association.DoesNotExist:
+ assoc = Association(
+ server_url=server_url,
+ handle=association.handle,
+ secret=base64.encodestring(association.secret),
+ issued=association.issued,
+ lifetime=association.lifetime,
+ assoc_type=association.assoc_type)
+ else:
+ assoc.secret = base64.encodestring(association.secret)
+ assoc.issued = association.issued
+ assoc.lifetime = association.lifetime
+ assoc.assoc_type = association.assoc_type
+ assoc.save()
+
+ def getAssociation(self, server_url, handle=None):
+ assocs = []
+ if handle is not None:
+ assocs = Association.objects.filter(
+ server_url=server_url, handle=handle)
+ else:
+ assocs = Association.objects.filter(server_url=server_url)
+ associations = []
+ expired = []
+ for assoc in assocs:
+ association = OIDAssociation(
+ assoc.handle, base64.decodestring(assoc.secret), assoc.issued,
+ assoc.lifetime, assoc.assoc_type
+ )
+ if association.getExpiresIn() == 0:
+ expired.append(assoc)
+ else:
+ associations.append((association.issued, association))
+ for assoc in expired:
+ assoc.delete()
+ if not associations:
+ return None
+ associations.sort()
+ return associations[-1][1]
+
+ def removeAssociation(self, server_url, handle):
+ assocs = list(Association.objects.filter(
+ server_url=server_url, handle=handle))
+ assocs_exist = len(assocs) > 0
+ for assoc in assocs:
+ assoc.delete()
+ return assocs_exist
+
+ def useNonce(self, server_url, timestamp, salt):
+ if abs(timestamp - time.time()) > SKEW:
+ return False
+
+ try:
+ ononce = Nonce.objects.get(
+ server_url__exact=server_url,
+ timestamp__exact=timestamp,
+ salt__exact=salt)
+ except Nonce.DoesNotExist:
+ ononce = Nonce(
+ server_url=server_url,
+ timestamp=timestamp,
+ salt=salt)
+ ononce.save()
+ return True
+
+ return False
+
+ def cleanupNonces(self, _now=None):
+ if _now is None:
+ _now = int(time.time())
+ expired = Nonce.objects.filter(timestamp__lt=_now - SKEW)
+ count = expired.count()
+ if count:
+ expired.delete()
+ return count
+
+ def cleanupAssociations(self):
+ now = int(time.time())
+ expired = Association.objects.extra(
+ where=['issued + lifetime < %d' % now])
+ count = expired.count()
+ if count:
+ expired.delete()
+ return count
diff --git a/lib/django_openid_auth/teams.py b/lib/django_openid_auth/teams.py
new file mode 100644
index 0000000..b5744e9
--- /dev/null
+++ b/lib/django_openid_auth/teams.py
@@ -0,0 +1,411 @@
+# Launchpad OpenID Teams Extension support for python-openid
+#
+# Copyright (C) 2008-2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""Team membership support for Launchpad.
+
+The primary form of communication between the RP and Launchpad is an
+OpenID authentication request. Our solution is to piggyback a team
+membership test onto this interaction.
+
+As part of an OpenID authentication request, the RP includes the
+following fields:
+
+ openid.ns.lp:
+ An OpenID 2.0 namespace URI for the extension. It is not strictly
+ required for 1.1 requests, but including it is good for forward
+ compatibility.
+
+ It must be set to: http://ns.launchpad.net/2007/openid-teams
+
+ openid.lp.query_membership:
+ A comma separated list of Launchpad team names that the RP is
+ interested in.
+
+As part of the positive assertion OpenID response, the following field
+will be provided:
+
+ openid.ns.lp:
+ (as above)
+
+ openid.lp.is_member:
+ A comma separated list of teams that the user is actually a member
+ of. The list may be limited to those teams mentioned in the
+ request.
+
+ This field must be included in the response signature in order to
+ be considered valid (as the response is bounced through the user's
+ web browser, an unsigned value could be modified).
+
+ since: 2.1.1
+"""
+
+from openid.message import registerNamespaceAlias, \
+ NamespaceAliasRegistrationError
+from openid.extension import Extension
+from openid import oidutil
+
+try:
+ basestring #pylint:disable-msg=W0104
+except NameError:
+ # For Python 2.2
+ basestring = (str, unicode) #pylint:disable-msg=W0622
+
+__all__ = [
+ 'TeamsRequest',
+ 'TeamsResponse',
+ 'ns_uri',
+ 'supportsTeams',
+ ]
+
+ns_uri = 'http://ns.launchpad.net/2007/openid-teams'
+
+try:
+ registerNamespaceAlias(ns_uri, 'lp')
+except NamespaceAliasRegistrationError, e:
+ oidutil.log('registerNamespaceAlias(%r, %r) failed: %s' % (ns_uri,
+ 'lp', str(e),))
+
+def supportsTeams(endpoint):
+ """Does the given endpoint advertise support for Launchpad Teams?
+
+ @param endpoint: The endpoint object as returned by OpenID discovery
+ @type endpoint: openid.consumer.discover.OpenIDEndpoint
+
+ @returns: Whether an lp type was advertised by the endpoint
+ @rtype: bool
+ """
+ return endpoint.usesExtension(ns_uri)
+
+class TeamsNamespaceError(ValueError):
+ """The Launchpad teams namespace was not found and could not
+ be created using the expected name (there's another extension
+ using the name 'lp')
+
+ This is not I{illegal}, for OpenID 2, although it probably
+ indicates a problem, since it's not expected that other extensions
+ will re-use the alias that is in use for OpenID 1.
+
+ If this is an OpenID 1 request, then there is no recourse. This
+ should not happen unless some code has modified the namespaces for
+ the message that is being processed.
+ """
+
+def getTeamsNS(message):
+ """Extract the Launchpad teams namespace URI from the given
+ OpenID message.
+
+ @param message: The OpenID message from which to parse Launchpad
+ teams. This may be a request or response message.
+ @type message: C{L{openid.message.Message}}
+
+ @returns: the lp namespace URI for the supplied message. The
+ message may be modified to define a Launchpad teams
+ namespace.
+ @rtype: C{str}
+
+ @raise ValueError: when using OpenID 1 if the message defines
+ the 'lp' alias to be something other than a Launchpad
+ teams type.
+ """
+ # See if there exists an alias for the Launchpad teams type.
+ alias = message.namespaces.getAlias(ns_uri)
+ if alias is None:
+ # There is no alias, so try to add one. (OpenID version 1)
+ try:
+ message.namespaces.addAlias(ns_uri, 'lp')
+ except KeyError, why:
+ # An alias for the string 'lp' already exists, but it's
+ # defined for something other than Launchpad teams
+ raise TeamsNamespaceError(why[0])
+
+ # we know that ns_uri defined, because it's defined in the
+ # else clause of the loop as well, so disable the warning
+ return ns_uri #pylint:disable-msg=W0631
+
+class TeamsRequest(Extension):
+ """An object to hold the state of a Launchpad teams request.
+
+ @ivar query_membership: A comma separated list of Launchpad team
+ names that the RP is interested in.
+ @type required: [str]
+
+ @group Consumer: requestField, requestTeams, getExtensionArgs, addToOpenIDRequest
+ @group Server: fromOpenIDRequest, parseExtensionArgs
+ """
+
+ ns_alias = 'lp'
+
+ def __init__(self, query_membership=None, lp_ns_uri=ns_uri):
+ """Initialize an empty Launchpad teams request"""
+ Extension.__init__(self)
+ self.query_membership = []
+ self.ns_uri = lp_ns_uri
+
+ if query_membership:
+ self.requestTeams(query_membership)
+
+ # Assign getTeamsNS to a static method so that it can be
+ # overridden for testing.
+ _getTeamsNS = staticmethod(getTeamsNS)
+
+ def fromOpenIDRequest(cls, request):
+ """Create a Launchpad teams request that contains the
+ fields that were requested in the OpenID request with the
+ given arguments
+
+ @param request: The OpenID request
+ @type request: openid.server.CheckIDRequest
+
+ @returns: The newly created Launchpad teams request
+ @rtype: C{L{TeamsRequest}}
+ """
+ self = cls()
+
+ # Since we're going to mess with namespace URI mapping, don't
+ # mutate the object that was passed in.
+ message = request.message.copy()
+
+ self.ns_uri = self._getTeamsNS(message)
+ args = message.getArgs(self.ns_uri)
+ self.parseExtensionArgs(args)
+
+ return self
+
+ fromOpenIDRequest = classmethod(fromOpenIDRequest)
+
+ def parseExtensionArgs(self, args, strict=False):
+ """Parse the unqualified Launchpad teams request
+ parameters and add them to this object.
+
+ This method is essentially the inverse of
+ C{L{getExtensionArgs}}. This method restores the serialized
+ Launchpad teams request fields.
+
+ If you are extracting arguments from a standard OpenID
+ checkid_* request, you probably want to use C{L{fromOpenIDRequest}},
+ which will extract the lp namespace and arguments from the
+ OpenID request. This method is intended for cases where the
+ OpenID server needs more control over how the arguments are
+ parsed than that method provides.
+
+ >>> args = message.getArgs(ns_uri)
+ >>> request.parseExtensionArgs(args)
+
+ @param args: The unqualified Launchpad teams arguments
+ @type args: {str:str}
+
+ @param strict: Whether requests with fields that are not
+ defined in the Launchpad teams specification should be
+ tolerated (and ignored)
+ @type strict: bool
+
+ @returns: None; updates this object
+ """
+ items = args.get('query_membership')
+ if items:
+ for team_name in items.split(','):
+ try:
+ self.requestTeam(team_name, strict)
+ except ValueError:
+ if strict:
+ raise
+
+ def allRequestedTeams(self):
+ """A list of all of the Launchpad teams that were
+ requested.
+
+ @rtype: [str]
+ """
+ return self.query_membership
+
+ def wereTeamsRequested(self):
+ """Have any Launchpad teams been requested?
+
+ @rtype: bool
+ """
+ return bool(self.allRequestedTeams())
+
+ def __contains__(self, team_name):
+ """Was this team in the request?"""
+ return team_name in self.query_membership
+
+ def requestTeam(self, team_name, strict=False):
+ """Request the specified team from the OpenID user
+
+ @param team_name: the unqualified Launchpad team name
+ @type team_name: str
+
+ @param strict: whether to raise an exception when a team is
+ added to a request more than once
+
+ @raise ValueError: when strict is set and the team was
+ requested more than once
+ """
+ if strict:
+ if team_name in self.query_membership:
+ raise ValueError('That team has already been requested')
+ else:
+ if team_name in self.query_membership:
+ return
+
+ self.query_membership.append(team_name)
+
+ def requestTeams(self, query_membership, strict=False):
+ """Add the given list of teams to the request
+
+ @param query_membership: The Launchpad teams request
+ @type query_membership: [str]
+
+ @raise ValueError: when a team requested is not a string
+ or strict is set and a team was requested more than once
+ """
+ if isinstance(query_membership, basestring):
+ raise TypeError('Teams should be passed as a list of '
+ 'strings (not %r)' % (type(query_membership),))
+
+ for team_name in query_membership:
+ self.requestTeam(team_name, strict=strict)
+
+ def getExtensionArgs(self):
+ """Get a dictionary of unqualified Launchpad teams
+ arguments representing this request.
+
+ This method is essentially the inverse of
+ C{L{parseExtensionArgs}}. This method serializes the Launchpad
+ teams request fields.
+
+ @rtype: {str:str}
+ """
+ args = {}
+
+ if self.query_membership:
+ args['query_membership'] = ','.join(self.query_membership)
+
+ return args
+
+class TeamsResponse(Extension):
+ """Represents the data returned in a Launchpad teams response
+ inside of an OpenID C{id_res} response. This object will be
+ created by the OpenID server, added to the C{id_res} response
+ object, and then extracted from the C{id_res} message by the
+ Consumer.
+
+ @ivar data: The Launchpad teams data, an array.
+
+ @ivar ns_uri: The URI under which the Launchpad teams data was
+ stored in the response message.
+
+ @group Server: extractResponse
+ @group Consumer: fromSuccessResponse
+ @group Read-only dictionary interface: keys, iterkeys, items, iteritems,
+ __iter__, get, __getitem__, keys, has_key
+ """
+
+ ns_alias = 'lp'
+
+ def __init__(self, is_member=None, lp_ns_uri=ns_uri):
+ Extension.__init__(self)
+ if is_member is None:
+ self.is_member = []
+ else:
+ self.is_member = is_member
+
+ self.ns_uri = lp_ns_uri
+
+ def addTeam(self, team_name):
+ if team_name not in self.is_member:
+ self.is_member.append(team_name)
+
+ def extractResponse(cls, request, is_member_str):
+ """Take a C{L{TeamsRequest}} and a list of Launchpad
+ team values and create a C{L{TeamsResponse}}
+ object containing that data.
+
+ @param request: The Launchpad teams request object
+ @type request: TeamsRequest
+
+ @param is_member: The Launchpad teams data for this
+ response, as a list of strings.
+ @type is_member: {str:str}
+
+ @returns: a Launchpad teams response object
+ @rtype: TeamsResponse
+ """
+ self = cls()
+ self.ns_uri = request.ns_uri
+ self.is_member = is_member_str.split(',')
+ return self
+
+ extractResponse = classmethod(extractResponse)
+
+ # Assign getTeamsNS to a static method so that it can be
+ # overridden for testing
+ _getTeamsNS = staticmethod(getTeamsNS)
+
+ def fromSuccessResponse(cls, success_response, signed_only=True):
+ """Create a C{L{TeamsResponse}} object from a successful OpenID
+ library response
+ (C{L{openid.consumer.consumer.SuccessResponse}}) response
+ message
+
+ @param success_response: A SuccessResponse from consumer.complete()
+ @type success_response: C{L{openid.consumer.consumer.SuccessResponse}}
+
+ @param signed_only: Whether to process only data that was
+ signed in the id_res message from the server.
+ @type signed_only: bool
+
+ @rtype: TeamsResponse
+ @returns: A Launchpad teams response containing the data
+ that was supplied with the C{id_res} response.
+ """
+ self = cls()
+ self.ns_uri = self._getTeamsNS(success_response.message)
+ if signed_only:
+ args = success_response.getSignedNS(self.ns_uri)
+ else:
+ args = success_response.message.getArgs(self.ns_uri)
+
+ if "is_member" in args:
+ is_member_str = args["is_member"]
+ self.is_member = is_member_str.split(',')
+ #self.is_member = args["is_member"]
+
+ return self
+
+ fromSuccessResponse = classmethod(fromSuccessResponse)
+
+ def getExtensionArgs(self):
+ """Get the fields to put in the Launchpad teams namespace
+ when adding them to an id_res message.
+
+ @see: openid.extension
+ """
+ ns_args = {'is_member': ','.join(self.is_member),}
+ return ns_args
+
diff --git a/lib/django_openid_auth/templates/openid/failure.html b/lib/django_openid_auth/templates/openid/failure.html
new file mode 100644
index 0000000..87839ab
--- /dev/null
+++ b/lib/django_openid_auth/templates/openid/failure.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+ <title>OpenID failed</title>
+</head>
+<body>
+<h1>OpenID failed</h1>
+
+<p>{{ message|escape }}</p>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/lib/django_openid_auth/templates/openid/login.html b/lib/django_openid_auth/templates/openid/login.html
new file mode 100644
index 0000000..712d047
--- /dev/null
+++ b/lib/django_openid_auth/templates/openid/login.html
@@ -0,0 +1,44 @@
+{% load i18n %}
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>Sign in with your OpenID</title>
+<style type="text/css">
+input.openid {
+ background: url({% url openid-logo %}) no-repeat;
+ background-position: 0 50%;
+ padding-left: 16px;
+}
+</style>
+</head>
+<body>
+<h1>Sign in with your OpenID</h1>
+{% if form.errors %}
+<p class="errors">{% trans "Please correct errors below:" %}<br />
+ {% if form.openid_identifier.errors %}
+ <span class="error">{{ form.openid_identifier.errors|join:", " }}</span>
+ {% endif %}
+ {% if form.next.errors %}
+ <span class="error">{{ form.next.errors|join:", " }}</span>
+ {% endif %}
+</p>
+{% endif %}
+<form name="fopenid" action="{{ action }}" method="post">
+ <fieldset>
+ <legend>{% trans "Sign In Using Your OpenID" %}</legend>
+ <div class="form-row">
+ <label for="id_openid_identifier">{% trans "OpenID:" %}</label><br />
+ {{ form.openid_identifier }}
+ </div>
+ <div class="submit-row "><input name="bsignin" type="submit" value="{% trans "Sign in" %}"></div>
+
+ {% if next %}
+ <input type="hidden" name="next" value="{{ next }}" />
+ {% endif %}
+
+ </fieldset>
+</form>
+</body>
+</html>
diff --git a/lib/django_openid_auth/tests/__init__.py b/lib/django_openid_auth/tests/__init__.py
new file mode 100644
index 0000000..a324e69
--- /dev/null
+++ b/lib/django_openid_auth/tests/__init__.py
@@ -0,0 +1,37 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+
+def suite():
+ suite = unittest.TestSuite()
+ for name in ['test_store', 'test_views']:
+ mod = __import__('%s.%s' % (__name__, name), {}, {}, ['suite'])
+ suite.addTest(mod.suite())
+ return suite
diff --git a/lib/django_openid_auth/tests/test_store.py b/lib/django_openid_auth/tests/test_store.py
new file mode 100644
index 0000000..588c8fa
--- /dev/null
+++ b/lib/django_openid_auth/tests/test_store.py
@@ -0,0 +1,193 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import time
+import unittest
+
+from django.test import TestCase
+from openid.association import Association as OIDAssociation
+from openid.store.nonce import SKEW
+
+from django_openid_auth.models import Association, Nonce
+from django_openid_auth.store import DjangoOpenIDStore
+
+
+class OpenIDStoreTests(TestCase):
+
+ def setUp(self):
+ super(OpenIDStoreTests, self).setUp()
+ self.store = DjangoOpenIDStore()
+
+ def test_storeAssociation(self):
+ assoc = OIDAssociation('handle', 'secret', 42, 600, 'HMAC-SHA1')
+ self.store.storeAssociation('server-url', assoc)
+
+ dbassoc = Association.objects.get(
+ server_url='server-url', handle='handle')
+ self.assertEquals(dbassoc.server_url, 'server-url')
+ self.assertEquals(dbassoc.handle, 'handle')
+ self.assertEquals(dbassoc.secret, 'secret'.encode('base-64'))
+ self.assertEquals(dbassoc.issued, 42)
+ self.assertEquals(dbassoc.lifetime, 600)
+ self.assertEquals(dbassoc.assoc_type, 'HMAC-SHA1')
+
+ def test_storeAssociation_update_existing(self):
+ assoc = OIDAssociation('handle', 'secret', 42, 600, 'HMAC-SHA1')
+ self.store.storeAssociation('server-url', assoc)
+
+ # Now update the association with new information.
+ assoc = OIDAssociation('handle', 'secret2', 420, 900, 'HMAC-SHA256')
+ self.store.storeAssociation('server-url', assoc)
+ dbassoc = Association.objects.get(
+ server_url='server-url', handle='handle')
+ self.assertEqual(dbassoc.secret, 'secret2'.encode('base-64'))
+ self.assertEqual(dbassoc.issued, 420)
+ self.assertEqual(dbassoc.lifetime, 900)
+ self.assertEqual(dbassoc.assoc_type, 'HMAC-SHA256')
+
+ def test_getAssociation(self):
+ timestamp = int(time.time())
+ self.store.storeAssociation(
+ 'server-url', OIDAssociation('handle', 'secret', timestamp, 600,
+ 'HMAC-SHA1'))
+ assoc = self.store.getAssociation('server-url', 'handle')
+ self.assertTrue(isinstance(assoc, OIDAssociation))
+
+ self.assertEquals(assoc.handle, 'handle')
+ self.assertEquals(assoc.secret, 'secret')
+ self.assertEquals(assoc.issued, timestamp)
+ self.assertEquals(assoc.lifetime, 600)
+ self.assertEquals(assoc.assoc_type, 'HMAC-SHA1')
+
+ def test_getAssociation_unknown(self):
+ assoc = self.store.getAssociation('server-url', 'unknown')
+ self.assertEquals(assoc, None)
+
+ def test_getAssociation_expired(self):
+ lifetime = 600
+ timestamp = int(time.time()) - 2 * lifetime
+ self.store.storeAssociation(
+ 'server-url', OIDAssociation('handle', 'secret', timestamp,
+ lifetime, 'HMAC-SHA1'))
+
+ # The association is not returned, and is removed from the database.
+ assoc = self.store.getAssociation('server-url', 'handle')
+ self.assertEquals(assoc, None)
+ self.assertRaises(Association.DoesNotExist, Association.objects.get,
+ server_url='server-url', handle='handle')
+
+ def test_getAssociation_no_handle(self):
+ timestamp = int(time.time())
+
+ self.store.storeAssociation(
+ 'server-url', OIDAssociation('handle1', 'secret', timestamp + 1,
+ 600, 'HMAC-SHA1'))
+ self.store.storeAssociation(
+ 'server-url', OIDAssociation('handle2', 'secret', timestamp,
+ 600, 'HMAC-SHA1'))
+
+ # The newest handle is returned.
+ assoc = self.store.getAssociation('server-url', None)
+ self.assertNotEquals(assoc, None)
+ self.assertEquals(assoc.handle, 'handle1')
+ self.assertEquals(assoc.issued, timestamp + 1)
+
+ def test_removeAssociation(self):
+ timestamp = int(time.time())
+ self.store.storeAssociation(
+ 'server-url', OIDAssociation('handle', 'secret', timestamp, 600,
+ 'HMAC-SHA1'))
+ self.assertEquals(
+ self.store.removeAssociation('server-url', 'handle'), True)
+ self.assertEquals(
+ self.store.getAssociation('server-url', 'handle'), None)
+
+ def test_removeAssociation_unknown(self):
+ self.assertEquals(
+ self.store.removeAssociation('server-url', 'unknown'), False)
+
+ def test_useNonce(self):
+ timestamp = time.time()
+ # The nonce can only be used once.
+ self.assertEqual(
+ self.store.useNonce('server-url', timestamp, 'salt'), True)
+ self.assertEqual(
+ self.store.useNonce('server-url', timestamp, 'salt'), False)
+ self.assertEqual(
+ self.store.useNonce('server-url', timestamp, 'salt'), False)
+
+ def test_useNonce_expired(self):
+ timestamp = time.time() - 2 * SKEW
+ self.assertEqual(
+ self.store.useNonce('server-url', timestamp, 'salt'), False)
+
+ def test_useNonce_future(self):
+ timestamp = time.time() + 2 * SKEW
+ self.assertEqual(
+ self.store.useNonce('server-url', timestamp, 'salt'), False)
+
+ def test_cleanupNonces(self):
+ timestamp = time.time()
+ self.assertEqual(
+ self.store.useNonce('server1', timestamp, 'salt1'), True)
+ self.assertEqual(
+ self.store.useNonce('server2', timestamp, 'salt2'), True)
+ self.assertEqual(
+ self.store.useNonce('server3', timestamp, 'salt3'), True)
+ self.assertEqual(Nonce.objects.count(), 3)
+
+ self.assertEqual(
+ self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 3)
+ self.assertEqual(Nonce.objects.count(), 0)
+
+ # The nonces have now been cleared:
+ self.assertEqual(
+ self.store.useNonce('server1', timestamp, 'salt1'), True)
+ self.assertEqual(
+ self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 1)
+ self.assertEqual(
+ self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 0)
+
+ def test_cleanupAssociations(self):
+ timestamp = int(time.time()) - 100
+ self.store.storeAssociation(
+ 'server-url', OIDAssociation('handle1', 'secret', timestamp,
+ 50, 'HMAC-SHA1'))
+ self.store.storeAssociation(
+ 'server-url', OIDAssociation('handle2', 'secret', timestamp,
+ 200, 'HMAC-SHA1'))
+
+ self.assertEquals(self.store.cleanupAssociations(), 1)
+
+ # The second (non-expired) association is left behind.
+ self.assertNotEqual(self.store.getAssociation('server-url', 'handle2'),
+ None)
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromName(__name__)
diff --git a/lib/django_openid_auth/tests/test_views.py b/lib/django_openid_auth/tests/test_views.py
new file mode 100644
index 0000000..0c6c546
--- /dev/null
+++ b/lib/django_openid_auth/tests/test_views.py
@@ -0,0 +1,423 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import cgi
+import re
+import time
+import unittest
+
+from django.conf import settings
+from django.contrib.auth.models import User, Group
+from django.test import TestCase
+from openid.extensions.sreg import SRegRequest, SRegResponse
+from openid.fetchers import (
+ HTTPFetcher, HTTPFetchingError, HTTPResponse, setDefaultFetcher)
+from openid.oidutil import importElementTree
+from openid.server.server import BROWSER_REQUEST_MODES, Server
+from openid.store.memstore import MemoryStore
+
+from django_openid_auth import teams
+from django_openid_auth.models import UserOpenID
+from django_openid_auth.views import sanitise_redirect_url
+
+
+ET = importElementTree()
+
+
+class StubOpenIDProvider(HTTPFetcher):
+
+ def __init__(self, base_url):
+ self.store = MemoryStore()
+ self.identity_url = base_url + 'identity'
+ self.localid_url = base_url + 'localid'
+ self.endpoint_url = base_url + 'endpoint'
+ self.server = Server(self.store, self.endpoint_url)
+ self.last_request = None
+
+ def fetch(self, url, body=None, headers=None):
+ if url == self.identity_url:
+ # Serve an XRDS document directly, which is the
+ return HTTPResponse(
+ url, 200, {'content-type': 'application/xrds+xml'}, """\
+<?xml version="1.0"?>
+<xrds:XRDS
+ xmlns="xri://$xrd*($v*2.0)"
+ xmlns:xrds="xri://$xrds">
+ <XRD>
+ <Service priority="0">
+ <Type>http://specs.openid.net/auth/2.0/signon</Type>
+ <URI>%s</URI>
+ <LocalID>%s</LocalID>
+ </Service>
+ </XRD>
+</xrds:XRDS>
+""" % (self.endpoint_url, self.localid_url))
+ elif url.startswith(self.endpoint_url):
+ # Gather query parameters
+ query = {}
+ if '?' in url:
+ query.update(cgi.parse_qsl(url.split('?', 1)[1]))
+ if body is not None:
+ query.update(cgi.parse_qsl(body))
+ self.last_request = self.server.decodeRequest(query)
+
+ # The browser based requests should not be handled through
+ # the fetcher interface.
+ assert self.last_request.mode not in BROWSER_REQUEST_MODES
+
+ response = self.server.handleRequest(self.last_request)
+ webresponse = self.server.encodeResponse(response)
+ return HTTPResponse(url, webresponse.code, webresponse.headers,
+ webresponse.body)
+ else:
+ raise HTTPFetchingError('unknown URL %s' % url)
+
+ def parseFormPost(self, content):
+ """Parse an HTML form post to create an OpenID request."""
+ # Hack to make the javascript XML compliant ...
+ content = content.replace('i < elements.length',
+ 'i < elements.length')
+ tree = ET.XML(content)
+ form = tree.find('.//form')
+ assert form is not None, 'No form in document'
+ assert form.get('action') == self.endpoint_url, (
+ 'Form posts to %s instead of %s' % (form.get('action'),
+ self.endpoint_url))
+ query = {}
+ for input in form.findall('input'):
+ if input.get('type') != 'hidden':
+ continue
+ query[input.get('name').encode('UTF-8')] = \
+ input.get('value').encode('UTF-8')
+ self.last_request = self.server.decodeRequest(query)
+ return self.last_request
+
+
+class RelyingPartyTests(TestCase):
+ urls = 'django_openid_auth.tests.urls'
+
+ def setUp(self):
+ super(RelyingPartyTests, self).setUp()
+ self.provider = StubOpenIDProvider('http://example.com/')
+ setDefaultFetcher(self.provider, wrap_exceptions=False)
+
+ self.old_login_redirect_url = getattr(settings, 'LOGIN_REDIRECT_URL', '/accounts/profile/')
+ self.old_create_users = getattr(settings, 'OPENID_CREATE_USERS', False)
+ self.old_update_details = getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False)
+ self.old_sso_server_url = getattr(settings, 'OPENID_SSO_SERVER_URL')
+ self.old_teams_map = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
+ self.old_use_as_admin_login = getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False)
+
+ settings.OPENID_CREATE_USERS = False
+ settings.OPENID_UPDATE_DETAILS_FROM_SREG = False
+ settings.OPENID_SSO_SERVER_URL = None
+ settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {}
+ settings.OPENID_USE_AS_ADMIN_LOGIN = False
+
+ def tearDown(self):
+ settings.LOGIN_REDIRECT_URL = self.old_login_redirect_url
+ settings.OPENID_CREATE_USERS = self.old_create_users
+ settings.OPENID_UPDATE_DETAILS_FROM_SREG = self.old_update_details
+ settings.OPENID_SSO_SERVER_URL = self.old_sso_server_url
+ settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = self.old_teams_map
+ settings.OPENID_USE_AS_ADMIN_LOGIN = self.old_use_as_admin_login
+
+ setDefaultFetcher(None)
+ super(RelyingPartyTests, self).tearDown()
+
+ def complete(self, openid_response):
+ """Complete an OpenID authentication request."""
+ webresponse = self.provider.server.encodeResponse(openid_response)
+ self.assertEquals(webresponse.code, 302)
+ redirect_to = webresponse.headers['location']
+ self.assertTrue(redirect_to.startswith(
+ 'http://testserver/openid/complete/'))
+ return self.client.get('/openid/complete/',
+ dict(cgi.parse_qsl(redirect_to.split('?', 1)[1])))
+
+ def test_login(self):
+ user = User.objects.create_user('someuser', 'someone example com')
+ useropenid = UserOpenID(
+ user=user,
+ claimed_id='http://example.com/identity',
+ display_id='http://example.com/identity')
+ useropenid.save()
+
+ # The login form is displayed:
+ response = self.client.get('/openid/login/')
+ self.assertTemplateUsed(response, 'openid/login.html')
+
+ # Posting in an identity URL begins the authentication request:
+ response = self.client.post('/openid/login/',
+ {'openid_identifier': 'http://example.com/identity',
+ 'next': '/getuser/'})
+ self.assertContains(response, 'OpenID transaction in progress')
+
+ openid_request = self.provider.parseFormPost(response.content)
+ self.assertEquals(openid_request.mode, 'checkid_setup')
+ self.assertTrue(openid_request.return_to.startswith(
+ 'http://testserver/openid/complete/'))
+
+ # Complete the request. The user is redirected to the next URL.
+ openid_response = openid_request.answer(True)
+ response = self.complete(openid_response)
+ self.assertRedirects(response, 'http://testserver/getuser/')
+
+ # And they are now logged in:
+ response = self.client.get('/getuser/')
+ self.assertEquals(response.content, 'someuser')
+
+ def test_login_no_next(self):
+ """Logins with no next parameter redirect to LOGIN_REDIRECT_URL."""
+ user = User.objects.create_user('someuser', 'someone example com')
+ useropenid = UserOpenID(
+ user=user,
+ claimed_id='http://example.com/identity',
+ display_id='http://example.com/identity')
+ useropenid.save()
+
+ settings.LOGIN_REDIRECT_URL = '/getuser/'
+ response = self.client.post('/openid/login/',
+ {'openid_identifier': 'http://example.com/identity'})
+ self.assertContains(response, 'OpenID transaction in progress')
+
+ openid_request = self.provider.parseFormPost(response.content)
+ self.assertEquals(openid_request.mode, 'checkid_setup')
+ self.assertTrue(openid_request.return_to.startswith(
+ 'http://testserver/openid/complete/'))
+
+ # Complete the request. The user is redirected to the next URL.
+ openid_response = openid_request.answer(True)
+ response = self.complete(openid_response)
+ self.assertRedirects(
+ response, 'http://testserver' + settings.LOGIN_REDIRECT_URL)
+
+ def test_login_sso(self):
+ settings.OPENID_SSO_SERVER_URL = 'http://example.com/identity'
+ user = User.objects.create_user('someuser', 'someone example com')
+ useropenid = UserOpenID(
+ user=user,
+ claimed_id='http://example.com/identity',
+ display_id='http://example.com/identity')
+ useropenid.save()
+
+ # Requesting the login form immediately begins an
+ # authentication request.
+ response = self.client.get('/openid/login/', {'next': '/getuser/'})
+ self.assertEquals(response.status_code, 200)
+ self.assertContains(response, 'OpenID transaction in progress')
+
+ openid_request = self.provider.parseFormPost(response.content)
+ self.assertEquals(openid_request.mode, 'checkid_setup')
+ self.assertTrue(openid_request.return_to.startswith(
+ 'http://testserver/openid/complete/'))
+
+ # Complete the request. The user is redirected to the next URL.
+ openid_response = openid_request.answer(True)
+ response = self.complete(openid_response)
+ self.assertRedirects(response, 'http://testserver/getuser/')
+
+ # And they are now logged in:
+ response = self.client.get('/getuser/')
+ self.assertEquals(response.content, 'someuser')
+
+ def test_login_create_users(self):
+ settings.OPENID_CREATE_USERS = True
+ # Create a user with the same name as we'll pass back via sreg.
+ User.objects.create_user('someuser', 'someone example com')
+
+ # Posting in an identity URL begins the authentication request:
+ response = self.client.post('/openid/login/',
+ {'openid_identifier': 'http://example.com/identity',
+ 'next': '/getuser/'})
+ self.assertContains(response, 'OpenID transaction in progress')
+
+ # Complete the request, passing back some simple registration
+ # data. The user is redirected to the next URL.
+ openid_request = self.provider.parseFormPost(response.content)
+ sreg_request = SRegRequest.fromOpenIDRequest(openid_request)
+ openid_response = openid_request.answer(True)
+ sreg_response = SRegResponse.extractResponse(
+ sreg_request, {'nickname': 'someuser', 'fullname': 'Some User',
+ 'email': 'foo example com'})
+ openid_response.addExtension(sreg_response)
+ response = self.complete(openid_response)
+ self.assertRedirects(response, 'http://testserver/getuser/')
+
+ # And they are now logged in as a new user (they haven't taken
+ # over the existing "someuser" user).
+ response = self.client.get('/getuser/')
+ self.assertEquals(response.content, 'someuser2')
+
+ # Check the details of the new user.
+ user = User.objects.get(username='someuser2')
+ self.assertEquals(user.first_name, 'Some')
+ self.assertEquals(user.last_name, 'User')
+ self.assertEquals(user.email, 'foo example com')
+
+ def test_login_update_details(self):
+ settings.OPENID_UPDATE_DETAILS_FROM_SREG = True
+ user = User.objects.create_user('testuser', 'someone example com')
+ useropenid = UserOpenID(
+ user=user,
+ claimed_id='http://example.com/identity',
+ display_id='http://example.com/identity')
+ useropenid.save()
+
+ # Posting in an identity URL begins the authentication request:
+ response = self.client.post('/openid/login/',
+ {'openid_identifier': 'http://example.com/identity',
+ 'next': '/getuser/'})
+ self.assertContains(response, 'OpenID transaction in progress')
+
+ # Complete the request, passing back some simple registration
+ # data. The user is redirected to the next URL.
+ openid_request = self.provider.parseFormPost(response.content)
+ sreg_request = SRegRequest.fromOpenIDRequest(openid_request)
+ openid_response = openid_request.answer(True)
+ sreg_response = SRegResponse.extractResponse(
+ sreg_request, {'nickname': 'someuser', 'fullname': 'Some User',
+ 'email': 'foo example com'})
+ openid_response.addExtension(sreg_response)
+ response = self.complete(openid_response)
+ self.assertRedirects(response, 'http://testserver/getuser/')
+
+ # And they are now logged in as testuser (the passed in
+ # nickname has not caused the username to change).
+ response = self.client.get('/getuser/')
+ self.assertEquals(response.content, 'testuser')
+
+ # The user's full name and email have been updated.
+ user = User.objects.get(username='testuser')
+ self.assertEquals(user.first_name, 'Some')
+ self.assertEquals(user.last_name, 'User')
+ self.assertEquals(user.email, 'foo example com')
+
+ def test_login_teams(self):
+ settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {'teamname': 'groupname',
+ 'otherteam': 'othergroup'}
+ user = User.objects.create_user('testuser', 'someone example com')
+ group = Group(name='groupname')
+ group.save()
+ ogroup = Group(name='othergroup')
+ ogroup.save()
+ user.groups.add(ogroup)
+ user.save()
+ useropenid = UserOpenID(
+ user=user,
+ claimed_id='http://example.com/identity',
+ display_id='http://example.com/identity')
+ useropenid.save()
+
+ # Posting in an identity URL begins the authentication request:
+ response = self.client.post('/openid/login/',
+ {'openid_identifier': 'http://example.com/identity',
+ 'next': '/getuser/'})
+ self.assertContains(response, 'OpenID transaction in progress')
+
+ # Complete the request
+ openid_request = self.provider.parseFormPost(response.content)
+ openid_response = openid_request.answer(True)
+ teams_request = teams.TeamsRequest.fromOpenIDRequest(openid_request)
+ teams_response = teams.TeamsResponse.extractResponse(
+ teams_request, 'teamname,some-other-team')
+ openid_response.addExtension(teams_response)
+ response = self.complete(openid_response)
+ self.assertRedirects(response, 'http://testserver/getuser/')
+
+ # And they are now logged in as testuser
+ response = self.client.get('/getuser/')
+ self.assertEquals(response.content, 'testuser')
+
+ # The user's groups have been updated.
+ user = User.objects.get(username='testuser')
+ self.assertTrue(group in user.groups.all())
+ self.assertTrue(ogroup not in user.groups.all())
+
+ def test_login_teams_automapping(self):
+ settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {'teamname': 'groupname',
+ 'otherteam': 'othergroup'}
+ settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = True
+ settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST = ['django-group1', 'django-group2']
+ user = User.objects.create_user('testuser', 'someone example com')
+ group1 = Group(name='django-group1')
+ group1.save()
+ group2 = Group(name='django-group2')
+ group2.save()
+ group3 = Group(name='django-group3')
+ group3.save()
+ user.save()
+ useropenid = UserOpenID(
+ user=user,
+ claimed_id='http://example.com/identity',
+ display_id='http://example.com/identity')
+ useropenid.save()
+
+ # Posting in an identity URL begins the authentication request:
+ response = self.client.post('/openid/login/',
+ {'openid_identifier': 'http://example.com/identity',
+ 'next': '/getuser/'})
+ self.assertContains(response, 'OpenID transaction in progress')
+
+ # Complete the request
+ openid_request = self.provider.parseFormPost(response.content)
+ openid_response = openid_request.answer(True)
+ teams_request = teams.TeamsRequest.fromOpenIDRequest(openid_request)
+
+ self.assertEqual(group1 in user.groups.all(), False)
+ self.assertEqual(group2 in user.groups.all(), False)
+ self.assertTrue(group3 not in user.groups.all())
+
+class HelperFunctionsTest(TestCase):
+ def test_sanitise_redirect_url(self):
+ settings.ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [
+ "example.com", "example.org"]
+ # list of URLs and whether they should be passed or not
+ urls = [
+ ("http://example.com", True),
+ ("http://example.org/", True),
+ ("http://example.org/foo/bar", True),
+ ("http://example.org/foo/bar?baz=quux", True),
+ ("http://example.org:9999/foo/bar?baz=quux", True),
+ ("http://www.example.org/", False),
+ ("http://example.net/foo/bar?baz=quux", False),
+ ("/somewhere/local", True),
+ ("/somewhere/local?url=http://fail.com/bar", True),
+ # An empty path, as seen when no "next" parameter is passed.
+ ("", False),
+ ("/path with spaces", False),
+ ]
+ for url, returns_self in urls:
+ sanitised = sanitise_redirect_url(url)
+ if returns_self:
+ self.assertEqual(url, sanitised)
+ else:
+ self.assertEqual(settings.LOGIN_REDIRECT_URL, sanitised)
+
+def suite():
+ return unittest.TestLoader().loadTestsFromName(__name__)
diff --git a/lib/django_openid_auth/tests/urls.py b/lib/django_openid_auth/tests/urls.py
new file mode 100644
index 0000000..f241550
--- /dev/null
+++ b/lib/django_openid_auth/tests/urls.py
@@ -0,0 +1,39 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.http import HttpResponse
+from django.conf.urls.defaults import *
+
+
+def get_user(request):
+ return HttpResponse(request.user.username)
+
+urlpatterns = patterns('',
+ (r'^getuser/$', get_user),
+ (r'^openid/', include('django_openid_auth.urls')),
+)
diff --git a/lib/django_openid_auth/urls.py b/lib/django_openid_auth/urls.py
new file mode 100644
index 0000000..6df0561
--- /dev/null
+++ b/lib/django_openid_auth/urls.py
@@ -0,0 +1,36 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('django_openid_auth.views',
+ (r'^login/$', 'login_begin'),
+ (r'^complete/$', 'login_complete'),
+ url(r'^logo.gif$', 'logo', name='openid-logo'),
+)
diff --git a/lib/django_openid_auth/views.py b/lib/django_openid_auth/views.py
new file mode 100644
index 0000000..5488cf2
--- /dev/null
+++ b/lib/django_openid_auth/views.py
@@ -0,0 +1,239 @@
+# django-openid-auth - OpenID integration for django.contrib.auth
+#
+# Copyright (C) 2007 Simon Willison
+# Copyright (C) 2008-2009 Canonical Ltd.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import re
+import urllib
+from urlparse import urlsplit
+
+from django.conf import settings
+from django.contrib.auth import (
+ REDIRECT_FIELD_NAME, authenticate, login as auth_login)
+from django.contrib.auth.models import Group
+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.template.loader import render_to_string
+
+from openid.consumer.consumer import (
+ Consumer, SUCCESS, CANCEL, FAILURE)
+from openid.consumer.discover import DiscoveryFailure
+from openid.extensions import sreg
+
+from django_openid_auth import teams
+from django_openid_auth.forms import OpenIDLoginForm
+from django_openid_auth.store import DjangoOpenIDStore
+
+
+next_url_re = re.compile('^/[-\w/]+$')
+
+def is_valid_next_url(next):
+ # When we allow this:
+ # /openid/?next=/welcome/
+ # For security reasons we want to restrict the next= bit to being a local
+ # path, not a complete URL.
+ return bool(next_url_re.match(next))
+
+
+def sanitise_redirect_url(redirect_to):
+ """Sanitise the redirection URL."""
+ # Light security check -- make sure redirect_to isn't garbage.
+ is_valid = True
+ if not redirect_to or ' ' in redirect_to:
+ is_valid = False
+ elif '//' in redirect_to:
+ # Allow the redirect URL to be external if it's a permitted domain
+ allowed_domains = getattr(settings,
+ "ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS", [])
+ s, netloc, p, q, f = urlsplit(redirect_to)
+ # allow it if netloc is blank or if the domain is allowed
+ if netloc:
+ # a domain was specified. Is it an allowed domain?
+ if netloc.find(":") != -1:
+ netloc, _ = netloc.split(":", 1)
+ if netloc not in allowed_domains:
+ is_valid = False
+
+ # If the return_to URL is not valid, use the default.
+ if not is_valid:
+ redirect_to = settings.LOGIN_REDIRECT_URL
+
+ return redirect_to
+
+
+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', {})
+ store = DjangoOpenIDStore()
+ return Consumer(session, store)
+
+
+def render_openid_request(request, openid_request, return_to, trust_root=None):
+ """Render an OpenID authentication request."""
+ if trust_root is None:
+ trust_root = getattr(settings, 'OPENID_TRUST_ROOT',
+ request.build_absolute_uri('/'))
+
+ if openid_request.shouldSendRedirect():
+ redirect_url = openid_request.redirectURL(
+ trust_root, return_to)
+ return HttpResponseRedirect(redirect_url)
+ else:
+ form_html = openid_request.htmlMarkup(
+ trust_root, return_to, form_tag_attrs={'id': 'openid_message'})
+ return HttpResponse(form_html, content_type='text/html;charset=UTF-8')
+
+
+def render_failure(request, message, status=403):
+ """Render an error page to the user."""
+ data = render_to_string(
+ 'openid/failure.html', dict(message=message),
+ context_instance=RequestContext(request))
+ return HttpResponse(data, status=status)
+
+
+def parse_openid_response(request):
+ """Parse an OpenID response from a Django request."""
+ # Short cut if there is no request parameters.
+ #if len(request.REQUEST) == 0:
+ # return None
+
+ current_url = request.build_absolute_uri()
+
+ consumer = make_consumer(request)
+ return consumer.complete(dict(request.REQUEST.items()), current_url)
+
+
+def login_begin(request, template_name='openid/login.html',
+ redirect_field_name=REDIRECT_FIELD_NAME):
+ """Begin an OpenID login request, possibly asking for an identity URL."""
+ redirect_to = request.REQUEST.get(redirect_field_name, '')
+
+ # Get the OpenID URL to try. First see if we've been configured
+ # to use a fixed server URL.
+ openid_url = getattr(settings, 'OPENID_SSO_SERVER_URL', None)
+
+ if openid_url is None:
+ if request.POST:
+ login_form = OpenIDLoginForm(data=request.POST)
+ if login_form.is_valid():
+ openid_url = login_form.cleaned_data['openid_identifier']
+ else:
+ login_form = OpenIDLoginForm()
+
+ # Invalid or no form data:
+ if openid_url is None:
+ return render_to_response(template_name, {
+ 'form': login_form,
+ redirect_field_name: redirect_to
+ }, context_instance=RequestContext(request))
+
+ error = None
+ consumer = make_consumer(request)
+ try:
+ openid_request = consumer.begin(openid_url)
+ except DiscoveryFailure, exc:
+ return render_failure(
+ request, "OpenID discovery error: %s" % (str(exc),), status=500)
+
+ # Request some user details.
+ openid_request.addExtension(
+ sreg.SRegRequest(optional=['email', 'fullname', 'nickname']))
+
+ # Request team info
+ teams_mapping_auto = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO', False)
+ teams_mapping_auto_blacklist = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST', [])
+ launchpad_teams = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
+ if teams_mapping_auto:
+ #ignore launchpad teams. use all django-groups
+ launchpad_teams = dict()
+ all_groups = Group.objects.exclude(name__in=teams_mapping_auto_blacklist)
+ for group in all_groups:
+ launchpad_teams[group.name] = group.name
+
+ if launchpad_teams:
+ openid_request.addExtension(teams.TeamsRequest(launchpad_teams.keys()))
+
+ # 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 render_failure(
+ request, 'This is an OpenID relying party endpoint.')
+
+ if openid_response.status == SUCCESS:
+ user = authenticate(openid_response=openid_response)
+ if user is not None:
+ if user.is_active:
+ auth_login(request, user)
+ return HttpResponseRedirect(sanitise_redirect_url(redirect_to))
+ else:
+ return render_failure(request, 'Disabled account')
+ else:
+ return render_failure(request, 'Unknown user')
+ elif openid_response.status == FAILURE:
+ return render_failure(
+ request, 'OpenID authentication failed: %s' %
+ openid_response.message)
+ elif openid_response.status == CANCEL:
+ return render_failure(request, 'Authentication cancelled')
+ else:
+ assert False, (
+ "Unknown OpenID response type: %r" % openid_response.status)
+
+
+def logo(request):
+ return HttpResponse(
+ OPENID_LOGO_BASE_64.decode('base64'), mimetype='image/gif'
+ )
+
+# Logo from http://openid.net/login-bg.gif
+# Embedded here for convenience; you should serve this as a static file
+OPENID_LOGO_BASE_64 = """
+R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d
+3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA
+AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg
+EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD
+Fzk0lpcjIQA7
+"""
diff --git a/settings.py b/settings.py
index 5ab267b..e3cfc84 100644
--- a/settings.py
+++ b/settings.py
@@ -115,6 +115,7 @@ INSTALLED_APPS = (
'core',
'api',
'accounts',
+ 'django_openid_auth',
'notes',
# System apps
@@ -143,7 +144,11 @@ ACCOUNT_ACTIVATION_DAYS = 15
AUTH_PROFILE_MODULE = 'accounts.UserProfile'
+OPENID_CREATE_USERS = True
+OPENID_UPDATE_DETAILS_FROM_SREG = True
+
LOGIN_REDIRECT_URL = '/'
+LOGIN_URL = '/openid/login/'
# local_settings.py can be used to override environment-specific settings
# like database and email that differ between development and production.
diff --git a/urls.py b/urls.py
index 46fbae4..a995afc 100644
--- a/urls.py
+++ b/urls.py
@@ -28,7 +28,7 @@ urlpatterns = patterns('',
(r'^api/', include('snowy.api.urls')),
(r'^accounts/', include('snowy.accounts.urls')),
-
+ (r'^openid/', include('django_openid_auth.urls')),
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
(r'^admin/', include(admin.site.urls)),
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]