Commit 5a85c704 by Max Rothman
parents
Copyright (C) 2007 Simon Willison
Copyright (C) 2008-2010 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.
include Makefile
include MANIFEST.in
include LICENSE.txt
include README.txt
include TODO.txt
recursive-include django_openid_auth/templates *.html
recursive-include example_consumer *.py
check:
PYTHONPATH=$(shell pwd) python manage.py test --verbosity=2 django_openid_auth
run-example-consumer:
PYTHONPATH=$(shell pwd) python manage.py syncdb --migrate
PYTHONPATH=$(shell pwd) python manage.py runserver
.PHONY: check run-example-consumer
= Django OpenID Authentication Support =
This package provides integration between Django's authentication
system and OpenID authentication. It also includes support for using
a fixed OpenID server endpoint, which can be useful when implementing
single signon systems.
== Basic Installation ==
0. Install the Jan Rain Python OpenID library. It can be found at:
http://openidenabled.com/python-openid/
It can also be found in most Linux distributions packaged as
"python-openid". You will need version 2.2.0 or later.
1. If you are using Django 1.6, configure your project to use the
pickle based session serializer:
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
2. Add 'django_openid_auth' to INSTALLED_APPS for your application.
At a minimum, you'll need the following in there:
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django_openid_auth',
)
3. Add 'django_auth_openid.auth.OpenIDBackend' to
AUTHENTICATION_BACKENDS. This should be in addition to the
default ModelBackend:
AUTHENTICATION_BACKENDS = (
'django_openid_auth.auth.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
)
4. To create users automatically when a new OpenID is used, add the
following to the settings:
OPENID_CREATE_USERS = True
5. To have user details updated from OpenID Simple Registration or
Attribute Exchange extension data each time they log in, add the
following:
OPENID_UPDATE_DETAILS_FROM_SREG = True
6. Hook up the login URLs to your application's urlconf with
something like:
urlpatterns = patterns('',
...
(r'^openid/', include('django_openid_auth.urls')),
...
)
7. Configure the LOGIN_URL and LOGIN_REDIRECT_URL appropriately for
your site:
LOGIN_URL = '/openid/login/'
LOGIN_REDIRECT_URL = '/'
This will allow pages that use the standard @login_required
decorator to use the OpenID login page.
8. Rerun "python manage.py syncdb" to add the UserOpenID table to
your database.
== Configuring Single Sign-On ==
If you only want to accept identities from a single OpenID server and
that server implemnts OpenID 2.0 identifier select mode, add the
following setting to your app:
OPENID_SSO_SERVER_URL = 'server-endpoint-url'
With this setting enabled, the user will not be prompted to enter
their identity URL, and instead an OpenID authentication request will
be started with the given server URL.
As an example, to use Launchpad accounts for SSO, you'd use:
OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
== Launchpad Teams Support ==
This library supports the Launchpad Teams OpenID extension. Using
this feature, it is possible to map Launchpad team memberships to
Django group memberships. It can be configured with:
OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
OPENID_LAUNCHPAD_TEAMS_MAPPING = {
'launchpad-team-1': 'django-group-1',
'launchpad-team-2': 'django-group-2',
}
When a user logs in, they will be added or removed from the relevant
teams listed in the mapping.
If you have already django-groups and want to map these groups automatically, you can use the OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO variable in your settings.py file.
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = True
If you use OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO, the variable OPENID_LAUNCHPAD_TEAMS_MAPPING will be ignored.
If you want to exclude some groups from the auto mapping, use OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST. This variable has only an effect if OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO is True.
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST = ['django-group1', 'django-group2']
If you want to restrict login to a subset of teams, so that only members of
those teams can login, you can use the OPENID_LAUNCHPAD_TEAMS_REQUIRED variable
in your settings.py file.
OPENID_LAUNCHPAD_TEAMS_REQUIRED = ['launchpad-team-1', 'launchpad-team-2']
Some accounts can be whitelisted from this required team restriction. This is
specifically useful for doing testing. In order to whitelist an account from
the required teams restriction you can use the OPENID_EMAIL_WHITELIST_REGEXP_LIST setting.
As an example, the following value
OPENID_EMAIL_WHITELIST_REGEXP_LIST = ['foo(\+[^@]*)?@foo.com']
would whitelist users with the following emails (and other matching the regular expression)
from being in a required team:
foo@foo.com
foo+bar@foo.com
== External redirect domains ==
By default, redirecting back to an external URL after auth is forbidden. To permit redirection to external URLs on a separate domain, define ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS in your settings.py file as a list of permitted domains:
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['example.com', 'example.org']
and redirects to external URLs on those domains will additionally be permitted.
== Use as /admin (django.admin.contrib) login ==
If you require openid authentication into the admin application, add the following setting:
OPENID_USE_AS_ADMIN_LOGIN = True
It is worth noting that a user needs to be be marked as a "staff user" to be able to access the admin interface. A new openid user will not normally be a "staff user".
The easiest way to resolve this is to use traditional authentication (OPENID_USE_AS_ADMIN_LOGIN = False) to sign in as your first user with a password and authorise your
openid user to be staff.
== Change Django usernames if the nickname changes on the provider ==
If you want your Django username to change when a user updates the nickname on their provider, add the following setting:
OPENID_FOLLOW_RENAMES = True
If the new nickname is available as a Django username, the user is renamed.
Otherwise the user will be renamed to nickname+i for an incrememnting value of i until no conflict occurs.
If the user has already been renamed to nickname+1 due to a conflict, and the nickname is still not available, the user will keep their existing username.
== Require a valid nickname ==
If you must have a valid, unique nickname in order to create a user accont, add the following setting:
OPENID_STRICT_USERNAMES = True
This will cause an OpenID login attempt to fail if the provider does not return a 'nickname' (username) for the user, or if the nickname conflicts with an existing user with a different openid identiy url.
Without this setting, logins without a nickname will be given the username 'openiduser', and upon conflicts with existing username, an incrementing number will be appended to the username until it is unique.
== Require Physical Multi-Factor Authentication ==
If your users should use a physical multi-factor authentication method, such as RSA tokens or YubiKey, add the following setting:
OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True
If the user's OpenID provider supports the PAPE extension and provides the Physical Multifactor authentication policy, this will
cause the OpenID login to fail if the user does not provide valid physical authentication to the provider.
== Override Login Failure Handling ==
You can optionally provide your own handler for login failures by adding the following setting:
OPENID_RENDER_FAILURE = failure_handler_function
Where failure_handler_function is a function reference that will take the following parameters:
def failure_handler_function(request, message, status=None, template_name=None, exception=None)
This function must return a Django.http.HttpResponse instance.
== Use the user's email for suggested usernames ==
You can optionally strip out non-alphanumeric characters from the user's email
to generate a preferred username, if the server doesn't provide nick
information, by setting the following setting:
OPENID_USE_EMAIL_FOR_USERNAME = True
Otherwise, and by default, if the server omits nick information and a user is
created it'll receive a username 'openiduser' + a number.
Consider also the OPENID_STRICT_USERNAMES setting (see ``Require a valid nickname``)
== Specify Valid Account Verification Schemes ==
When using OpenID Attribute Exchange, the attribute URI
http://ns.login.ubuntu.com/2013/validation/account is included in the request.
OpenID Providers that support this extension can reply with a token
representing what measures they have taken to validate the e-mail address
included in the response. To change the list of schemes acceptable for your
purposes you can change the setting:
OPENID_VALID_VERIFICATION_SCHEMES = {
None: (),
'http://example.com/': ('token_via_email',),
}
The element with the None key specifies a list of verification schemes that
will be accepted as trusted from OpenID Providers that we haven't explicitly
configured. These are, almost by definition, untrusted, so it is strongly
recommended that this list remain empty. Verified accounts will be granted the
django_openid_auth.account_verified permission, which can be checked using
user.has_perm() and the perms RequestContext attribute in the normal way.
N.B. Users of the South migration framework will need to provide a data
migration to create the permission when upgrading django-openid-auth, due to a
known issue in South. See http://south.aeracode.org/ticket/211 for details.
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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.
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2008-2013 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 __future__ import unicode_literals
from urllib import urlencode
from urlparse import parse_qsl, urlparse
from django.conf import settings
from django.contrib import admin
from django.http import HttpResponseRedirect
from django_openid_auth import views
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):
raw_id_fields = ('user',)
list_display = ('user', 'claimed_id')
search_fields = ('claimed_id',)
admin.site.register(UserOpenID, UserOpenIDAdmin)
# store a reference to the original admin login
original_admin_login = admin.sites.AdminSite.login
def _openid_login(instance, request, error_message='', extra_context=None):
# Support for allowing openid authentication for /admin
# (django.contrib.admin)
if not getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False):
return original_admin_login(
instance, request, extra_context=extra_context)
if not request.user.is_authenticated():
# Redirect to openid login path,
_, _, path, _, query, _ = urlparse(request.get_full_path())
qs = dict(parse_qsl(query))
qs.setdefault('next', path)
return HttpResponseRedirect(
settings.LOGIN_URL + "?" + urlencode(qs))
if not request.user.is_staff:
return views.default_render_failure(
request, "User %s does not have admin/staff access."
% request.user.username)
# No error message was supplied
assert error_message, "Unknown Error: %s" % error_message
# Overide the standard admin login form.
admin.sites.AdminSite.login = _openid_login
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2008-2013 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."""
from __future__ import unicode_literals
__metaclass__ = type
import re
from django.conf import settings
from django.contrib.auth.models import User, Group, Permission
from openid.consumer.consumer import SUCCESS
from openid.extensions import ax, sreg, pape
from django_openid_auth import teams
from django_openid_auth.models import UserOpenID
from django_openid_auth.exceptions import (
IdentityAlreadyClaimed,
DuplicateUsernameViolation,
MissingUsernameViolation,
MissingPhysicalMultiFactor,
RequiredAttributeNotReturned,
)
class OpenIDBackend:
"""A django.contrib.auth backend that authenticates the user based on
an OpenID response."""
supports_object_permissions = False
supports_anonymous_user = True
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):
details = self._extract_user_details(openid_response)
self.update_user_details(user, details, openid_response)
if getattr(settings, 'OPENID_PHYSICAL_MULTIFACTOR_REQUIRED', False):
pape_response = pape.Response.fromSuccessResponse(openid_response)
key = pape.AUTH_MULTI_FACTOR_PHYSICAL
if (pape_response is None or
key not in pape_response.auth_policies):
raise MissingPhysicalMultiFactor()
teams_response = teams.TeamsResponse.fromSuccessResponse(
openid_response)
if teams_response:
self.update_groups_from_teams(user, teams_response)
self.update_staff_status_from_teams(user, teams_response)
teams_required = getattr(settings,
'OPENID_LAUNCHPAD_TEAMS_REQUIRED', [])
if teams_required:
teams_mapping = self.get_teams_mapping()
groups_required = [group for team, group in teams_mapping.items()
if team in teams_required]
matches = set(groups_required).intersection(
user.groups.values_list('name', flat=True))
if not matches:
name = 'OPENID_EMAIL_WHITELIST_REGEXP_LIST'
whitelist_regexp_list = getattr(settings, name, [])
for pattern in whitelist_regexp_list:
if re.match(pattern, user.email):
return user
return None
return user
def _extract_user_details(self, openid_response):
email = fullname = first_name = last_name = nickname = None
verified = 'no'
sreg_response = sreg.SRegResponse.fromSuccessResponse(openid_response)
if sreg_response:
email = sreg_response.get('email')
fullname = sreg_response.get('fullname')
nickname = sreg_response.get('nickname')
# If any attributes are provided via Attribute Exchange, use
# them in preference.
fetch_response = ax.FetchResponse.fromSuccessResponse(openid_response)
if fetch_response:
# The myOpenID provider advertises AX support, but uses
# attribute names from an obsolete draft of the
# specification. We check for them first so the common
# names take precedence.
email = fetch_response.getSingle(
'http://schema.openid.net/contact/email', email)
fullname = fetch_response.getSingle(
'http://schema.openid.net/namePerson', fullname)
nickname = fetch_response.getSingle(
'http://schema.openid.net/namePerson/friendly', nickname)
email = fetch_response.getSingle(
'http://axschema.org/contact/email', email)
fullname = fetch_response.getSingle(
'http://axschema.org/namePerson', fullname)
first_name = fetch_response.getSingle(
'http://axschema.org/namePerson/first', first_name)
last_name = fetch_response.getSingle(
'http://axschema.org/namePerson/last', last_name)
nickname = fetch_response.getSingle(
'http://axschema.org/namePerson/friendly', nickname)
verified = fetch_response.getSingle(
'http://ns.login.ubuntu.com/2013/validation/account', verified)
if fullname and not (first_name or last_name):
# Django wants to store first and last names separately,
# so we do our best to split the full name.
fullname = fullname.strip()
split_names = fullname.rsplit(None, 1)
if len(split_names) == 2:
first_name, last_name = split_names
else:
first_name = ''
last_name = fullname
verification_scheme_map = getattr(
settings, 'OPENID_VALID_VERIFICATION_SCHEMES', {})
valid_schemes = verification_scheme_map.get(
openid_response.endpoint.server_url,
verification_scheme_map.get(None, ()))
verified = (verified in valid_schemes)
return dict(email=email, nickname=nickname, account_verified=verified,
first_name=first_name, last_name=last_name)
def _get_preferred_username(self, nickname, email):
if nickname:
return nickname
if email and getattr(settings, 'OPENID_USE_EMAIL_FOR_USERNAME', False):
suggestion = ''.join([x for x in email if x.isalnum()])
if suggestion:
return suggestion
return 'openiduser'
def _get_available_username(self, nickname, identity_url):
# If we're being strict about usernames, throw an error if we didn't
# get one back from the provider
if getattr(settings, 'OPENID_STRICT_USERNAMES', False):
if nickname is None or nickname == '':
raise MissingUsernameViolation()
# If we don't have a nickname, and we're not being strict, use a
# default
nickname = nickname or 'openiduser'
# See if we already have this nickname assigned to a username
try:
User.objects.get(username__exact=nickname)
except User.DoesNotExist:
# No conflict, we can use this nickname
return nickname
# Check if we already have nickname+i for this identity_url
try:
user_openid = UserOpenID.objects.get(
claimed_id__exact=identity_url,
user__username__startswith=nickname)
# No exception means we have an existing user for this identity
# that starts with this nickname.
# If they are an exact match, the user already exists and hasn't
# changed their username, so continue to use it
if nickname == user_openid.user.username:
return nickname
# It is possible we've had to assign them to nickname+i already.
oid_username = user_openid.user.username
if len(oid_username) > len(nickname):
try:
# check that it ends with a number
int(oid_username[len(nickname):])
return oid_username
except ValueError:
# username starts with nickname, but isn't nickname+#
pass
except UserOpenID.DoesNotExist:
# No user associated with this identity_url
pass
if getattr(settings, 'OPENID_STRICT_USERNAMES', False):
if User.objects.filter(username__exact=nickname).count() > 0:
raise DuplicateUsernameViolation(
"The username (%s) with which you tried to log in is "
"already in use for a different account." % nickname)
# Pick a username for the user based on their nickname,
# checking for conflicts. Start with number of existing users who's
# username starts with this nickname to avoid having to iterate over
# all of the existing ones.
i = User.objects.filter(username__startswith=nickname).count() + 1
while True:
username = nickname
if i > 1:
username += str(i)
try:
User.objects.get(username__exact=username)
except User.DoesNotExist:
break
i += 1
return username
def create_user_from_openid(self, openid_response):
details = self._extract_user_details(openid_response)
required_attrs = getattr(settings, 'OPENID_SREG_REQUIRED_FIELDS', [])
if getattr(settings, 'OPENID_STRICT_USERNAMES', False):
required_attrs.append('nickname')
for required_attr in required_attrs:
if required_attr not in details or not details[required_attr]:
raise RequiredAttributeNotReturned(
"An attribute required for logging in was not "
"returned ({0}).".format(required_attr))
nickname = self._get_preferred_username(
details['nickname'], details['email'])
email = details['email'] or ''
username = self._get_available_username(
nickname, openid_response.identity_url)
user = User.objects.create_user(username, email, password=None)
self.associate_openid(user, openid_response)
self.update_user_details(user, details, 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(self, user, details, openid_response):
updated = False
if details['first_name']:
user.first_name = details['first_name'][:30]
updated = True
if details['last_name']:
user.last_name = details['last_name'][:30]
updated = True
if details['email']:
user.email = details['email']
updated = True
if getattr(settings, 'OPENID_FOLLOW_RENAMES', False):
user.username = self._get_available_username(
details['nickname'], openid_response.identity_url)
updated = True
account_verified = details.get('account_verified', None)
if (account_verified is not None):
permission = Permission.objects.get(codename='account_verified')
perm_label = '%s.%s' % (permission.content_type.app_label,
permission.codename)
if account_verified and not user.has_perm(perm_label):
user.user_permissions.add(permission)
elif not account_verified and user.has_perm(perm_label):
user.user_permissions.remove(permission)
if updated:
user.save()
def get_teams_mapping(self):
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
return teams_mapping
def update_groups_from_teams(self, user, teams_response):
teams_mapping = self.get_teams_mapping()
if len(teams_mapping) == 0:
return
mapping = [
teams_mapping[lp_team] for lp_team in teams_response.is_member
if lp_team in teams_mapping]
current_groups = set(
user.groups.filter(name__in=teams_mapping.values()))
desired_groups = set(Group.objects.filter(name__in=mapping))
for group in current_groups - desired_groups:
user.groups.remove(group)
for group in desired_groups - current_groups:
user.groups.add(group)
def update_staff_status_from_teams(self, user, teams_response):
if not hasattr(settings, 'OPENID_LAUNCHPAD_STAFF_TEAMS'):
return
staff_teams = getattr(settings, 'OPENID_LAUNCHPAD_STAFF_TEAMS', [])
user.is_staff = False
for lp_team in teams_response.is_member:
if lp_team in staff_teams:
user.is_staff = True
break
user.save()
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2008-2013 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.
"""Exception classes thrown by OpenID Authentication and Validation."""
from __future__ import unicode_literals
class DjangoOpenIDException(Exception):
pass
class RequiredAttributeNotReturned(DjangoOpenIDException):
pass
class IdentityAlreadyClaimed(DjangoOpenIDException):
def __init__(self, message=None):
if message is None:
self.message = (
"Another user already exists for your selected OpenID")
else:
self.message = message
class DuplicateUsernameViolation(DjangoOpenIDException):
def __init__(self, message=None):
if message is None:
self.message = "Your desired username is already being used."
else:
self.message = message
class MissingUsernameViolation(DjangoOpenIDException):
def __init__(self, message=None):
if message is None:
self.message = "No nickname given for your account."
else:
self.message = message
class MissingPhysicalMultiFactor(DjangoOpenIDException):
def __init__(self, message=None):
if message is None:
self.message = (
"Login requires physical multi-factor authentication.")
else:
self.message = message
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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 __future__ import unicode_literals
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']
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 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.
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 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.
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 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 __future__ import unicode_literals
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()
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Association',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('server_url', models.TextField(max_length=2047)),
('handle', models.CharField(max_length=255)),
('secret', models.TextField(max_length=255)),
('issued', models.IntegerField()),
('lifetime', models.IntegerField()),
('assoc_type', models.TextField(max_length=64)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Nonce',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('server_url', models.CharField(max_length=2047)),
('timestamp', models.IntegerField()),
('salt', models.CharField(max_length=40)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='UserOpenID',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('claimed_id', models.TextField(unique=True, max_length=2047)),
('display_id', models.TextField(max_length=2047)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
options={
'permissions': (('account_verified', 'The OpenID has been verified'),),
},
bases=(models.Model,),
),
]
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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 __future__ import unicode_literals
from django.db import models
from django.contrib.auth.models import Permission, User
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)
class Meta:
permissions = (
('account_verified', 'The OpenID has been verified'),
)
def delete(self, using=None):
permission = Permission.objects.get(codename='account_verified')
self.user.user_permissions.remove(permission)
super(UserOpenID, self).delete(using)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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 __future__ import unicode_literals
import django.dispatch
openid_login_complete = django.dispatch.Signal(providing_args=[
'request', 'openid_response'])
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Nonce'
db.create_table(u'django_openid_auth_nonce', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('server_url', self.gf('django.db.models.fields.CharField')(max_length=2047)),
('timestamp', self.gf('django.db.models.fields.IntegerField')()),
('salt', self.gf('django.db.models.fields.CharField')(max_length=40)),
))
db.send_create_signal(u'django_openid_auth', ['Nonce'])
# Adding model 'Association'
db.create_table(u'django_openid_auth_association', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('server_url', self.gf('django.db.models.fields.TextField')(max_length=2047)),
('handle', self.gf('django.db.models.fields.CharField')(max_length=255)),
('secret', self.gf('django.db.models.fields.TextField')(max_length=255)),
('issued', self.gf('django.db.models.fields.IntegerField')()),
('lifetime', self.gf('django.db.models.fields.IntegerField')()),
('assoc_type', self.gf('django.db.models.fields.TextField')(max_length=64)),
))
db.send_create_signal(u'django_openid_auth', ['Association'])
# Adding model 'UserOpenID'
db.create_table(u'django_openid_auth_useropenid', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('claimed_id', self.gf('django.db.models.fields.TextField')(unique=True, max_length=2047)),
('display_id', self.gf('django.db.models.fields.TextField')(max_length=2047)),
))
db.send_create_signal(u'django_openid_auth', ['UserOpenID'])
def backwards(self, orm):
# Deleting model 'Nonce'
db.delete_table(u'django_openid_auth_nonce')
# Deleting model 'Association'
db.delete_table(u'django_openid_auth_association')
# Deleting model 'UserOpenID'
db.delete_table(u'django_openid_auth_useropenid')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'django_openid_auth.association': {
'Meta': {'object_name': 'Association'},
'assoc_type': ('django.db.models.fields.TextField', [], {'max_length': '64'}),
'handle': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'issued': ('django.db.models.fields.IntegerField', [], {}),
'lifetime': ('django.db.models.fields.IntegerField', [], {}),
'secret': ('django.db.models.fields.TextField', [], {'max_length': '255'}),
'server_url': ('django.db.models.fields.TextField', [], {'max_length': '2047'})
},
u'django_openid_auth.nonce': {
'Meta': {'object_name': 'Nonce'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'salt': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'server_url': ('django.db.models.fields.CharField', [], {'max_length': '2047'}),
'timestamp': ('django.db.models.fields.IntegerField', [], {})
},
u'django_openid_auth.useropenid': {
'Meta': {'object_name': 'UserOpenID'},
'claimed_id': ('django.db.models.fields.TextField', [], {'unique': 'True', 'max_length': '2047'}),
'display_id': ('django.db.models.fields.TextField', [], {'max_length': '2047'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
}
}
complete_apps = ['django_openid_auth']
\ No newline at end of file
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
from django.db import connection, models, transaction
class Migration(DataMigration):
def add_account_verified_permission(self, orm):
ct, _ = orm['contenttypes.ContentType'].objects.get_or_create(
model='useropenid', app_label='django_openid_auth',
defaults=dict(name='user open id'))
perm, _ = orm['auth.permission'].objects.get_or_create(
content_type=ct, codename='account_verified',
defaults=dict(name=u'The OpenID account has been verified'))
def forwards(self, orm):
"Write your forwards methods here."
if getattr(connection.features,
'autocommits_when_autocommit_is_off', False):
# likely sqlite3 with django 1.6 and above
with transaction.autocommit():
self.add_account_verified_permission(orm)
else:
self.add_account_verified_permission(orm)
def backwards(self, orm):
"Write your backwards methods here."
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'django_openid_auth.association': {
'Meta': {'object_name': 'Association'},
'assoc_type': ('django.db.models.fields.TextField', [], {'max_length': '64'}),
'handle': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'issued': ('django.db.models.fields.IntegerField', [], {}),
'lifetime': ('django.db.models.fields.IntegerField', [], {}),
'secret': ('django.db.models.fields.TextField', [], {'max_length': '255'}),
'server_url': ('django.db.models.fields.TextField', [], {'max_length': '2047'})
},
u'django_openid_auth.nonce': {
'Meta': {'object_name': 'Nonce'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'salt': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'server_url': ('django.db.models.fields.CharField', [], {'max_length': '2047'}),
'timestamp': ('django.db.models.fields.IntegerField', [], {})
},
u'django_openid_auth.useropenid': {
'Meta': {'object_name': 'UserOpenID'},
'claimed_id': ('django.db.models.fields.TextField', [], {'unique': 'True', 'max_length': '2047'}),
'display_id': ('django.db.models.fields.TextField', [], {'max_length': '2047'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
}
}
complete_apps = ['contenttypes', 'auth', 'django_openid_auth']
symmetrical = True
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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 __future__ import unicode_literals
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):
super(DjangoOpenIDStore, self).__init__()
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
# Launchpad OpenID Teams Extension support for python-openid
#
# Copyright (C) 2008-2013 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 __future__ import unicode_literals
from openid import oidutil
from openid.extension import Extension
from openid.message import (
registerNamespaceAlias,
NamespaceAliasRegistrationError,
)
__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
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(',')
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
<!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>
{% load i18n %}
{% load url from future %}
<!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">
{% csrf_token %}
<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>
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 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 .test_views import * # flake8: noqa
from .test_settings import *
from .test_store import *
from .test_auth import *
from .test_admin import *
from __future__ import unicode_literals
from django.test.utils import override_settings
override_session_serializer = override_settings(
SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer')
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 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.
"""Tests for the django_openid_auth Admin login form replacement."""
from __future__ import unicode_literals
from django.conf import settings
from django.contrib.auth.models import User
from django.test import TestCase
from django.test.utils import override_settings
@override_settings(OPENID_USE_AS_ADMIN_LOGIN=True)
class SiteAdminTests(TestCase):
"""
TestCase for accessing /admin/ when the django_openid_auth form replacement
is in use.
"""
def test_admin_site_with_openid_login_authenticated_non_staff(self):
"""
If the request has an authenticated user, who is not flagged as a
staff member, then they get a failure response.
"""
User.objects.create_user(
username='testing', email='testing@example.com', password='test')
assert self.client.login(username='testing', password='test')
response = self.client.get('/admin/', follow=True)
self.assertContains(
response,
'User testing does not have admin/staff access.', status_code=403)
def test_admin_site_with_openid_login_non_authenticated_user(self):
"""
Unauthenticated users accessing the admin page should be directed to
the OpenID login url.
"""
response = self.client.get('/admin/', follow=True)
self.assertRedirects(
response,
getattr(settings, 'LOGIN_URL', '/openid/login') +
'?next=%2Fadmin%2F')
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2010-2013 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 __future__ import unicode_literals
from django.contrib.auth.models import Group, Permission, User
from django.test import TestCase
from django.test.utils import override_settings
from openid.consumer.consumer import SuccessResponse
from openid.consumer.discover import OpenIDServiceEndpoint
from openid.message import Message, OPENID2_NS
from django_openid_auth.auth import OpenIDBackend
from django_openid_auth.models import UserOpenID
from django_openid_auth.teams import ns_uri as TEAMS_NS
from django_openid_auth.tests.helpers import override_session_serializer
SREG_NS = "http://openid.net/sreg/1.0"
AX_NS = "http://openid.net/srv/ax/1.0"
@override_session_serializer
@override_settings(
OPENID_USE_EMAIL_FOR_USERNAME=False,
OPENID_LAUNCHPAD_TEAMS_REQUIRED=[],
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO=False,
OPENID_EMAIL_WHITELIST_REGEXP_LIST=[])
class OpenIDBackendTests(TestCase):
def setUp(self):
super(OpenIDBackendTests, self).setUp()
self.backend = OpenIDBackend()
def make_openid_response(self, sreg_args=None, teams_args=None):
endpoint = OpenIDServiceEndpoint()
endpoint.claimed_id = 'some-id'
message = Message(OPENID2_NS)
if sreg_args is not None:
for key, value in sreg_args.items():
message.setArg(SREG_NS, key, value)
if teams_args is not None:
for key, value in teams_args.items():
message.setArg(TEAMS_NS, key, value)
response = SuccessResponse(
endpoint, message, signed_fields=message.toPostArgs().keys())
return response
def make_response_ax(
self, schema="http://axschema.org/",
fullname="Some User", nickname="someuser", email="foo@example.com",
first=None, last=None, verified=False):
endpoint = OpenIDServiceEndpoint()
message = Message(OPENID2_NS)
attributes = [
("nickname", schema + "namePerson/friendly", nickname),
("fullname", schema + "namePerson", fullname),
("email", schema + "contact/email", email),
("account_verified",
"http://ns.login.ubuntu.com/2013/validation/account",
"token_via_email" if verified else "no")
]
if first:
attributes.append(
("first", "http://axschema.org/namePerson/first", first))
if last:
attributes.append(
("last", "http://axschema.org/namePerson/last", last))
message.setArg(AX_NS, "mode", "fetch_response")
for (alias, uri, value) in attributes:
message.setArg(AX_NS, "type.%s" % alias, uri)
message.setArg(AX_NS, "value.%s" % alias, value)
return SuccessResponse(
endpoint, message, signed_fields=message.toPostArgs().keys())
def make_user_openid(self, user=None,
claimed_id='http://example.com/existing_identity',
display_id='http://example.com/existing_identity'):
if user is None:
user = User.objects.create_user(
username='someuser', email='someuser@example.com',
password='12345678')
user_openid, created = UserOpenID.objects.get_or_create(
user=user, claimed_id=claimed_id, display_id=display_id)
return user_openid
def assert_account_verified(self, user, initially_verified, verified):
# set user's verification status
permission = Permission.objects.get(codename='account_verified')
if initially_verified:
user.user_permissions.add(permission)
else:
user.user_permissions.remove(permission)
user = User.objects.get(pk=user.pk)
has_perm = user.has_perm('django_openid_auth.account_verified')
assert has_perm == initially_verified
if hasattr(user, '_perm_cache'):
del user._perm_cache
# get a response including verification status
response = self.make_response_ax()
data = dict(first_name=u"Some56789012345678901234567890123",
last_name=u"User56789012345678901234567890123",
email=u"someotheruser@example.com",
account_verified=verified)
self.backend.update_user_details(user, data, response)
# refresh object from the database
user = User.objects.get(pk=user.pk)
# check the verification status
self.assertEqual(
user.has_perm('django_openid_auth.account_verified'), verified)
def test_extract_user_details_sreg(self):
expected = {
'nickname': 'someuser',
'first_name': 'Some',
'last_name': 'User',
'email': 'foo@example.com',
'account_verified': False,
}
data = {
'nickname': expected['nickname'],
'fullname': "%s %s" % (expected['first_name'],
expected['last_name']),
'email': expected['email'],
}
response = self.make_openid_response(sreg_args=data)
details = self.backend._extract_user_details(response)
self.assertEqual(details, expected)
def test_extract_user_details_ax(self):
response = self.make_response_ax(
fullname="Some User", nickname="someuser", email="foo@example.com")
data = self.backend._extract_user_details(response)
self.assertEqual(data, {"nickname": "someuser",
"first_name": "Some",
"last_name": "User",
"email": "foo@example.com",
"account_verified": False})
def test_extract_user_details_ax_split_name(self):
# Include fullname too to show that the split data takes
# precedence.
response = self.make_response_ax(
fullname="Bad Data", first="Some", last="User")
data = self.backend._extract_user_details(response)
self.assertEqual(data, {"nickname": "someuser",
"first_name": "Some",
"last_name": "User",
"email": "foo@example.com",
"account_verified": False})
def test_extract_user_details_ax_broken_myopenid(self):
response = self.make_response_ax(
schema="http://schema.openid.net/", fullname="Some User",
nickname="someuser", email="foo@example.com")
data = self.backend._extract_user_details(response)
self.assertEqual(data, {"nickname": "someuser",
"first_name": "Some",
"last_name": "User",
"email": "foo@example.com",
"account_verified": False})
def test_update_user_details_long_names(self):
response = self.make_response_ax()
user = User.objects.create_user(
'someuser', 'someuser@example.com', password=None)
user_openid, created = UserOpenID.objects.get_or_create(
user=user,
claimed_id='http://example.com/existing_identity',
display_id='http://example.com/existing_identity')
data = dict(
first_name=u"Some56789012345678901234567890123",
last_name=u"User56789012345678901234567890123",
email=u"someotheruser@example.com", account_verified=False)
self.backend.update_user_details(user, data, response)
self.assertEqual("Some56789012345678901234567890", user.first_name)
self.assertEqual("User56789012345678901234567890", user.last_name)
def test_update_user_perms_initially_verified_then_verified(self):
self.assert_account_verified(
self.make_user_openid().user,
initially_verified=True, verified=True)
def test_update_user_perms_initially_verified_then_unverified(self):
self.assert_account_verified(
self.make_user_openid().user,
initially_verified=True, verified=False)
def test_update_user_perms_initially_not_verified_then_verified(self):
self.assert_account_verified(
self.make_user_openid().user,
initially_verified=False, verified=True)
def test_update_user_perms_initially_not_verified_then_unverified(self):
self.assert_account_verified(
self.make_user_openid().user,
initially_verified=False, verified=False)
def test_extract_user_details_name_with_trailing_space(self):
response = self.make_response_ax(fullname="SomeUser ")
data = self.backend._extract_user_details(response)
self.assertEqual("", data['first_name'])
self.assertEqual("SomeUser", data['last_name'])
def test_extract_user_details_name_with_thin_space(self):
response = self.make_response_ax(fullname=u"Some\u2009User")
data = self.backend._extract_user_details(response)
self.assertEqual("Some", data['first_name'])
self.assertEqual("User", data['last_name'])
@override_settings(OPENID_USE_EMAIL_FOR_USERNAME=True)
def test_preferred_username_email_munging(self):
for nick, email, expected in [
('nickcomesfirst', 'foo@example.com', 'nickcomesfirst'),
('', 'foo@example.com', 'fooexamplecom'),
('noemail', '', 'noemail'),
('', '@%.-', 'openiduser'),
('', '', 'openiduser'),
(None, None, 'openiduser')]:
self.assertEqual(
expected,
self.backend._get_preferred_username(nick, email))
def test_preferred_username_no_email_munging(self):
for nick, email, expected in [
('nickcomesfirst', 'foo@example.com', 'nickcomesfirst'),
('', 'foo@example.com', 'openiduser'),
('noemail', '', 'noemail'),
('', '@%.-', 'openiduser'),
('', '', 'openiduser'),
(None, None, 'openiduser')]:
self.assertEqual(
expected,
self.backend._get_preferred_username(nick, email))
@override_settings(
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO=True,
OPENID_LAUNCHPAD_TEAMS_REQUIRED=['team'])
def test_authenticate_when_not_member_of_teams_required(self):
Group.objects.create(name='team')
response = self.make_openid_response(
sreg_args=dict(nickname='someuser'),
teams_args=dict(is_member='foo'))
user = self.backend.authenticate(openid_response=response)
self.assertIsNone(user)
@override_settings(
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO=True,
OPENID_LAUNCHPAD_TEAMS_REQUIRED=['team'])
def test_authenticate_when_no_group_mapping_to_required_team(self):
assert Group.objects.filter(name='team').count() == 0
response = self.make_openid_response(
sreg_args=dict(nickname='someuser'),
teams_args=dict(is_member='foo'))
user = self.backend.authenticate(openid_response=response)
self.assertIsNone(user)
@override_settings(
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO=True,
OPENID_LAUNCHPAD_TEAMS_REQUIRED=['team'])
def test_authenticate_when_member_of_teams_required(self):
Group.objects.create(name='team')
response = self.make_openid_response(
sreg_args=dict(nickname='someuser'),
teams_args=dict(is_member='foo,team'))
user = self.backend.authenticate(openid_response=response)
self.assertIsNotNone(user)
@override_settings(OPENID_LAUNCHPAD_TEAMS_REQUIRED=[])
def test_authenticate_when_no_teams_required(self):
response = self.make_openid_response(
sreg_args=dict(nickname='someuser'),
teams_args=dict(is_member='team'))
user = self.backend.authenticate(openid_response=response)
self.assertIsNotNone(user)
@override_settings(
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO=True,
OPENID_LAUNCHPAD_TEAMS_REQUIRED=['team1', 'team2'])
def test_authenticate_when_member_of_at_least_one_team(self):
Group.objects.create(name='team1')
response = self.make_openid_response(
sreg_args=dict(nickname='someuser'),
teams_args=dict(is_member='foo,team1'))
user = self.backend.authenticate(openid_response=response)
self.assertIsNotNone(user)
@override_settings(
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO=True,
OPENID_LAUNCHPAD_TEAMS_REQUIRED=['team'],
OPENID_EMAIL_WHITELIST_REGEXP_LIST=['foo(\+[^@]*)?@foo.com'])
def test_authenticate_when_not_in_required_team_but_email_whitelisted(
self):
assert Group.objects.filter(name='team').count() == 0
response = self.make_openid_response(
sreg_args=dict(nickname='someuser', email='foo@foo.com'),
teams_args=dict(is_member='foo'))
user = self.backend.authenticate(openid_response=response)
self.assertIsNotNone(user)
response = self.make_openid_response(
sreg_args=dict(nickname='someuser', email='foo+bar@foo.com'),
teams_args=dict(is_member='foo'))
user = self.backend.authenticate(openid_response=response)
self.assertIsNotNone(user)
@override_settings(
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO=True,
OPENID_LAUNCHPAD_TEAMS_REQUIRED=['team'],
OPENID_EMAIL_WHITELIST_REGEXP_LIST=['foo@foo.com', 'bar@foo.com'])
def test_authenticate_whitelisted_email_multiple_patterns(self):
assert Group.objects.filter(name='team').count() == 0
response = self.make_openid_response(
sreg_args=dict(nickname='someuser', email='bar@foo.com'),
teams_args=dict(is_member='foo'))
user = self.backend.authenticate(openid_response=response)
self.assertIsNotNone(user)
@override_settings(
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO=True,
OPENID_LAUNCHPAD_TEAMS_REQUIRED=['team'],
OPENID_EMAIL_WHITELIST_REGEXP_LIST=['foo@foo.com'])
def test_authenticate_whitelisted_email_not_match(self):
assert Group.objects.filter(name='team').count() == 0
response = self.make_openid_response(
sreg_args=dict(nickname='someuser', email='bar@foo.com'),
teams_args=dict(is_member='foo'))
user = self.backend.authenticate(openid_response=response)
self.assertIsNone(user)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2013 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 __future__ import unicode_literals
from django.contrib.auth.models import User
from django.test import TestCase
from django_openid_auth.models import (
Permission,
UserOpenID,
)
class UserOpenIDModelTestCase(TestCase):
def test_create_useropenid(self):
user = User.objects.create_user('someuser', 'someuser@example.com',
password=None)
user_openid, created = UserOpenID.objects.get_or_create(
user=user,
claimed_id='http://example.com/existing_identity',
display_id='http://example.com/existing_identity')
self.assertEqual('someuser', user_openid.user.username)
self.assertEqual(
user_openid.claimed_id, 'http://example.com/existing_identity')
self.assertEqual(
user_openid.display_id, 'http://example.com/existing_identity')
self.assertFalse(
User.objects.get(username='someuser').has_perm(
'django_openid_auth.account_verified'))
def test_delete_verified_useropenid(self):
user = User.objects.create_user('someuser', 'someuser@example.com',
password=None)
user_openid, created = UserOpenID.objects.get_or_create(
user=user,
claimed_id='http://example.com/existing_identity',
display_id='http://example.com/existing_identity')
permission = Permission.objects.get(codename='account_verified')
user.user_permissions.add(permission)
self.assertTrue(
User.objects.get(username='someuser').has_perm(
'django_openid_auth.account_verified'))
user_openid.delete()
self.assertFalse(
User.objects.get(username='someuser').has_perm(
'django_openid_auth.account_verified'))
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2013 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 __future__ import unicode_literals
from unittest import skipIf
from django import VERSION
from django.conf import settings
from django.test import TestCase
class SessionSerializerTest(TestCase):
"""Django 1.6 changed the default session serializer to use JSON
instead of pickle for security reasons[0]. Unfortunately the
openid module on which we rely stores objects which are not JSON
serializable[1], so until this is fixed upstream (or we decide to
create a wrapper serializer) we are recommending Django 1.6 users
to fallback to the PickleSerializer.
[0] https://bit.ly/1myzetd
[1] https://github.com/openid/python-openid/issues/17
"""
@skipIf(VERSION < (1, 5), "Django 1.4 does not provide SESSION_SERIALIZER")
def test_using_pickle_session_serializer(self):
serializer = getattr(settings, 'SESSION_SERIALIZER', '')
self.assertEqual(
serializer, 'django.contrib.sessions.serializers.PickleSerializer')
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 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 __future__ import unicode_literals
import time
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)
# -*- coding: utf-8 -*-
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 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 __future__ import unicode_literals
import cgi
from urlparse import parse_qs
from django.conf import settings
from django.contrib.auth.models import User, Group, Permission
from django.core.urlresolvers import reverse
from django.http import HttpRequest, HttpResponse
from django.test import TestCase
from django.test.utils import override_settings
from mock import patch
from openid.consumer.consumer import Consumer, SuccessResponse
from openid.consumer.discover import OpenIDServiceEndpoint
from openid.extensions import ax, sreg, pape
from openid.fetchers import (
HTTPFetcher, HTTPFetchingError, HTTPResponse, setDefaultFetcher)
from openid.oidutil import importElementTree
from openid.server.server import BROWSER_REQUEST_MODES, ENCODE_URL, Server
from openid.store.memstore import MemoryStore
from openid.message import IDENTIFIER_SELECT
from django_openid_auth import teams
from django_openid_auth.models import UserOpenID
from django_openid_auth.tests.helpers import override_session_serializer
from django_openid_auth.views import (
sanitise_redirect_url,
make_consumer,
)
from django_openid_auth.signals import openid_login_complete
from django_openid_auth.store import DjangoOpenIDStore
from django_openid_auth.exceptions import (
MissingUsernameViolation,
DuplicateUsernameViolation,
MissingPhysicalMultiFactor,
RequiredAttributeNotReturned,
)
ET = importElementTree()
class StubOpenIDProvider(HTTPFetcher):
def __init__(self, base_url):
super(StubOpenIDProvider, self).__init__()
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
self.type_uris = ['http://specs.openid.net/auth/2.0/signon']
def fetch(self, url, body=None, headers=None):
if url == self.identity_url:
# Serve an XRDS document directly, pointing at our endpoint.
type_uris = ['<Type>%s</Type>' % uri for uri in self.type_uris]
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">
%s
<URI>%s</URI>
<LocalID>%s</LocalID>
</Service>
</XRD>
</xrds:XRDS>
""" % ('\n'.join(type_uris), 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 &lt; 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 DummyDjangoRequest(object):
def __init__(self, request_path):
super(DummyDjangoRequest, self).__init__()
self.request_path = request_path
self.META = {
'HTTP_HOST': "localhost",
'SCRIPT_NAME': "http://localhost",
'SERVER_PROTOCOL': "http",
}
self.POST = {
'openid_identifier': "http://example.com/identity",
}
self.GET = {}
self.session = {}
def get_full_path(self):
return self.META['SCRIPT_NAME'] + self.request_path
def build_absolute_uri(self):
return self.META['SCRIPT_NAME'] + self.request_path
def _combined_request(self):
request = {}
request.update(self.POST)
request.update(self.GET)
return request
REQUEST = property(_combined_request)
@override_session_serializer
@override_settings(
OPENID_CREATE_USERS=False,
OPENID_STRICT_USERNAMES=False,
OPENID_UPDATE_DETAILS_FROM_SREG=False,
OPENID_SSO_SERVER_URL=None,
OPENID_LAUNCHPAD_TEAMS_MAPPING={},
OPENID_USE_AS_ADMIN_LOGIN=False,
OPENID_FOLLOW_RENAMES=False,
OPENID_PHYSICAL_MULTIFACTOR_REQUIRED=False,
OPENID_SREG_REQUIRED_FIELDS=[],
OPENID_USE_EMAIL_FOR_USERNAME=False,
OPENID_VALID_VERIFICATION_SCHEMES={},
)
class RelyingPartyTests(TestCase):
urls = 'django_openid_auth.tests.urls'
login_url = reverse('openid-login')
def setUp(self):
super(RelyingPartyTests, self).setUp()
self.provider = StubOpenIDProvider('http://example.com/')
self.req = DummyDjangoRequest('http://localhost/')
self.endpoint = OpenIDServiceEndpoint()
self.endpoint.claimed_id = 'http://example.com/identity'
server_url = 'http://example.com/'
self.endpoint.server_url = server_url
self.consumer = make_consumer(self.req)
self.server = Server(DjangoOpenIDStore(), op_endpoint=server_url)
setDefaultFetcher(self.provider, wrap_exceptions=False)
self.addCleanup(setDefaultFetcher, None)
self.openid_req_no_next = {
'openid_identifier': 'http://example.com/identity'}
self.openid_req = {
'openid_identifier': 'http://example.com/identity',
'next': '/getuser/'}
self.openid_resp = {
'nickname': 'testuser', 'fullname': 'Openid User',
'email': 'test@example.com'}
def complete(self, openid_response):
"""Complete an OpenID authentication request."""
# The server can generate either a redirect or a form post
# here. For simplicity, force generation of a redirect.
openid_response.whichEncoding = lambda: ENCODE_URL
webresponse = self.provider.server.encodeResponse(openid_response)
self.assertEqual(webresponse.code, 302)
redirect_to = webresponse.headers['location']
self.assertTrue(redirect_to.startswith(
'http://testserver/openid/complete/'))
return self.client.get(
reverse('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.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# The login form is displayed:
response = self.client.get(self.login_url)
self.assertTemplateUsed(response, 'openid/login.html')
# Posting in an identity URL begins the authentication request:
response = self.client.post(self.login_url, self.openid_req)
self.assertContains(response, 'OpenID transaction in progress')
openid_request = self.provider.parseFormPost(response.content)
self.assertEqual(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.assertEqual(response.content, 'someuser')
def test_login_with_nonascii_return_to(self):
"""Ensure non-ascii characters can be used for the 'next' arg."""
response = self.client.post(
self.login_url,
{'openid_identifier': 'http://example.com/identity',
'next': '/files/ñandú.jpg'.encode('utf-8')})
self.assertContains(response, 'OpenID transaction in progress')
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.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
response = self.client.post(self.login_url, self.openid_req_no_next)
self.assertContains(response, 'OpenID transaction in progress')
openid_request = self.provider.parseFormPost(response.content)
self.assertEqual(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)
with self.settings(LOGIN_REDIRECT_URL='/getuser/'):
response = self.complete(openid_response)
self.assertRedirects(response, 'http://testserver/getuser/')
def test_login_sso(self):
user = User.objects.create_user('someuser', 'someone@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# Requesting the login form immediately begins an
# authentication request.
with self.settings(
OPENID_SSO_SERVER_URL='http://example.com/identity'):
response = self.client.get(self.login_url, {'next': '/getuser/'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'OpenID transaction in progress')
openid_request = self.provider.parseFormPost(response.content)
self.assertEqual(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.assertEqual(response.content, 'someuser')
def test_login_create_users(self):
# 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(self.login_url, self.openid_req)
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 = sreg.SRegRequest.fromOpenIDRequest(openid_request)
openid_response = openid_request.answer(True)
sreg_response = sreg.SRegResponse.extractResponse(
sreg_request, {'nickname': 'someuser', 'fullname': 'Some User',
'email': 'foo@example.com'})
openid_response.addExtension(sreg_response)
with self.settings(OPENID_CREATE_USERS=True):
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.assertEqual(response.content, 'someuser2')
# Check the details of the new user.
user = User.objects.get(username='someuser2')
self.assertEqual(user.first_name, 'Some')
self.assertEqual(user.last_name, 'User')
self.assertEqual(user.email, 'foo@example.com')
def _do_user_login(self, req_data, resp_data, use_sreg=True,
use_pape=None):
openid_request = self._get_login_request(req_data)
openid_response = self._get_login_response(
openid_request, resp_data, use_sreg, use_pape)
response = self.complete(openid_response)
self.assertRedirects(response, 'http://testserver/getuser/')
return response
def _get_login_request(self, req_data):
# Posting in an identity URL begins the authentication request:
response = self.client.post(self.login_url, req_data)
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)
return openid_request
def _get_login_response(self, openid_request, resp_data, use_sreg,
use_pape):
openid_response = openid_request.answer(True)
if use_sreg:
sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
sreg_response = sreg.SRegResponse.extractResponse(
sreg_request, resp_data)
openid_response.addExtension(sreg_response)
if use_pape is not None:
policies = [use_pape]
pape_response = pape.Response(auth_policies=policies)
openid_response.addExtension(pape_response)
return openid_response
@override_settings(OPENID_PHYSICAL_MULTIFACTOR_REQUIRED=True)
def test_login_physical_multifactor_request(self):
preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL
self.provider.type_uris.append(pape.ns_uri)
response = self.client.post(self.login_url, self.openid_req)
openid_request = self.provider.parseFormPost(response.content)
request_auth = openid_request.message.getArg(
'http://specs.openid.net/extensions/pape/1.0',
'preferred_auth_policies',
)
self.assertEqual(request_auth, preferred_auth)
@override_settings(OPENID_PHYSICAL_MULTIFACTOR_REQUIRED=True)
def test_login_physical_multifactor_response(self):
preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL
self.provider.type_uris.append(pape.ns_uri)
def mock_complete(this, request_args, return_to):
request = {
'openid.mode': 'checkid_setup',
'openid.trust_root': 'http://localhost/',
'openid.return_to': 'http://localhost/',
'openid.identity': IDENTIFIER_SELECT,
'openid.ns.pape': pape.ns_uri,
'openid.pape.auth_policies': request_args.get(
'openid.pape.auth_policies', pape.AUTH_NONE),
}
openid_server = self.provider.server
orequest = openid_server.decodeRequest(request)
response = SuccessResponse(
self.endpoint, orequest.message,
signed_fields=['openid.pape.auth_policies'])
return response
patch.object(Consumer, 'complete', mock_complete)
user = User.objects.create_user('testuser', 'test@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
response = self._do_user_login(
self.openid_req, self.openid_resp,
use_pape=pape.AUTH_MULTI_FACTOR_PHYSICAL)
query = parse_qs(response.request['QUERY_STRING'])
self.assertTrue('openid.pape.auth_policies' in query)
self.assertEqual(
query['openid.pape.auth_policies'], [preferred_auth])
response = self.client.get('/getuser/')
self.assertEqual(response.content, 'testuser')
@override_settings(OPENID_PHYSICAL_MULTIFACTOR_REQUIRED=True)
def test_login_physical_multifactor_not_provided(self):
preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL
self.provider.type_uris.append(pape.ns_uri)
def mock_complete(this, request_args, return_to):
request = {
'openid.mode': 'checkid_setup',
'openid.trust_root': 'http://localhost/',
'openid.return_to': 'http://localhost/',
'openid.identity': IDENTIFIER_SELECT,
'openid.ns.pape': pape.ns_uri,
'openid.pape.auth_policies': request_args.get(
'openid.pape.auth_policies', pape.AUTH_NONE),
}
openid_server = self.provider.server
orequest = openid_server.decodeRequest(request)
response = SuccessResponse(
self.endpoint, orequest.message,
signed_fields=['openid.pape.auth_policies'])
return response
patch.object(Consumer, 'complete', mock_complete)
user = User.objects.create_user('testuser', 'test@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
openid_request = self._get_login_request(self.openid_req)
openid_response = self._get_login_response(
openid_request, self.openid_req, self.openid_resp,
use_pape=pape.AUTH_NONE)
response_auth = openid_request.message.getArg(
'http://specs.openid.net/extensions/pape/1.0',
'auth_policies',
)
self.assertNotEqual(response_auth, preferred_auth)
response = self.complete(openid_response)
self.assertEqual(403, response.status_code)
self.assertContains(
response, '<h1>OpenID failed</h1>', status_code=403)
self.assertContains(
response,
'<p>Login requires physical multi-factor authentication.</p>',
status_code=403)
@override_settings(OPENID_PHYSICAL_MULTIFACTOR_REQUIRED=True)
def test_login_physical_multifactor_not_provided_override(self):
preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL
self.provider.type_uris.append(pape.ns_uri)
# Override the login_failure handler
def mock_login_failure_handler(request, message, status=403,
template_name=None,
exception=None):
self.assertIsInstance(exception, MissingPhysicalMultiFactor)
return HttpResponse('Test Failure Override', status=200)
def mock_complete(this, request_args, return_to):
pape_policy = request_args.get(
'openid.pape.auth_policies', pape.AUTH_NONE)
request = {
'openid.mode': 'checkid_setup',
'openid.trust_root': 'http://localhost/',
'openid.return_to': 'http://localhost/',
'openid.identity': IDENTIFIER_SELECT,
'openid.ns.pape': pape.ns_uri,
'openid.pape.auth_policies': pape_policy,
}
openid_server = self.provider.server
orequest = openid_server.decodeRequest(request)
response = SuccessResponse(
self.endpoint, orequest.message,
signed_fields=['openid.pape.auth_policies'])
return response
patch.object(Consumer, 'complete', mock_complete)
user = User.objects.create_user('testuser', 'test@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
openid_request = self._get_login_request(self.openid_req)
openid_response = self._get_login_response(
openid_request, self.openid_req, self.openid_resp,
use_pape=pape.AUTH_NONE)
response_auth = openid_request.message.getArg(
'http://specs.openid.net/extensions/pape/1.0',
'auth_policies',
)
self.assertNotEqual(response_auth, preferred_auth)
# Status code should be 200, since we over-rode the login_failure
with self.settings(OPENID_RENDER_FAILURE=mock_login_failure_handler):
response = self.complete(openid_response)
self.assertEqual(200, response.status_code)
self.assertContains(response, 'Test Failure Override')
def test_login_without_nickname(self):
self.openid_resp = {
'nickname': '', 'fullname': 'Openid User',
'email': 'foo@example.com'}
with self.settings(OPENID_CREATE_USERS=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# username defaults to 'openiduser'
self.assertEqual(response.content, 'openiduser')
# The user's full name and email have been updated.
user = User.objects.get(username=response.content)
self.assertEqual(user.first_name, 'Openid')
self.assertEqual(user.last_name, 'User')
self.assertEqual(user.email, 'foo@example.com')
def test_login_without_nickname_with_email_suggestion(self):
self.openid_resp = {
'nickname': '', 'fullname': 'Openid User',
'email': 'foo@example.com'}
with self.settings(
OPENID_CREATE_USERS=True, OPENID_USE_EMAIL_FOR_USERNAME=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# username defaults to a munged version of the email
self.assertEqual(response.content, 'fooexamplecom')
def test_login_duplicate_username_numbering(self):
# Setup existing user who's name we're going to conflict with
User.objects.create_user('testuser', 'someone@example.com')
# identity url is for 'renameuser'
# but returned username is for 'testuser', which already exists for
# another identity
with self.settings(
OPENID_FOLLOW_RENAMES=False, OPENID_CREATE_USERS=True,
OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# Since this username is already taken by someone else, we go through
# the process of adding +i to it, and get testuser2.
self.assertEqual(response.content, 'testuser2')
def test_login_duplicate_username_numbering_with_conflicts(self):
# Setup existing user who's name we're going to conflict with
User.objects.create_user('testuser', 'someone@example.com')
User.objects.create_user('testuser3', 'someone@example.com')
# identity url is for 'renameuser'
# but returned username is for 'testuser', which already exists for
# another identity
with self.settings(
OPENID_FOLLOW_RENAMES=False, OPENID_CREATE_USERS=True,
OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# Since this username is already taken by someone else, we go through
# the process of adding +i to it starting with the count of users with
# username starting with 'testuser', of which there are 2. i should
# start at 3, which already exists, so it should skip to 4.
self.assertEqual(response.content, 'testuser4')
def test_login_duplicate_username_numbering_with_holes(self):
# Setup existing user who's name we're going to conflict with
User.objects.create_user('testuser', 'someone@example.com')
User.objects.create_user('testuser1', 'someone@example.com')
User.objects.create_user('testuser6', 'someone@example.com')
User.objects.create_user('testuser7', 'someone@example.com')
User.objects.create_user('testuser8', 'someone@example.com')
# identity url is for 'renameuser'
# but returned username is for 'testuser', which already exists for
# another identity
with self.settings(
OPENID_FOLLOW_RENAMES=False, OPENID_CREATE_USERS=True,
OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# Since this username is already taken by someone else, we go through
# the process of adding +i to it starting with the count of users with
# username starting with 'testuser', of which there are 5. i should
# start at 6, and increment until it reaches 9.
self.assertEqual(response.content, 'testuser9')
def test_login_duplicate_username_numbering_with_nonsequential_matches(
self):
# Setup existing user who's name we're going to conflict with
User.objects.create_user('testuser', 'someone@example.com')
User.objects.create_user('testuserfoo', 'someone@example.com')
# identity url is for 'renameuser'
# but returned username is for 'testuser', which already exists for
# another identity
with self.settings(
OPENID_FOLLOW_RENAMES=False, OPENID_CREATE_USERS=True,
OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# Since this username is already taken by someone else, we go through
# the process of adding +i to it starting with the count of users with
# username starting with 'testuser', of which there are 2. i should
# start at 3, which will be available.
self.assertEqual(response.content, 'testuser3')
def test_login_follow_rename(self):
user = User.objects.create_user('testuser', 'someone@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
self.openid_resp = {
'nickname': 'someuser', 'fullname': 'Some User',
'email': 'foo@example.com'}
with self.settings(
OPENID_FOLLOW_RENAMES=True,
OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# If OPENID_FOLLOW_RENAMES, they are logged in as
# someuser (the passed in nickname has changed the username)
self.assertEqual(response.content, 'someuser')
# The user's full name and email have been updated.
user = User.objects.get(username=response.content)
self.assertEqual(user.first_name, 'Some')
self.assertEqual(user.last_name, 'User')
self.assertEqual(user.email, 'foo@example.com')
def test_login_follow_rename_without_nickname_change(self):
user = User.objects.create_user('testuser', 'someone@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
self.openid_resp = {
'nickname': 'testuser', 'fullname': 'Some User',
'email': 'foo@example.com'}
with self.settings(
OPENID_FOLLOW_RENAMES=True, OPENID_STRICT_USERNAMES=True,
OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# Username should not have changed
self.assertEqual(response.content, 'testuser')
# The user's full name and email have been updated.
user = User.objects.get(username=response.content)
self.assertEqual(user.first_name, 'Some')
self.assertEqual(user.last_name, 'User')
self.assertEqual(user.email, 'foo@example.com')
def test_login_follow_rename_conflict(self):
# Setup existing user who's name we're going to switch to
user = User.objects.create_user('testuser', 'someone@example.com')
UserOpenID.objects.get_or_create(
user=user,
claimed_id='http://example.com/existing_identity',
display_id='http://example.com/existing_identity')
# Setup user who is going to try to change username to 'testuser'
renamed_user = User.objects.create_user(
'renameuser', 'someone@example.com')
UserOpenID.objects.get_or_create(
user=renamed_user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# identity url is for 'renameuser'
# but returned username is for 'testuser', which already exists for
# another identity
self.openid_resp = {
'nickname': 'testuser', 'fullname': 'Rename User',
'email': 'rename@example.com'}
with self.settings(
OPENID_FOLLOW_RENAMES=True,
OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# If OPENID_FOLLOW_RENAMES, attempt to change username to 'testuser'
# but since that username is already taken by someone else, we go
# through the process of adding +i to it, and get testuser2.
self.assertEqual(response.content, 'testuser2')
# The user's full name and email have been updated.
user = User.objects.get(username=response.content)
self.assertEqual(user.first_name, 'Rename')
self.assertEqual(user.last_name, 'User')
self.assertEqual(user.email, 'rename@example.com')
def test_login_follow_rename_false_onlyonce(self):
# Setup existing user who's name we're going to switch to
user = User.objects.create_user('testuser', 'someone@example.com')
UserOpenID.objects.get_or_create(
user=user,
claimed_id='http://example.com/existing_identity',
display_id='http://example.com/existing_identity')
# Setup user who is going to try to change username to 'testuser'
renamed_user = User.objects.create_user(
'testuser2000eight', 'someone@example.com')
UserOpenID.objects.get_or_create(
user=renamed_user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# identity url is for 'testuser2000eight'
# but returned username is for 'testuser', which already exists for
# another identity
self.openid_resp = {
'nickname': 'testuser2', 'fullname': 'Rename User',
'email': 'rename@example.com'}
with self.settings(
OPENID_FOLLOW_RENAMES=True,
OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# If OPENID_FOLLOW_RENAMES, attempt to change username to 'testuser'
# but since that username is already taken by someone else, we go
# through the process of adding +i to it. Even though it looks like
# the username follows the nickname+i scheme, it has non-numbers in the
# suffix, so it's not an auto-generated one. The regular process of
# renaming to 'testuser' has a conflict, so we get +2 at the end.
self.assertEqual(response.content, 'testuser2')
# The user's full name and email have been updated.
user = User.objects.get(username=response.content)
self.assertEqual(user.first_name, 'Rename')
self.assertEqual(user.last_name, 'User')
self.assertEqual(user.email, 'rename@example.com')
def test_login_follow_rename_conflict_onlyonce(self):
# Setup existing user who's name we're going to switch to
user = User.objects.create_user('testuser', 'someone@example.com')
UserOpenID.objects.get_or_create(
user=user,
claimed_id='http://example.com/existing_identity',
display_id='http://example.com/existing_identity')
# Setup user who is going to try to change username to 'testuser'
renamed_user = User.objects.create_user(
'testuser2000', 'someone@example.com')
UserOpenID.objects.get_or_create(
user=renamed_user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# identity url is for 'testuser2000'
# but returned username is for 'testuser', which already exists for
# another identity
self.openid_resp = {
'nickname': 'testuser', 'fullname': 'Rename User',
'email': 'rename@example.com'}
with self.settings(
OPENID_FOLLOW_RENAMES=True,
OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# If OPENID_FOLLOW_RENAMES, attempt to change username to 'testuser'
# but since that username is already taken by someone else, we go
# through the process of adding +i to it. Since the user for this
# identity url already has a name matching that pattern, check if first
self.assertEqual(response.content, 'testuser2000')
# The user's full name and email have been updated.
user = User.objects.get(username=response.content)
self.assertEqual(user.first_name, 'Rename')
self.assertEqual(user.last_name, 'User')
self.assertEqual(user.email, 'rename@example.com')
def test_login_follow_rename_false_conflict(self):
# Setup existing user who's username matches the name+i pattern
user = User.objects.create_user('testuser2', 'someone@example.com')
UserOpenID.objects.get_or_create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# identity url is for 'testuser2'
# but returned username is for 'testuser', which looks like we've done
# a username+1 for them already, but 'testuser' isn't actually taken
self.openid_resp = {
'nickname': 'testuser', 'fullname': 'Same User',
'email': 'same@example.com'}
with self.settings(
OPENID_FOLLOW_RENAMES=True,
OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
# If OPENID_FOLLOW_RENAMES, username should be changed to 'testuser'
# because it wasn't currently taken
self.assertEqual(response.content, 'testuser')
# The user's full name and email have been updated.
user = User.objects.get(username=response.content)
self.assertEqual(user.first_name, 'Same')
self.assertEqual(user.last_name, 'User')
self.assertEqual(user.email, 'same@example.com')
@override_settings(
OPENID_CREATE_USERS=True, OPENID_STRICT_USERNAMES=True,
OPENID_SREG_REQUIRED_FIELDS=[])
def test_strict_username_no_nickname(self):
# Posting in an identity URL begins the authentication request:
response = self.client.post(self.login_url, self.openid_req)
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 = sreg.SRegRequest.fromOpenIDRequest(openid_request)
openid_response = openid_request.answer(True)
sreg_response = sreg.SRegResponse.extractResponse(
sreg_request, {'nickname': '', # No nickname
'fullname': 'Some User',
'email': 'foo@example.com'})
openid_response.addExtension(sreg_response)
response = self.complete(openid_response)
# Status code should be 403: Forbidden
self.assertEqual(403, response.status_code)
self.assertContains(
response, '<h1>OpenID failed</h1>', status_code=403)
self.assertContains(
response,
"An attribute required for logging in was not returned (nickname)",
status_code=403)
@override_settings(
OPENID_CREATE_USERS=True, OPENID_STRICT_USERNAMES=True,
OPENID_SREG_REQUIRED_FIELDS=[])
def test_strict_username_no_nickname_override(self):
# Override the login_failure handler
def mock_login_failure_handler(request, message, status=403,
template_name=None,
exception=None):
self.assertIsInstance(
exception,
(RequiredAttributeNotReturned, MissingUsernameViolation))
return HttpResponse('Test Failure Override', status=200)
# Posting in an identity URL begins the authentication request:
response = self.client.post(self.login_url, self.openid_req)
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 = sreg.SRegRequest.fromOpenIDRequest(openid_request)
openid_response = openid_request.answer(True)
sreg_response = sreg.SRegResponse.extractResponse(
sreg_request, {'nickname': '', # No nickname
'fullname': 'Some User',
'email': 'foo@example.com'})
openid_response.addExtension(sreg_response)
with self.settings(OPENID_RENDER_FAILURE=mock_login_failure_handler):
response = self.complete(openid_response)
# Status code should be 200, since we over-rode the login_failure
self.assertEqual(200, response.status_code)
self.assertContains(response, 'Test Failure Override')
def test_strict_username_duplicate_user(self):
# Create a user with the same name as we'll pass back via sreg.
user = User.objects.create_user('someuser', 'someone@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/different_identity',
display_id='http://example.com/different_identity')
# Posting in an identity URL begins the authentication request:
response = self.client.post(self.login_url, self.openid_req)
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 = sreg.SRegRequest.fromOpenIDRequest(openid_request)
openid_response = openid_request.answer(True)
sreg_response = sreg.SRegResponse.extractResponse(
sreg_request, {'nickname': 'someuser', 'fullname': 'Some User',
'email': 'foo@example.com'})
openid_response.addExtension(sreg_response)
with self.settings(
OPENID_CREATE_USERS=True, OPENID_STRICT_USERNAMES=True):
response = self.complete(openid_response)
# Status code should be 403: Forbidden
self.assertEqual(403, response.status_code)
self.assertContains(
response, '<h1>OpenID failed</h1>', status_code=403)
self.assertContains(
response,
"The username (someuser) with which you tried to log in is "
"already in use for a different account.",
status_code=403)
def test_strict_username_duplicate_user_override(self):
# Override the login_failure handler
def mock_login_failure_handler(request, message, status=403,
template_name=None,
exception=None):
self.assertIsInstance(exception, DuplicateUsernameViolation)
return HttpResponse('Test Failure Override', status=200)
# Create a user with the same name as we'll pass back via sreg.
user = User.objects.create_user('someuser', 'someone@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/different_identity',
display_id='http://example.com/different_identity')
# Posting in an identity URL begins the authentication request:
response = self.client.post(self.login_url, self.openid_req)
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 = sreg.SRegRequest.fromOpenIDRequest(openid_request)
openid_response = openid_request.answer(True)
sreg_response = sreg.SRegResponse.extractResponse(
sreg_request, {'nickname': 'someuser', 'fullname': 'Some User',
'email': 'foo@example.com'})
openid_response.addExtension(sreg_response)
with self.settings(
OPENID_RENDER_FAILURE=mock_login_failure_handler,
OPENID_CREATE_USERS=True, OPENID_STRICT_USERNAMES=True):
response = self.complete(openid_response)
# Status code should be 200, since we over-rode the login_failure
self.assertEqual(200, response.status_code)
self.assertContains(response, 'Test Failure Override')
def test_login_requires_sreg_required_fields(self):
# Posting in an identity URL begins the authentication request:
response = self.client.post(self.login_url, self.openid_req)
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 = sreg.SRegRequest.fromOpenIDRequest(openid_request)
openid_response = openid_request.answer(True)
sreg_response = sreg.SRegResponse.extractResponse(
sreg_request, {'nickname': 'foo',
'fullname': 'Some User',
'email': 'foo@example.com'})
openid_response.addExtension(sreg_response)
# If any required attributes are not included in the response,
# we fail with a forbidden.
with self.settings(
OPENID_CREATE_USERS=True,
OPENID_SREG_REQUIRED_FIELDS=('email', 'language')):
response = self.complete(openid_response)
# Status code should be 403: Forbidden as we didn't include
# a required field - language.
self.assertContains(
response,
"An attribute required for logging in was not returned (language)",
status_code=403)
def test_login_update_details(self):
user = User.objects.create_user('testuser', 'someone@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
self.openid_resp = {
'nickname': 'testuser', 'fullname': 'Some User',
'email': 'foo@example.com'}
with self.settings(OPENID_UPDATE_DETAILS_FROM_SREG=True):
self._do_user_login(self.openid_req, self.openid_resp)
response = self.client.get('/getuser/')
self.assertEqual(response.content, 'testuser')
# The user's full name and email have been updated.
user = User.objects.get(username=response.content)
self.assertEqual(user.first_name, 'Some')
self.assertEqual(user.last_name, 'User')
self.assertEqual(user.email, 'foo@example.com')
def test_login_uses_sreg_extra_fields(self):
user = User.objects.create_user('testuser', 'someone@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# Posting in an identity URL begins the authentication request:
with self.settings(OPENID_SREG_EXTRA_FIELDS=('language',)):
response = self.client.post(self.login_url, self.openid_req)
openid_request = self.provider.parseFormPost(response.content)
sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
for field in ('email', 'fullname', 'nickname', 'language'):
self.assertTrue(field in sreg_request)
def test_login_uses_sreg_required_fields(self):
# The configurable sreg attributes are used in the request.
user = User.objects.create_user('testuser', 'someone@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# Posting in an identity URL begins the authentication request:
with self.settings(OPENID_SREG_REQUIRED_FIELDS=('email', 'language')):
response = self.client.post(self.login_url, self.openid_req)
openid_request = self.provider.parseFormPost(response.content)
sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
self.assertEqual(['email', 'language'], sreg_request.required)
self.assertEqual(['fullname', 'nickname'], sreg_request.optional)
def check_login_attribute_exchange(self, validation_type, is_verified,
request_account_verified=True):
user = User.objects.create_user('testuser', 'someone@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# Configure the provider to advertise attribute exchange
# protocol and start the authentication process:
self.provider.type_uris.append('http://openid.net/srv/ax/1.0')
response = self.client.post(self.login_url, self.openid_req)
self.assertContains(response, 'OpenID transaction in progress')
# The resulting OpenID request uses the Attribute Exchange
# extension rather than the Simple Registration extension.
openid_request = self.provider.parseFormPost(response.content)
sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request)
self.assertEqual(sreg_request.required, [])
self.assertEqual(sreg_request.optional, [])
fetch_request = ax.FetchRequest.fromOpenIDRequest(openid_request)
self.assertIn('http://axschema.org/contact/email', fetch_request)
self.assertIn('http://axschema.org/namePerson', fetch_request)
self.assertIn('http://axschema.org/namePerson/first', fetch_request)
self.assertIn('http://axschema.org/namePerson/last', fetch_request)
self.assertIn('http://axschema.org/namePerson/friendly', fetch_request)
# myOpenID compatibilty attributes:
self.assertIn('http://schema.openid.net/contact/email', fetch_request)
self.assertIn('http://schema.openid.net/namePerson', fetch_request)
self.assertIn(
'http://schema.openid.net/namePerson/friendly', fetch_request)
# Account verification:
validation = 'http://ns.login.ubuntu.com/2013/validation/account'
self.assertEqual(validation in fetch_request, request_account_verified)
# Build up a response including AX data.
openid_response = openid_request.answer(True)
fetch_response = ax.FetchResponse(fetch_request)
fetch_response.addValue(
'http://axschema.org/contact/email', 'foo@example.com')
fetch_response.addValue(
'http://axschema.org/namePerson/first', 'Firstname')
fetch_response.addValue(
'http://axschema.org/namePerson/last', 'Lastname')
fetch_response.addValue(
'http://axschema.org/namePerson/friendly', 'someuser')
if validation_type is not None:
fetch_response.addValue(
'http://ns.login.ubuntu.com/2013/validation/account',
validation_type)
openid_response.addExtension(fetch_response)
with self.settings(OPENID_UPDATE_DETAILS_FROM_SREG=True):
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), because
assert not settings.OPENID_FOLLOW_RENAMES, (
'OPENID_FOLLOW_RENAMES must be False')
response = self.client.get('/getuser/')
self.assertEqual(response.content, 'testuser')
# The user's full name and email have been updated.
user = User.objects.get(username='testuser')
self.assertEqual(user.first_name, 'Firstname')
self.assertEqual(user.last_name, 'Lastname')
self.assertEqual(user.email, 'foo@example.com')
# So have the user's permissions
self.assertEqual(
user.has_perm('django_openid_auth.account_verified'), is_verified)
def test_login_attribute_exchange_with_verification(self):
schemes = {
self.provider.endpoint_url: ('token_via_email',),
}
with self.settings(OPENID_VALID_VERIFICATION_SCHEMES=schemes):
self.check_login_attribute_exchange('token_via_email',
is_verified=True)
def test_login_attribute_exchange_without_verification(self):
schemes = {
self.provider.endpoint_url: ('token_via_email',),
}
with self.settings(OPENID_VALID_VERIFICATION_SCHEMES=schemes):
self.check_login_attribute_exchange(None, is_verified=False)
def test_login_attribute_exchange_without_account_verified(self):
# don't request account_verified attribute in AX request (as there are
# no valid verificatation schemes defined)
# and check account verification status is left unmodified
# (it's set to False by default for a new user)
self.check_login_attribute_exchange(None, is_verified=False,
request_account_verified=False)
def test_login_attribute_exchange_unrecognised_verification(self):
schemes = {
self.provider.endpoint_url: ('token_via_email',),
}
with self.settings(OPENID_VALID_VERIFICATION_SCHEMES=schemes):
self.check_login_attribute_exchange('unrecognised_scheme',
is_verified=False)
def test_login_attribute_exchange_different_default_verification(self):
schemes = {
None: ('token_via_email', 'sms'),
'http://otherprovider/': ('unrecognised_scheme',),
}
with self.settings(OPENID_VALID_VERIFICATION_SCHEMES=schemes):
self.check_login_attribute_exchange('unrecognised_scheme',
is_verified=False)
def test_login_attribute_exchange_matched_default_verification(self):
schemes = {
None: ('token_via_email',),
'http://otherprovider/': ('unrecognised_scheme',),
}
with self.settings(OPENID_VALID_VERIFICATION_SCHEMES=schemes):
self.check_login_attribute_exchange('token_via_email',
is_verified=True)
def test_login_teams(self):
user = User.objects.create_user('testuser', 'someone@example.com')
group = Group(name='groupname')
group.save()
# Django creates the add_nonce permission by default
group.permissions.add(
Permission.objects.get(codename='add_nonce'))
ogroup = Group(name='othergroup')
ogroup.save()
# Django creates the add_useropenid permission by default
ogroup.permissions.add(
Permission.objects.get(codename='add_useropenid'))
user.groups.add(ogroup)
user.save()
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# Posting in an identity URL begins the authentication request:
response = self.client.post(self.login_url, self.openid_req)
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)
mapping = {'teamname': 'groupname', 'otherteam': 'othergroup'}
with self.settings(
OPENID_LAUNCHPAD_TEAMS_MAPPING=mapping,
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO=False):
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.assertEqual(response.content, 'testuser')
# The user's groups have been updated.
User.objects.get(username='testuser')
self.assertIn(group, user.groups.all())
self.assertNotIn(ogroup, user.groups.all())
def test_login_teams_automapping(self):
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.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# Posting in an identity URL begins the authentication request:
response = self.client.post(self.login_url, self.openid_req)
self.assertContains(response, 'OpenID transaction in progress')
# Complete the request
mapping = {'teamname': 'groupname', 'otherteam': 'othergroup'}
blacklist = ['django-group1', 'django-group2']
with self.settings(
OPENID_LAUNCHPAD_TEAMS_MAPPING=mapping,
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO=True,
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST=blacklist):
openid_request = self.provider.parseFormPost(response.content)
openid_request.answer(True)
teams.TeamsRequest.fromOpenIDRequest(openid_request)
self.assertNotIn(group1, user.groups.all())
self.assertNotIn(group2, user.groups.all())
self.assertNotIn(group3, user.groups.all())
def test_login_teams_staff_not_defined(self):
assert getattr(settings, 'OPENID_LAUNCHPAD_STAFF_TEAMS', None) is None
user = User.objects.create_user('testuser', 'someone@example.com')
user.is_staff = True
user.save()
self.assertTrue(user.is_staff)
user = self.get_openid_authed_user_with_teams(
user, 'teamname,some-other-team')
self.assertTrue(user.is_staff)
def test_login_teams_staff_assignment(self):
user = User.objects.create_user('testuser', 'someone@example.com')
user.is_staff = False
user.save()
self.assertFalse(user.is_staff)
with self.settings(
OPENID_LAUNCHPAD_STAFF_TEAMS=('teamname',)):
user = self.get_openid_authed_user_with_teams(
user, 'teamname,some-other-team')
self.assertTrue(user.is_staff)
def test_login_teams_staff_unassignment(self):
user = User.objects.create_user('testuser', 'someone@example.com')
user.is_staff = True
user.save()
self.assertTrue(user.is_staff)
with self.settings(
OPENID_LAUNCHPAD_STAFF_TEAMS=('different-teamname',)):
user = self.get_openid_authed_user_with_teams(
user, 'teamname,some-other-team')
self.assertFalse(user.is_staff)
def get_openid_authed_user_with_teams(self, user, teams_str):
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
# Posting in an identity URL begins the authentication request:
response = self.client.post(self.login_url, self.openid_req_no_next)
# 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, teams_str)
openid_response.addExtension(teams_response)
response = self.complete(openid_response)
return User.objects.get(username=user.username)
def test_login_complete_signals_login(self):
# An oauth_login_complete signal is emitted including the
# request and sreg_response.
user = User.objects.create_user('someuser', 'someone@example.com')
UserOpenID.objects.create(
user=user,
claimed_id='http://example.com/identity',
display_id='http://example.com/identity')
response = self.client.post(self.login_url, self.openid_req_no_next)
openid_request = self.provider.parseFormPost(response.content)
openid_response = openid_request.answer(True)
# Use a closure to test whether the signal handler was called.
self.signal_handler_called = False
def login_callback(sender, **kwargs):
self.assertIsInstance(
kwargs.get('request', None), HttpRequest)
self.assertIsInstance(
kwargs.get('openid_response', None), SuccessResponse)
self.signal_handler_called = True
openid_login_complete.connect(login_callback)
response = self.complete(openid_response)
self.assertTrue(self.signal_handler_called)
openid_login_complete.disconnect(login_callback)
@override_session_serializer
class HelperFunctionsTest(TestCase):
domains = ["example.com", "example.org"]
@override_settings(ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS=domains)
def test_sanitise_redirect_url(self):
# 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)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 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 __future__ import unicode_literals
from django.conf.urls import patterns, include
from django.http import HttpResponse
def get_user(request):
return HttpResponse(request.user.username)
urlpatterns = patterns(
'',
(r'^getuser/$', get_user),
(r'^openid/', include('django_openid_auth.urls')),
)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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 __future__ import unicode_literals
from django.conf.urls import patterns, url
urlpatterns = patterns(
'django_openid_auth.views',
url(r'^login/$', 'login_begin', name='openid-login'),
url(r'^complete/$', 'login_complete', name='openid-complete'),
url(r'^logo.gif$', 'logo', name='openid-logo'),
)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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 __future__ import unicode_literals
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
try:
from django.views.decorators.csrf import csrf_exempt
except ImportError:
from django.contrib.csrf.middleware import csrf_exempt
from openid.consumer.consumer import (
Consumer, SUCCESS, CANCEL, FAILURE)
from openid.consumer.discover import DiscoveryFailure
from openid.extensions import sreg, ax, pape
from django_openid_auth import teams
from django_openid_auth.forms import OpenIDLoginForm
from django_openid_auth.models import UserOpenID
from django_openid_auth.signals import openid_login_complete
from django_openid_auth.store import DjangoOpenIDStore
from django_openid_auth.exceptions import (
DjangoOpenIDException,
)
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)
response = HttpResponseRedirect(redirect_url)
else:
form_html = openid_request.htmlMarkup(
trust_root, return_to, form_tag_attrs={'id': 'openid_message'})
response = HttpResponse(
form_html, content_type='text/html;charset=UTF-8')
return response
def default_render_failure(request, message, status=403,
template_name='openid/failure.html',
exception=None):
"""Render an error page to the user."""
data = render_to_string(
template_name, dict(message=message, exception=exception),
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',
login_complete_view='openid-complete',
form_class=OpenIDLoginForm,
render_failure=default_render_failure,
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 = form_class(data=request.POST)
if login_form.is_valid():
openid_url = login_form.cleaned_data['openid_identifier']
else:
login_form = form_class()
# Invalid or no form data:
if openid_url is None:
context = {'form': login_form, redirect_field_name: redirect_to}
return render_to_response(
template_name, context,
context_instance=RequestContext(request))
consumer = make_consumer(request)
try:
openid_request = consumer.begin(openid_url)
except DiscoveryFailure as exc:
return render_failure(
request, "OpenID discovery error: %s" % (str(exc),), status=500,
exception=exc)
# Request some user details. If the provider advertises support
# for attribute exchange, use that.
endpoint = openid_request.endpoint
if endpoint.supportsType(ax.AXMessage.ns_uri):
fetch_request = ax.FetchRequest()
# We mark all the attributes as required, since Google ignores
# optional attributes. We request both the full name and
# first/last components since some providers offer one but not
# the other.
for (attr, alias) in [
('http://axschema.org/contact/email', 'email'),
('http://axschema.org/namePerson', 'fullname'),
('http://axschema.org/namePerson/first', 'firstname'),
('http://axschema.org/namePerson/last', 'lastname'),
('http://axschema.org/namePerson/friendly', 'nickname'),
# The myOpenID provider advertises AX support, but uses
# attribute names from an obsolete draft of the
# specification. We request them for compatibility.
('http://schema.openid.net/contact/email', 'old_email'),
('http://schema.openid.net/namePerson', 'old_fullname'),
('http://schema.openid.net/namePerson/friendly',
'old_nickname')]:
fetch_request.add(ax.AttrInfo(attr, alias=alias, required=True))
# conditionally require account_verified attribute
verification_scheme_map = getattr(
settings, 'OPENID_VALID_VERIFICATION_SCHEMES', {})
valid_schemes = verification_scheme_map.get(
endpoint.server_url, verification_scheme_map.get(None, ()))
if valid_schemes:
# there are valid schemes configured for this endpoint, so
# request account_verified status
fetch_request.add(ax.AttrInfo(
'http://ns.login.ubuntu.com/2013/validation/account',
alias='account_verified', required=True))
openid_request.addExtension(fetch_request)
else:
sreg_required_fields = []
sreg_required_fields.extend(
getattr(settings, 'OPENID_SREG_REQUIRED_FIELDS', []))
sreg_optional_fields = ['email', 'fullname', 'nickname']
sreg_optional_fields.extend(
getattr(settings, 'OPENID_SREG_EXTRA_FIELDS', []))
sreg_optional_fields = [
field for field in sreg_optional_fields
if field not in sreg_required_fields]
openid_request.addExtension(
sreg.SRegRequest(optional=sreg_optional_fields,
required=sreg_required_fields))
if getattr(settings, 'OPENID_PHYSICAL_MULTIFACTOR_REQUIRED', False):
preferred_auth = [
pape.AUTH_MULTI_FACTOR_PHYSICAL,
]
pape_request = pape.Request(preferred_auth_policies=preferred_auth)
openid_request.addExtension(pape_request)
# 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_view))
if redirect_to:
if '?' in return_to:
return_to += '&'
else:
return_to += '?'
# Django gives us Unicode, which is great. We must encode URI.
# urllib enforces str. We can't trust anything about the default
# encoding inside str(foo) , so we must explicitly make foo a str.
return_to += urllib.urlencode(
{redirect_field_name: redirect_to.encode("UTF-8")})
return render_openid_request(request, openid_request, return_to)
@csrf_exempt
def login_complete(request, redirect_field_name=REDIRECT_FIELD_NAME,
render_failure=None):
redirect_to = request.REQUEST.get(redirect_field_name, '')
render_failure = (
render_failure or getattr(settings, 'OPENID_RENDER_FAILURE', None) or
default_render_failure)
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:
try:
user = authenticate(openid_response=openid_response)
except DjangoOpenIDException, e:
return render_failure(request, e.message, exception=e)
if user is not None:
if user.is_active:
auth_login(request, user)
response = HttpResponseRedirect(
sanitise_redirect_url(redirect_to))
# Notify any listeners that we successfully logged in.
openid_login_complete.send(
sender=UserOpenID, request=request,
openid_response=openid_response)
return response
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
"""
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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.
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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.
"""
Django settings for example_consumer project.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import django
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '34958734985734985734985798437'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
)
# Application definition
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.admin',
'django_openid_auth',
)
if django.VERSION < (1, 7):
INSTALLED_APPS += ('south',)
ROOT_URLCONF = 'example_consumer.urls'
WSGI_APPLICATION = 'example_consumer.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/'
# the library python-openid does not support a json session serializer
# <openid.yadis.manager.YadisServiceManager> is not JSON serializable
# https://github.com/openid/python-openid/issues/17
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
AUTHENTICATION_BACKENDS = (
'django_openid_auth.auth.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
)
# Should users be created when new OpenIDs are used to log in?
OPENID_CREATE_USERS = True
# When logging in again, should we overwrite user details based on
# data received via Simple Registration?
OPENID_UPDATE_DETAILS_FROM_SREG = True
# Map of OpenID Provider base URLs to recognised account verification schemes
# returned in response to a http://ns.login.ubuntu.com/2013/validation/account
# request. Use None as the key in place of a URL to specify verification
# schemes that will be trusted from unknown OpenID Providers (not recommended).
OPENID_VALID_VERIFICATION_SCHEMES = {
None: (),
}
# If set, always use this as the identity URL rather than asking the
# user. This only makes sense if it is a server URL.
OPENID_SSO_SERVER_URL = 'https://login.ubuntu.com/'
# Tell django.contrib.auth to use the OpenID signin URLs.
LOGIN_URL = '/openid/login/'
LOGIN_REDIRECT_URL = '/'
# Should django_auth_openid be used to sign into the admin interface?
OPENID_USE_AS_ADMIN_LOGIN = False
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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 import patterns, include, url
from django.contrib import admin
import views
admin.autodiscover()
urlpatterns = patterns(
'',
url(r'^$', views.index),
url(r'^openid/', include('django_openid_auth.urls')),
url(r'^logout/$', 'django.contrib.auth.views.logout'),
url(r'^private/$', views.require_authentication),
url(r'^admin/', include(admin.site.urls)),
)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 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.decorators import login_required
from django.http import HttpResponse
from django.utils.html import escape
def index(request):
s = ['<p>']
if request.user.is_authenticated():
s.append('You are signed in as <strong>%s</strong> (%s)' % (
escape(request.user.username),
escape(request.user.get_full_name())))
s.append(' | <a href="/logout">Sign out</a>')
else:
s.append('<a href="/openid/login">Sign in with OpenID</a>')
s.append('</p>')
s.append('<p><a href="/private">This requires authentication</a></p>')
return HttpResponse('\n'.join(s))
def next_works(request):
return HttpResponse('?next= bit works. <a href="/">Home</a>')
@login_required
def require_authentication(request):
return HttpResponse('This page requires authentication')
"""
WSGI config for demo project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_consumer.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head><title>OpenID in Django</title></head>
<body>
<h1>OpenID in Django</h1>
<p>The <tt class="docutils literal"><span class="pre">django_openidconsumer</span></tt> package contains all of the code needed to set up
your Django application as an OpenID consumer. You can use it to allow OpenID
users to sign in to your site without having to create a new username and
password.</p>
<div class="section">
<h2><a id="overview">Overview</a></h2>
<p>The OpenID consumer system consists of:</p>
<ul class="simple">
<li>Views for you to hook in to your application.</li>
<li>Database models implementing the persistence layer of an OpenID consumer.</li>
<li>Middleware that makes <tt class="docutils literal"><span class="pre">request.openid</span></tt> and <tt class="docutils literal"><span class="pre">request.openids</span></tt>
properties available to your application views.</li>
</ul>
</div>
<div class="section">
<h2><a id="dependencies">Dependencies</a></h2>
<p><tt class="docutils literal"><span class="pre">django_openidconsumer</span></tt> uses the <a class="reference" href="http://www.openidenabled.com/openid/libraries/python/">python-openid library</a>, which must be
installed separately somewhere on the Python path. You should install the 1.2.0
&#8220;combo&#8221; package which includes the <tt class="docutils literal"><span class="pre">yadis</span></tt> and <tt class="docutils literal"><span class="pre">urljr</span></tt> libraries.</p>
<p>The package also depends on the availability of Django&#8217;s <a class="reference" href="http://www.djangoproject.com/documentation/sessions/">session support</a>.</p>
</div>
<div class="section">
<h2><a id="installation">Installation</a></h2>
<p>Having ensured that both the <tt class="docutils literal"><span class="pre">python-openid</span></tt> library and the <tt class="docutils literal"><span class="pre">django_openidconsumer</span></tt> package are available on your Python path, you can
add OpenID consumer support to an application by doing the following:</p>
<ol class="arabic">
<li><p class="first">Put <tt class="docutils literal"><span class="pre">django_openidconsumer</span></tt> in your <tt class="docutils literal"><span class="pre">INSTALLED_APPS</span></tt> setting.</p>
</li>
<li><p class="first">Run the command <tt class="docutils literal"><span class="pre">manage.py</span> <span class="pre">syncdb</span></tt> to create the necessary tables.</p>
</li>
<li><p class="first">Add <tt class="docutils literal"><span class="pre">django_openidconsumer.middleware.OpenIDMiddleware</span></tt> to your list
of <tt class="docutils literal"><span class="pre">MIDDLEWARE_CLASSES</span></tt>, somewhere after the Session middleware.</p>
</li>
<li><p class="first">Add the following views to your urlconf:</p>
<pre class="literal-block">
(r'^openid/$', 'django_openidconsumer.views.begin'),
(r'^openid/complete/$', 'django_openidconsumer.views.complete'),
(r'^openid/signout/$', 'django_openidconsumer.views.signout'),
</pre>
</li>
</ol>
<p>You will then be able to browse to <tt class="docutils literal"><span class="pre">example.com/openid/</span></tt> and sign in using
an OpenID.</p>
</div>
<div class="section">
<h2><a id="using-the-openid-middleware">Using the OpenID middleware</a></h2>
<p>With the Middleware installed, your views will have access to the user&#8217;s OpenID
as the <tt class="docutils literal"><span class="pre">request.openid</span></tt> property. This will be <tt class="docutils literal"><span class="pre">None</span></tt> if the user has not
yet authenticated; otherwise it will be a <tt class="docutils literal"><span class="pre">django_openidconsumer.util.OpenID</span></tt>
instance.</p>
<p>If you want the user&#8217;s OpenID as a string, call the <tt class="docutils literal"><span class="pre">str()</span></tt> builtin on the
OpenID instance:</p>
<pre class="literal-block">
def example_view(request):
if request.openid:
return HttpResponse(&quot;OpenID is %s&quot; % escape(str(request.openid)))
else:
return HttpResponse(&quot;No OpenID&quot;)
</pre>
<p>Users can sign in with more than one OpenID. This is supported by the
<tt class="docutils literal"><span class="pre">request.openids</span></tt> property, which is a list of <tt class="docutils literal"><span class="pre">OpenID</span></tt> objects in the order
in which they were authenticated. <tt class="docutils literal"><span class="pre">request.openid</span></tt> merely returns the last
item in this list.</p>
</div>
<div class="section">
<h2><a id="using-simple-registration">Using simple registration</a></h2>
<p>Simple registration (or <a class="reference" href="http://openid.net/specs/openid-simple-registration-extension-1_0.html">sreg</a>) is an extension to the OpenID specification
that allows you to request extra details about a user from their OpenID
provider. It is frequently used to pre-populate registration forms with
information such as the user&#8217;s name, e-mail address or date of birth.</p>
<p>Be aware that not all OpenID providers support sreg, and there is no guarantee
that the information you have requested will be returned. Simple registration
should be used as a convenience for your users rather than as a required step in
your authentication process.</p>
<p>Available simple registration fields are <tt class="docutils literal"><span class="pre">nickname</span></tt>, <tt class="docutils literal"><span class="pre">email</span></tt>, <tt class="docutils literal"><span class="pre">fullname</span></tt>,
<tt class="docutils literal"><span class="pre">dob</span></tt>, <tt class="docutils literal"><span class="pre">gender</span></tt>, <tt class="docutils literal"><span class="pre">postcode</span></tt>, <tt class="docutils literal"><span class="pre">country</span></tt>, <tt class="docutils literal"><span class="pre">language</span></tt> and <tt class="docutils literal"><span class="pre">timezone</span></tt>.
Full details are available in the <a class="reference" href="http://openid.net/specs/openid-simple-registration-extension-1_0.html">spec</a>.</p>
<p>To request this information, pass the fields that you wish to retrieve as an
additional <tt class="docutils literal"><span class="pre">sreg</span></tt> argument to the <tt class="docutils literal"><span class="pre">django_openidconsumer.views.begin</span></tt> view:</p>
<pre class="literal-block">
(r'^openid/$', 'django_openidconsumer.views.begin', {
'sreg': 'email,nickname'
}),
</pre>
<p>Any simple registration fields that are returned will be available in a
dictionary as the <tt class="docutils literal"><span class="pre">sreg</span></tt> property of the OpenID object:</p>
<pre class="literal-block">
def example_sreg(request):
if request.openid and request.openid.sreg.has_key('email'):
return HttpResponse(&quot;Your e-mail address is: %s&quot; % escape(
request.openid.sreg['email']
))
else:
return HttpResponse(&quot;No e-mail address&quot;)
</pre>
</div>
<div class="section">
<h2><a id="customisation">Customisation</a></h2>
<p><tt class="docutils literal"><span class="pre">django_openidconsumer</span></tt> uses two templates:</p>
<dl class="docutils">
<dt><tt class="docutils literal"><span class="pre">openid_signin.html</span></tt></dt>
<dd>The form presented to the user when they sign in.</dd>
<dt><tt class="docutils literal"><span class="pre">openid_failure.html</span></tt></dt>
<dd>The template used to display an error message when something goes wrong.</dd>
</dl>
<p>You can over-ride the default templates by creating templates of the same name
and placing them somewhere on your template path. You can find the example
templates in the <tt class="docutils literal"><span class="pre">django_openidconsumer/templates</span></tt> directory.</p>
<p>The OpenID specification strongly recommends that any OpenID registration form
has a <tt class="docutils literal"><span class="pre">name</span></tt> attribute of <tt class="docutils literal"><span class="pre">openid_url</span></tt> to aid browser autocompletion, and
displays the <a class="reference" href="http://openid.net/login-bg.gif">OpenID logo</a> inline in the form field using the following CSS:</p>
<pre class="literal-block">
input.openid {
background: url(/path/to/login-bg.gif) no-repeat;
background-position: 0 50%;
padding-left: 16px;
}
</pre>
<p>By default, the package expects the <tt class="docutils literal"><span class="pre">django_openidconsumer.views.complete</span></tt>
view to be located at <tt class="docutils literal"><span class="pre">/openid/complete/</span></tt>. This is the view that the OpenID
provider will redirect the user to after they have authenticated. If you want to
put it somewhere else you can either pass an extra <tt class="docutils literal"><span class="pre">redirect_to</span></tt> argument to
<tt class="docutils literal"><span class="pre">django_openidconsumer.views.begin</span></tt> or add an <tt class="docutils literal"><span class="pre">OPENID_REDIRECT_TO</span></tt> setting
to <tt class="docutils literal"><span class="pre">settings.py</span></tt>.</p>
<p>You can pass a <tt class="docutils literal"><span class="pre">?next=</span></tt> query string argument containing a relative URL to
the <tt class="docutils literal"><span class="pre">begin</span></tt> view to control where the user will be redirected to having
returned to your site. You can also set the default redirection location
using the <tt class="docutils literal"><span class="pre">OPENID_REDIRECT_NEXT</span></tt> setting; if you do set set a default the user
will be redirected to your homepage.</p>
</div>
<div class="section">
<h2><a id="i-names">i-names</a></h2>
<p><a class="reference" href="http://www.inames.net/">i-names</a> are part of the OpenID 2.0 specification, which is currently being
developed. They are supported by the python-openid library, and hence are also
supported by <tt class="docutils literal"><span class="pre">django_openidconsumer</span></tt>. You can tell if an OpenID is an i-name
by checking the <tt class="docutils literal"><span class="pre">request.openid.is_iname</span></tt> property.</p>
<p>If you wish to disable i-name support, you can do so by adding the following to
your <tt class="docutils literal"><span class="pre">settings.py</span></tt>:</p>
<pre class="literal-block">
OPENID_DISALLOW_INAMES = True
</pre>
</div>
</body>
</html>
\ No newline at end of file
================
OpenID in Django
================
The ``django_openidconsumer`` package contains all of the code needed to set up
your Django application as an OpenID consumer. You can use it to allow OpenID
users to sign in to your site without having to create a new username and
password.
Overview
========
The OpenID consumer system consists of:
* Views for you to hook in to your application.
* Database models implementing the persistence layer of an OpenID consumer.
* Middleware that makes ``request.openid`` and ``request.openids``
properties available to your application views.
Dependencies
============
``django_openidconsumer`` uses the `python-openid library`_, which must be
installed separately somewhere on the Python path. You should install the 1.2.0
"combo" package which includes the ``yadis`` and ``urljr`` libraries.
The package also depends on the availability of Django's `session support`_.
.. _python-openid library: http://www.openidenabled.com/openid/libraries/python/
.. _session support: http://www.djangoproject.com/documentation/sessions/
Installation
============
Having ensured that both the ``python-openid`` library and the ``django_openidconsumer`` package are available on your Python path, you can
add OpenID consumer support to an application by doing the following:
1. Put ``django_openidconsumer`` in your ``INSTALLED_APPS`` setting.
2. Run the command ``manage.py syncdb`` to create the necessary tables.
3. Add ``django_openidconsumer.middleware.OpenIDMiddleware`` to your list
of ``MIDDLEWARE_CLASSES``, somewhere after the Session middleware.
4. Add the following views to your urlconf::
(r'^openid/$', 'django_openidconsumer.views.begin'),
(r'^openid/complete/$', 'django_openidconsumer.views.complete'),
(r'^openid/signout/$', 'django_openidconsumer.views.signout'),
You will then be able to browse to ``example.com/openid/`` and sign in using
an OpenID.
Using the OpenID middleware
===========================
With the Middleware installed, your views will have access to the user's OpenID
as the ``request.openid`` property. This will be ``None`` if the user has not
yet authenticated; otherwise it will be a ``django_openidconsumer.util.OpenID``
instance.
If you want the user's OpenID as a string, call the ``str()`` builtin on the
OpenID instance::
def example_view(request):
if request.openid:
return HttpResponse("OpenID is %s" % escape(str(request.openid)))
else:
return HttpResponse("No OpenID")
Users can sign in with more than one OpenID. This is supported by the
``request.openids`` property, which is a list of ``OpenID`` objects in the order
in which they were authenticated. ``request.openid`` merely returns the last
item in this list.
Using simple registration
=========================
Simple registration (or `sreg`_) is an extension to the OpenID specification
that allows you to request extra details about a user from their OpenID
provider. It is frequently used to pre-populate registration forms with
information such as the user's name, e-mail address or date of birth.
.. _sreg: http://openid.net/specs/openid-simple-registration-extension-1_0.html
Be aware that not all OpenID providers support sreg, and there is no guarantee
that the information you have requested will be returned. Simple registration
should be used as a convenience for your users rather than as a required step in
your authentication process.
Available simple registration fields are ``nickname``, ``email``, ``fullname``,
``dob``, ``gender``, ``postcode``, ``country``, ``language`` and ``timezone``.
Full details are available in the `spec`_.
.. _spec: http://openid.net/specs/openid-simple-registration-extension-1_0.html
To request this information, pass the fields that you wish to retrieve as an
additional ``sreg`` argument to the ``django_openidconsumer.views.begin`` view::
(r'^openid/$', 'django_openidconsumer.views.begin', {
'sreg': 'email,nickname'
}),
Any simple registration fields that are returned will be available in a
dictionary as the ``sreg`` property of the OpenID object::
def example_sreg(request):
if request.openid and request.openid.sreg.has_key('email'):
return HttpResponse("Your e-mail address is: %s" % escape(
request.openid.sreg['email']
))
else:
return HttpResponse("No e-mail address")
Customisation
=============
``django_openidconsumer`` uses two templates:
``openid_signin.html``
The form presented to the user when they sign in.
``openid_failure.html``
The template used to display an error message when something goes wrong.
You can over-ride the default templates by creating templates of the same name
and placing them somewhere on your template path. You can find the example
templates in the ``django_openidconsumer/templates`` directory.
The OpenID specification strongly recommends that any OpenID registration form
has a ``name`` attribute of ``openid_url`` to aid browser autocompletion, and
displays the `OpenID logo`_ inline in the form field using the following CSS::
input.openid {
background: url(/path/to/login-bg.gif) no-repeat;
background-position: 0 50%;
padding-left: 16px;
}
.. _OpenID logo: http://openid.net/login-bg.gif
By default, the package expects the ``django_openidconsumer.views.complete``
view to be located at ``/openid/complete/``. This is the view that the OpenID
provider will redirect the user to after they have authenticated. If you want to
put it somewhere else you can either pass an extra ``redirect_to`` argument to
``django_openidconsumer.views.begin`` or add an ``OPENID_REDIRECT_TO`` setting
to ``settings.py``.
You can pass a ``?next=`` query string argument containing a relative URL to
the ``begin`` view to control where the user will be redirected to having
returned to your site. You can also set the default redirection location
using the ``OPENID_REDIRECT_NEXT`` setting; if you do set set a default the user
will be redirected to your homepage.
i-names
=======
`i-names`_ are part of the OpenID 2.0 specification, which is currently being
developed. They are supported by the python-openid library, and hence are also
supported by ``django_openidconsumer``. You can tell if an OpenID is an i-name
by checking the ``request.openid.is_iname`` property.
.. _i-names: http://www.inames.net/
If you wish to disable i-name support, you can do so by adding the following to
your ``settings.py``::
OPENID_DISALLOW_INAMES = True
#!/usr/bin/env python
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 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.
"""OpenID integration for django.contrib.auth
A library that can be used to add OpenID support to Django applications.
The library integrates with Django's built in authentication system, so
most applications require minimal changes to support OpenID llogin. The
library also includes the following features:
* Basic user details are transferred from the OpenID server via the
Simple Registration extension or Attribute Exchange extension.
* can be configured to use a fixed OpenID server URL, for use in SSO.
* supports the launchpad.net teams extension to get team membership
info.
"""
from distutils.core import setup
description, long_description = __doc__.split('\n\n', 1)
VERSION = '0.6'
setup(
name='django-openid-auth',
version=VERSION,
author='Canonical Ltd',
author_email='noreply@canonical.com',
description=description,
long_description=long_description,
license='BSD',
platforms=['any'],
url='https://launchpad.net/django-openid-auth',
download_url=('http://launchpad.net/django-openid-auth/trunk/%s/+download'
'/django-openid-auth-%s.tar.gz' % (VERSION, VERSION)),
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules'
],
packages=[
'django_openid_auth',
'django_openid_auth.management',
'django_openid_auth.management.commands',
'django_openid_auth.tests',
],
package_data={
'django_openid_auth': ['templates/openid/*.html'],
},
provides=['django_openid_auth'],
requires=['django (>=1.4)', 'openid (>=2.2.0)', 'south'],
)
[tox]
envlist =
py2.7-django1.4, py2.7-django1.5, py2.7-django1.6, py2.7-django1.7, py2.7-django1.8
[testenv]
commands = python manage.py test django_openid_auth
deps=
mock
python-openid
[testenv:py2.7-django1.4]
basepython = python2.7
deps =
django >= 1.4, < 1.5
{[testenv]deps}
south==1.0
[testenv:py2.7-django1.5]
basepython = python2.7
deps =
django >= 1.5, < 1.6
{[testenv]deps}
south==1.0
[testenv:py2.7-django1.6]
basepython = python2.7
deps =
django >= 1.6, < 1.7
{[testenv]deps}
south==1.0
[testenv:py2.7-django1.7]
basepython = python2.7
deps =
django >= 1.7, < 1.8
{[testenv]deps}
[testenv:py2.7-django1.8]
basepython = python2.7
deps =
django >= 1.8, < 1.9
{[testenv]deps}
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