django-openid-auth-0.5/0000775000175000017500000000000012120431074016211 5ustar anthonyanthony00000000000000django-openid-auth-0.5/README.txt0000664000175000017500000001626312120161623017717 0ustar anthonyanthony00000000000000= 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 == 1. 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. 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'] == 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``) django-openid-auth-0.5/django_openid_auth/0000775000175000017500000000000012120431074022032 5ustar anthonyanthony00000000000000django-openid-auth-0.5/django_openid_auth/tests/0000775000175000017500000000000012120431074023174 5ustar anthonyanthony00000000000000django-openid-auth-0.5/django_openid_auth/tests/test_views.py0000664000175000017500000017643512120417252025764 0ustar anthonyanthony00000000000000# -*- 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. import cgi import unittest from urllib import quote_plus from django.conf import settings from django.contrib.auth.models import User, Group from django.http import HttpRequest, HttpResponse from django.test import TestCase 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.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): 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 = ['%s' % uri for uri in self.type_uris] return HTTPResponse( url, 200, {'content-type': 'application/xrds+xml'}, """\ %s %s %s """ % ('\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 < 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): 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) class RelyingPartyTests(TestCase): urls = 'django_openid_auth.tests.urls' 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.old_login_redirect_url = getattr(settings, 'LOGIN_REDIRECT_URL', '/accounts/profile/') self.old_create_users = getattr(settings, 'OPENID_CREATE_USERS', False) self.old_strict_usernames = getattr(settings, 'OPENID_STRICT_USERNAMES', False) self.old_update_details = getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False) self.old_sso_server_url = getattr(settings, 'OPENID_SSO_SERVER_URL', None) self.old_teams_map = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {}) self.old_use_as_admin_login = getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False) self.old_follow_renames = getattr(settings, 'OPENID_FOLLOW_RENAMES', False) self.old_physical_multifactor = getattr(settings, 'OPENID_PHYSICAL_MULTIFACTOR_REQUIRED', False) self.old_login_render_failure = getattr(settings, 'OPENID_RENDER_FAILURE', None) self.old_consumer_complete = Consumer.complete self.old_openid_use_email_for_username = getattr(settings, 'OPENID_USE_EMAIL_FOR_USERNAME', False) self.old_required_fields = getattr( settings, 'OPENID_SREG_REQUIRED_FIELDS', []) settings.OPENID_CREATE_USERS = False settings.OPENID_STRICT_USERNAMES = False settings.OPENID_UPDATE_DETAILS_FROM_SREG = False settings.OPENID_SSO_SERVER_URL = None settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {} settings.OPENID_USE_AS_ADMIN_LOGIN = False settings.OPENID_FOLLOW_RENAMES = False settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = False settings.OPENID_SREG_REQUIRED_FIELDS = [] settings.OPENID_USE_EMAIL_FOR_USERNAME = False def tearDown(self): settings.LOGIN_REDIRECT_URL = self.old_login_redirect_url settings.OPENID_CREATE_USERS = self.old_create_users settings.OPENID_STRICT_USERNAMES = self.old_strict_usernames settings.OPENID_UPDATE_DETAILS_FROM_SREG = self.old_update_details settings.OPENID_SSO_SERVER_URL = self.old_sso_server_url settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = self.old_teams_map settings.OPENID_USE_AS_ADMIN_LOGIN = self.old_use_as_admin_login settings.OPENID_FOLLOW_RENAMES = self.old_follow_renames settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = self.old_physical_multifactor settings.OPENID_RENDER_FAILURE = self.old_login_render_failure Consumer.complete = self.old_consumer_complete settings.OPENID_SREG_REQUIRED_FIELDS = self.old_required_fields settings.OPENID_USE_EMAIL_FOR_USERNAME = self.old_openid_use_email_for_username setDefaultFetcher(None) super(RelyingPartyTests, self).tearDown() 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.assertEquals(webresponse.code, 302) redirect_to = webresponse.headers['location'] self.assertTrue(redirect_to.startswith( 'http://testserver/openid/complete/')) return self.client.get('/openid/complete/', dict(cgi.parse_qsl(redirect_to.split('?', 1)[1]))) def test_login(self): user = User.objects.create_user('someuser', 'someone@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() # The login form is displayed: response = self.client.get('/openid/login/') self.assertTemplateUsed(response, 'openid/login.html') # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) self.assertContains(response, 'OpenID transaction in progress') openid_request = self.provider.parseFormPost(response.content) self.assertEquals(openid_request.mode, 'checkid_setup') self.assertTrue(openid_request.return_to.startswith( 'http://testserver/openid/complete/')) # Complete the request. The user is redirected to the next URL. openid_response = openid_request.answer(True) response = self.complete(openid_response) self.assertRedirects(response, 'http://testserver/getuser/') # And they are now logged in: response = self.client.get('/getuser/') self.assertEquals(response.content, 'someuser') def test_login_with_nonascii_return_to(self): """Ensure non-ascii characters can be used for the 'next' arg.""" response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': u'/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 = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() settings.LOGIN_REDIRECT_URL = '/getuser/' response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity'}) self.assertContains(response, 'OpenID transaction in progress') openid_request = self.provider.parseFormPost(response.content) self.assertEquals(openid_request.mode, 'checkid_setup') self.assertTrue(openid_request.return_to.startswith( 'http://testserver/openid/complete/')) # Complete the request. The user is redirected to the next URL. openid_response = openid_request.answer(True) response = self.complete(openid_response) self.assertRedirects( response, 'http://testserver' + settings.LOGIN_REDIRECT_URL) def test_login_sso(self): settings.OPENID_SSO_SERVER_URL = 'http://example.com/identity' user = User.objects.create_user('someuser', 'someone@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() # Requesting the login form immediately begins an # authentication request. response = self.client.get('/openid/login/', {'next': '/getuser/'}) self.assertEquals(response.status_code, 200) self.assertContains(response, 'OpenID transaction in progress') openid_request = self.provider.parseFormPost(response.content) self.assertEquals(openid_request.mode, 'checkid_setup') self.assertTrue(openid_request.return_to.startswith( 'http://testserver/openid/complete/')) # Complete the request. The user is redirected to the next URL. openid_response = openid_request.answer(True) response = self.complete(openid_response) self.assertRedirects(response, 'http://testserver/getuser/') # And they are now logged in: response = self.client.get('/getuser/') self.assertEquals(response.content, 'someuser') def test_login_create_users(self): settings.OPENID_CREATE_USERS = True # Create a user with the same name as we'll pass back via sreg. User.objects.create_user('someuser', 'someone@example.com') # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) self.assertContains(response, 'OpenID transaction in progress') # Complete the request, passing back some simple registration # data. The user is redirected to the next URL. openid_request = self.provider.parseFormPost(response.content) sreg_request = 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) response = self.complete(openid_response) self.assertRedirects(response, 'http://testserver/getuser/') # And they are now logged in as a new user (they haven't taken # over the existing "someuser" user). response = self.client.get('/getuser/') self.assertEquals(response.content, 'someuser2') # Check the details of the new user. user = User.objects.get(username='someuser2') self.assertEquals(user.first_name, 'Some') self.assertEquals(user.last_name, 'User') self.assertEquals(user.email, 'foo@example.com') def _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('/openid/login/', 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 def parse_query_string(self, query_str): query_items = map(tuple, [item.split('=') for item in query_str.split('&')]) query = dict(query_items) return query def test_login_physical_multifactor_request(self): settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL self.provider.type_uris.append(pape.ns_uri) openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} response = self.client.post('/openid/login/', 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) def test_login_physical_multifactor_response(self): settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True 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 Consumer.complete = mock_complete user = User.objects.create_user('testuser', 'test@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} openid_resp = {'nickname': 'testuser', 'fullname': 'Openid User', 'email': 'test@example.com'} response = self._do_user_login(openid_req, openid_resp, use_pape=pape.AUTH_MULTI_FACTOR_PHYSICAL) query = self.parse_query_string(response.request['QUERY_STRING']) self.assertTrue('openid.pape.auth_policies' in query) self.assertEqual(query['openid.pape.auth_policies'], quote_plus(preferred_auth)) response = self.client.get('/getuser/') self.assertEqual(response.content, 'testuser') def test_login_physical_multifactor_not_provided(self): settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True 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 Consumer.complete = mock_complete user = User.objects.create_user('testuser', 'test@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} openid_resp = {'nickname': 'testuser', 'fullname': 'Openid User', 'email': 'test@example.com'} openid_request = self._get_login_request(openid_req) openid_response = self._get_login_response(openid_request, openid_req, 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.assertEquals(403, response.status_code) self.assertContains(response, '

OpenID failed

', status_code=403) self.assertContains(response, '

Login requires physical multi-factor authentication.

', status_code=403) def test_login_physical_multifactor_not_provided_override(self): settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True 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.assertTrue(isinstance(exception, MissingPhysicalMultiFactor)) return HttpResponse('Test Failure Override', status=200) settings.OPENID_RENDER_FAILURE = mock_login_failure_handler 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 Consumer.complete = mock_complete user = User.objects.create_user('testuser', 'test@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} openid_resp = {'nickname': 'testuser', 'fullname': 'Openid User', 'email': 'test@example.com'} openid_request = self._get_login_request(openid_req) openid_response = self._get_login_response(openid_request, openid_req, 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 handler response = self.complete(openid_response) self.assertEquals(200, response.status_code) self.assertContains(response, 'Test Failure Override') def test_login_without_nickname(self): settings.OPENID_CREATE_USERS = True openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} openid_resp = {'nickname': '', 'fullname': 'Openid User', 'email': 'foo@example.com'} self._do_user_login(openid_req, openid_resp) response = self.client.get('/getuser/') # username defaults to 'openiduser' self.assertEquals(response.content, 'openiduser') # The user's full name and email have been updated. user = User.objects.get(username=response.content) self.assertEquals(user.first_name, 'Openid') self.assertEquals(user.last_name, 'User') self.assertEquals(user.email, 'foo@example.com') def test_login_without_nickname_with_email_suggestion(self): settings.OPENID_CREATE_USERS = True settings.OPENID_USE_EMAIL_FOR_USERNAME = True openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} openid_resp = {'nickname': '', 'fullname': 'Openid User', 'email': 'foo@example.com'} self._do_user_login(openid_req, openid_resp) response = self.client.get('/getuser/') # username defaults to a munged version of the email self.assertEquals(response.content, 'fooexamplecom') def test_login_duplicate_username_numbering(self): settings.OPENID_FOLLOW_RENAMES = False settings.OPENID_CREATE_USERS = True settings.OPENID_UPDATE_DETAILS_FROM_SREG = True # Setup existing user who's name we're going to conflict with user = User.objects.create_user('testuser', 'someone@example.com') # identity url is for 'renameuser' openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} # but returned username is for 'testuser', which already exists for another identity openid_resp = {'nickname': 'testuser', 'fullname': 'Test User', 'email': 'test@example.com'} self._do_user_login(openid_req, 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.assertEquals(response.content, 'testuser2') def test_login_duplicate_username_numbering_with_conflicts(self): settings.OPENID_FOLLOW_RENAMES = False settings.OPENID_CREATE_USERS = True settings.OPENID_UPDATE_DETAILS_FROM_SREG = True # Setup existing user who's name we're going to conflict with user = User.objects.create_user('testuser', 'someone@example.com') user = User.objects.create_user('testuser3', 'someone@example.com') # identity url is for 'renameuser' openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} # but returned username is for 'testuser', which already exists for another identity openid_resp = {'nickname': 'testuser', 'fullname': 'Test User', 'email': 'test@example.com'} self._do_user_login(openid_req, 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.assertEquals(response.content, 'testuser4') def test_login_duplicate_username_numbering_with_holes(self): settings.OPENID_FOLLOW_RENAMES = False settings.OPENID_CREATE_USERS = True settings.OPENID_UPDATE_DETAILS_FROM_SREG = True # Setup existing user who's name we're going to conflict with user = User.objects.create_user('testuser', 'someone@example.com') user = User.objects.create_user('testuser1', 'someone@example.com') user = User.objects.create_user('testuser6', 'someone@example.com') user = User.objects.create_user('testuser7', 'someone@example.com') user = User.objects.create_user('testuser8', 'someone@example.com') # identity url is for 'renameuser' openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} # but returned username is for 'testuser', which already exists for another identity openid_resp = {'nickname': 'testuser', 'fullname': 'Test User', 'email': 'test@example.com'} self._do_user_login(openid_req, 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.assertEquals(response.content, 'testuser9') def test_login_duplicate_username_numbering_with_nonsequential_matches(self): settings.OPENID_FOLLOW_RENAMES = False settings.OPENID_CREATE_USERS = True settings.OPENID_UPDATE_DETAILS_FROM_SREG = True # Setup existing user who's name we're going to conflict with user = User.objects.create_user('testuser', 'someone@example.com') user = User.objects.create_user('testuserfoo', 'someone@example.com') # identity url is for 'renameuser' openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} # but returned username is for 'testuser', which already exists for another identity openid_resp = {'nickname': 'testuser', 'fullname': 'Test User', 'email': 'test@example.com'} self._do_user_login(openid_req, 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.assertEquals(response.content, 'testuser3') def test_login_follow_rename(self): settings.OPENID_FOLLOW_RENAMES = True settings.OPENID_UPDATE_DETAILS_FROM_SREG = True user = User.objects.create_user('testuser', 'someone@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} openid_resp = {'nickname': 'someuser', 'fullname': 'Some User', 'email': 'foo@example.com'} self._do_user_login(openid_req, 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.assertEquals(response.content, 'someuser') # The user's full name and email have been updated. user = User.objects.get(username=response.content) self.assertEquals(user.first_name, 'Some') self.assertEquals(user.last_name, 'User') self.assertEquals(user.email, 'foo@example.com') def test_login_follow_rename_without_nickname_change(self): settings.OPENID_FOLLOW_RENAMES = True settings.OPENID_UPDATE_DETAILS_FROM_SREG = True settings.OPENID_STRICT_USERNAMES = True user = User.objects.create_user('testuser', 'someone@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} openid_resp = {'nickname': 'testuser', 'fullname': 'Some User', 'email': 'foo@example.com'} self._do_user_login(openid_req, openid_resp) response = self.client.get('/getuser/') # Username should not have changed self.assertEquals(response.content, 'testuser') # The user's full name and email have been updated. user = User.objects.get(username=response.content) self.assertEquals(user.first_name, 'Some') self.assertEquals(user.last_name, 'User') self.assertEquals(user.email, 'foo@example.com') def test_login_follow_rename_conflict(self): settings.OPENID_FOLLOW_RENAMES = True settings.OPENID_UPDATE_DETAILS_FROM_SREG = True # 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' openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} # but returned username is for 'testuser', which already exists for another identity openid_resp = {'nickname': 'testuser', 'fullname': 'Rename User', 'email': 'rename@example.com'} self._do_user_login(openid_req, 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.assertEquals(response.content, 'testuser2') # The user's full name and email have been updated. user = User.objects.get(username=response.content) self.assertEquals(user.first_name, 'Rename') self.assertEquals(user.last_name, 'User') self.assertEquals(user.email, 'rename@example.com') def test_login_follow_rename_false_onlyonce(self): settings.OPENID_FOLLOW_RENAMES = True settings.OPENID_UPDATE_DETAILS_FROM_SREG = True # 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' openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} # but returned username is for 'testuser', which already exists for another identity openid_resp = {'nickname': 'testuser2', 'fullname': 'Rename User', 'email': 'rename@example.com'} self._do_user_login(openid_req, 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.assertEquals(response.content, 'testuser2') # The user's full name and email have been updated. user = User.objects.get(username=response.content) self.assertEquals(user.first_name, 'Rename') self.assertEquals(user.last_name, 'User') self.assertEquals(user.email, 'rename@example.com') def test_login_follow_rename_conflict_onlyonce(self): settings.OPENID_FOLLOW_RENAMES = True settings.OPENID_UPDATE_DETAILS_FROM_SREG = True # 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' openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} # but returned username is for 'testuser', which already exists for another identity openid_resp = {'nickname': 'testuser', 'fullname': 'Rename User', 'email': 'rename@example.com'} self._do_user_login(openid_req, 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.assertEquals(response.content, 'testuser2000') # The user's full name and email have been updated. user = User.objects.get(username=response.content) self.assertEquals(user.first_name, 'Rename') self.assertEquals(user.last_name, 'User') self.assertEquals(user.email, 'rename@example.com') def test_login_follow_rename_false_conflict(self): settings.OPENID_FOLLOW_RENAMES = True settings.OPENID_UPDATE_DETAILS_FROM_SREG = True # 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' openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} # but returned username is for 'testuser', which looks like we've done # a username+1 for them already, but 'testuser' isn't actually taken openid_resp = {'nickname': 'testuser', 'fullname': 'Same User', 'email': 'same@example.com'} self._do_user_login(openid_req, openid_resp) response = self.client.get('/getuser/') # If OPENID_FOLLOW_RENAMES, username should be changed to 'testuser' # because it wasn't currently taken self.assertEquals(response.content, 'testuser') # The user's full name and email have been updated. user = User.objects.get(username=response.content) self.assertEquals(user.first_name, 'Same') self.assertEquals(user.last_name, 'User') self.assertEquals(user.email, 'same@example.com') def test_strict_username_no_nickname(self): settings.OPENID_CREATE_USERS = True settings.OPENID_STRICT_USERNAMES = True settings.OPENID_SREG_REQUIRED_FIELDS = [] # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) self.assertContains(response, 'OpenID transaction in progress') # Complete the request, passing back some simple registration # data. The user is redirected to the next URL. openid_request = self.provider.parseFormPost(response.content) sreg_request = 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.assertEquals(403, response.status_code) self.assertContains(response, '

OpenID failed

', status_code=403) self.assertContains(response, "An attribute required for logging in was not returned " "(nickname)", status_code=403) def test_strict_username_no_nickname_override(self): settings.OPENID_CREATE_USERS = True settings.OPENID_STRICT_USERNAMES = True settings.OPENID_SREG_REQUIRED_FIELDS = [] # Override the login_failure handler def mock_login_failure_handler(request, message, status=403, template_name=None, exception=None): self.assertTrue(isinstance(exception, (RequiredAttributeNotReturned, MissingUsernameViolation))) return HttpResponse('Test Failure Override', status=200) settings.OPENID_RENDER_FAILURE = mock_login_failure_handler # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) self.assertContains(response, 'OpenID transaction in progress') # Complete the request, passing back some simple registration # data. The user is redirected to the next URL. openid_request = self.provider.parseFormPost(response.content) sreg_request = 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 200, since we over-rode the login_failure handler self.assertEquals(200, response.status_code) self.assertContains(response, 'Test Failure Override') def test_strict_username_duplicate_user(self): settings.OPENID_CREATE_USERS = True settings.OPENID_STRICT_USERNAMES = True # Create a user with the same name as we'll pass back via sreg. user = User.objects.create_user('someuser', 'someone@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/different_identity', display_id='http://example.com/different_identity') useropenid.save() # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) self.assertContains(response, 'OpenID transaction in progress') # Complete the request, passing back some simple registration # data. The user is redirected to the next URL. openid_request = self.provider.parseFormPost(response.content) sreg_request = 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) response = self.complete(openid_response) # Status code should be 403: Forbidden self.assertEquals(403, response.status_code) self.assertContains(response, '

OpenID failed

', 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): settings.OPENID_CREATE_USERS = True settings.OPENID_STRICT_USERNAMES = True # Override the login_failure handler def mock_login_failure_handler(request, message, status=403, template_name=None, exception=None): self.assertTrue(isinstance(exception, DuplicateUsernameViolation)) return HttpResponse('Test Failure Override', status=200) settings.OPENID_RENDER_FAILURE = mock_login_failure_handler # Create a user with the same name as we'll pass back via sreg. user = User.objects.create_user('someuser', 'someone@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/different_identity', display_id='http://example.com/different_identity') useropenid.save() # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) self.assertContains(response, 'OpenID transaction in progress') # Complete the request, passing back some simple registration # data. The user is redirected to the next URL. openid_request = self.provider.parseFormPost(response.content) sreg_request = 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) response = self.complete(openid_response) # Status code should be 200, since we over-rode the login_failure handler self.assertEquals(200, response.status_code) self.assertContains(response, 'Test Failure Override') def test_login_requires_sreg_required_fields(self): # If any required attributes are not included in the response, # we fail with a forbidden. settings.OPENID_CREATE_USERS = True settings.OPENID_SREG_REQUIRED_FIELDS = ('email', 'language') # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) self.assertContains(response, 'OpenID transaction in progress') # Complete the request, passing back some simple registration # data. The user is redirected to the next URL. openid_request = self.provider.parseFormPost(response.content) sreg_request = 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) 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): settings.OPENID_UPDATE_DETAILS_FROM_SREG = True user = User.objects.create_user('testuser', 'someone@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() openid_req = {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'} openid_resp = {'nickname': 'testuser', 'fullname': 'Some User', 'email': 'foo@example.com'} self._do_user_login(openid_req, openid_resp) response = self.client.get('/getuser/') self.assertEquals(response.content, 'testuser') # The user's full name and email have been updated. user = User.objects.get(username=response.content) self.assertEquals(user.first_name, 'Some') self.assertEquals(user.last_name, 'User') self.assertEquals(user.email, 'foo@example.com') def test_login_uses_sreg_extra_fields(self): # The configurable sreg attributes are used in the request. settings.OPENID_SREG_EXTRA_FIELDS = ('language',) user = User.objects.create_user('testuser', 'someone@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) 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. settings.OPENID_SREG_REQUIRED_FIELDS = ('email', 'language') user = User.objects.create_user('testuser', 'someone@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) 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 test_login_attribute_exchange(self): settings.OPENID_UPDATE_DETAILS_FROM_SREG = True user = User.objects.create_user('testuser', 'someone@example.com') useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() # 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('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) 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.assertTrue(fetch_request.has_key( 'http://axschema.org/contact/email')) self.assertTrue(fetch_request.has_key( 'http://axschema.org/namePerson')) self.assertTrue(fetch_request.has_key( 'http://axschema.org/namePerson/first')) self.assertTrue(fetch_request.has_key( 'http://axschema.org/namePerson/last')) self.assertTrue(fetch_request.has_key( 'http://axschema.org/namePerson/friendly')) # myOpenID compatibilty attributes: self.assertTrue(fetch_request.has_key( 'http://schema.openid.net/contact/email')) self.assertTrue(fetch_request.has_key( 'http://schema.openid.net/namePerson')) self.assertTrue(fetch_request.has_key( 'http://schema.openid.net/namePerson/friendly')) # 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') openid_response.addExtension(fetch_response) response = self.complete(openid_response) self.assertRedirects(response, 'http://testserver/getuser/') # And they are now logged in as testuser (the passed in # nickname has not caused the username to change). response = self.client.get('/getuser/') self.assertEquals(response.content, 'testuser') # The user's full name and email have been updated. user = User.objects.get(username='testuser') self.assertEquals(user.first_name, 'Firstname') self.assertEquals(user.last_name, 'Lastname') self.assertEquals(user.email, 'foo@example.com') def test_login_teams(self): settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = False settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {'teamname': 'groupname', 'otherteam': 'othergroup'} user = User.objects.create_user('testuser', 'someone@example.com') group = Group(name='groupname') group.save() ogroup = Group(name='othergroup') ogroup.save() user.groups.add(ogroup) user.save() useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) self.assertContains(response, 'OpenID transaction in progress') # Complete the request openid_request = self.provider.parseFormPost(response.content) openid_response = openid_request.answer(True) teams_request = teams.TeamsRequest.fromOpenIDRequest(openid_request) teams_response = teams.TeamsResponse.extractResponse( teams_request, 'teamname,some-other-team') openid_response.addExtension(teams_response) response = self.complete(openid_response) self.assertRedirects(response, 'http://testserver/getuser/') # And they are now logged in as testuser response = self.client.get('/getuser/') self.assertEquals(response.content, 'testuser') # The user's groups have been updated. user = User.objects.get(username='testuser') self.assertTrue(group in user.groups.all()) self.assertTrue(ogroup not in user.groups.all()) def test_login_teams_automapping(self): settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {'teamname': 'groupname', 'otherteam': 'othergroup'} settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = True settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST = ['django-group1', 'django-group2'] user = User.objects.create_user('testuser', 'someone@example.com') group1 = Group(name='django-group1') group1.save() group2 = Group(name='django-group2') group2.save() group3 = Group(name='django-group3') group3.save() user.save() useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity', 'next': '/getuser/'}) self.assertContains(response, 'OpenID transaction in progress') # Complete the request openid_request = self.provider.parseFormPost(response.content) openid_response = openid_request.answer(True) teams_request = teams.TeamsRequest.fromOpenIDRequest(openid_request) self.assertEqual(group1 in user.groups.all(), False) self.assertEqual(group2 in user.groups.all(), False) self.assertTrue(group3 not in user.groups.all()) def test_login_teams_staff_not_defined(self): delattr(settings, 'OPENID_LAUNCHPAD_STAFF_TEAMS') 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): settings.OPENID_LAUNCHPAD_STAFF_TEAMS = ('teamname',) user = User.objects.create_user('testuser', 'someone@example.com') user.is_staff = False user.save() self.assertFalse(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_unassignment(self): settings.OPENID_LAUNCHPAD_STAFF_TEAMS = ('different-teamname',) 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.assertFalse(user.is_staff) def get_openid_authed_user_with_teams(self, user, teams_str): useropenid = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() # Posting in an identity URL begins the authentication request: response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity'}) # 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 = UserOpenID( user=user, claimed_id='http://example.com/identity', display_id='http://example.com/identity') useropenid.save() response = self.client.post('/openid/login/', {'openid_identifier': 'http://example.com/identity'}) 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.assertTrue(isinstance( kwargs.get('request', None), HttpRequest)) self.assertTrue(isinstance( 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) class HelperFunctionsTest(TestCase): def test_sanitise_redirect_url(self): settings.ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [ "example.com", "example.org"] # list of URLs and whether they should be passed or not urls = [ ("http://example.com", True), ("http://example.org/", True), ("http://example.org/foo/bar", True), ("http://example.org/foo/bar?baz=quux", True), ("http://example.org:9999/foo/bar?baz=quux", True), ("http://www.example.org/", False), ("http://example.net/foo/bar?baz=quux", False), ("/somewhere/local", True), ("/somewhere/local?url=http://fail.com/bar", True), # An empty path, as seen when no "next" parameter is passed. ("", False), ("/path with spaces", False), ] for url, returns_self in urls: sanitised = sanitise_redirect_url(url) if returns_self: self.assertEqual(url, sanitised) else: self.assertEqual(settings.LOGIN_REDIRECT_URL, sanitised) def suite(): return unittest.TestLoader().loadTestsFromName(__name__) django-openid-auth-0.5/django_openid_auth/tests/test_admin.py0000664000175000017500000000632212120173233025700 0ustar anthonyanthony00000000000000# 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. """ import os import unittest from django.conf import settings from django.contrib.auth.models import User, AnonymousUser settings.OPENID_USE_AS_ADMIN_LOGIN = True from django_openid_auth import admin from django.test import TestCase def create_user(is_staff=False, authenticated=True): """ Create and return a user, either the AnonymousUser or a normal Django user, setting the is_staff attribute if appropriate. """ if not authenticated: return AnonymousUser() else: user = User( username=u'testing', email='testing@example.com', is_staff=is_staff) user.set_password(u'test') user.save() 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. """ create_user() self.client.login(username='testing', password='test') response = self.client.get('/admin/') self.assertTrue('User testing does not have admin access.' in response.content, 'Missing error message in response') 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/') self.assertEqual(302, response.status_code) self.assertEqual('http://testserver/openid/login/?next=/admin/', response['Location']) def suite(): return unittest.TestLoader().loadTestsFromName(__name__) django-openid-auth-0.5/django_openid_auth/tests/__init__.py0000664000175000017500000000332512120172612025310 0ustar anthonyanthony00000000000000# 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. import unittest from test_views import * from test_store import * from test_auth import * from test_admin import * def suite(): suite = unittest.TestSuite() for name in ['test_auth', 'test_store', 'test_views', 'test_admin']: mod = __import__('%s.%s' % (__name__, name), {}, {}, ['suite']) suite.addTest(mod.suite()) return suite django-openid-auth-0.5/django_openid_auth/tests/test_store.py0000664000175000017500000001772712120172607025763 0ustar anthonyanthony00000000000000# 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. import time import unittest from django.test import TestCase from openid.association import Association as OIDAssociation from openid.store.nonce import SKEW from django_openid_auth.models import Association, Nonce from django_openid_auth.store import DjangoOpenIDStore class OpenIDStoreTests(TestCase): def setUp(self): super(OpenIDStoreTests, self).setUp() self.store = DjangoOpenIDStore() def test_storeAssociation(self): assoc = OIDAssociation('handle', 'secret', 42, 600, 'HMAC-SHA1') self.store.storeAssociation('server-url', assoc) dbassoc = Association.objects.get( server_url='server-url', handle='handle') self.assertEquals(dbassoc.server_url, 'server-url') self.assertEquals(dbassoc.handle, 'handle') self.assertEquals(dbassoc.secret, 'secret'.encode('base-64')) self.assertEquals(dbassoc.issued, 42) self.assertEquals(dbassoc.lifetime, 600) self.assertEquals(dbassoc.assoc_type, 'HMAC-SHA1') def test_storeAssociation_update_existing(self): assoc = OIDAssociation('handle', 'secret', 42, 600, 'HMAC-SHA1') self.store.storeAssociation('server-url', assoc) # Now update the association with new information. assoc = OIDAssociation('handle', 'secret2', 420, 900, 'HMAC-SHA256') self.store.storeAssociation('server-url', assoc) dbassoc = Association.objects.get( server_url='server-url', handle='handle') self.assertEqual(dbassoc.secret, 'secret2'.encode('base-64')) self.assertEqual(dbassoc.issued, 420) self.assertEqual(dbassoc.lifetime, 900) self.assertEqual(dbassoc.assoc_type, 'HMAC-SHA256') def test_getAssociation(self): timestamp = int(time.time()) self.store.storeAssociation( 'server-url', OIDAssociation('handle', 'secret', timestamp, 600, 'HMAC-SHA1')) assoc = self.store.getAssociation('server-url', 'handle') self.assertTrue(isinstance(assoc, OIDAssociation)) self.assertEquals(assoc.handle, 'handle') self.assertEquals(assoc.secret, 'secret') self.assertEquals(assoc.issued, timestamp) self.assertEquals(assoc.lifetime, 600) self.assertEquals(assoc.assoc_type, 'HMAC-SHA1') def test_getAssociation_unknown(self): assoc = self.store.getAssociation('server-url', 'unknown') self.assertEquals(assoc, None) def test_getAssociation_expired(self): lifetime = 600 timestamp = int(time.time()) - 2 * lifetime self.store.storeAssociation( 'server-url', OIDAssociation('handle', 'secret', timestamp, lifetime, 'HMAC-SHA1')) # The association is not returned, and is removed from the database. assoc = self.store.getAssociation('server-url', 'handle') self.assertEquals(assoc, None) self.assertRaises(Association.DoesNotExist, Association.objects.get, server_url='server-url', handle='handle') def test_getAssociation_no_handle(self): timestamp = int(time.time()) self.store.storeAssociation( 'server-url', OIDAssociation('handle1', 'secret', timestamp + 1, 600, 'HMAC-SHA1')) self.store.storeAssociation( 'server-url', OIDAssociation('handle2', 'secret', timestamp, 600, 'HMAC-SHA1')) # The newest handle is returned. assoc = self.store.getAssociation('server-url', None) self.assertNotEquals(assoc, None) self.assertEquals(assoc.handle, 'handle1') self.assertEquals(assoc.issued, timestamp + 1) def test_removeAssociation(self): timestamp = int(time.time()) self.store.storeAssociation( 'server-url', OIDAssociation('handle', 'secret', timestamp, 600, 'HMAC-SHA1')) self.assertEquals( self.store.removeAssociation('server-url', 'handle'), True) self.assertEquals( self.store.getAssociation('server-url', 'handle'), None) def test_removeAssociation_unknown(self): self.assertEquals( self.store.removeAssociation('server-url', 'unknown'), False) def test_useNonce(self): timestamp = time.time() # The nonce can only be used once. self.assertEqual( self.store.useNonce('server-url', timestamp, 'salt'), True) self.assertEqual( self.store.useNonce('server-url', timestamp, 'salt'), False) self.assertEqual( self.store.useNonce('server-url', timestamp, 'salt'), False) def test_useNonce_expired(self): timestamp = time.time() - 2 * SKEW self.assertEqual( self.store.useNonce('server-url', timestamp, 'salt'), False) def test_useNonce_future(self): timestamp = time.time() + 2 * SKEW self.assertEqual( self.store.useNonce('server-url', timestamp, 'salt'), False) def test_cleanupNonces(self): timestamp = time.time() self.assertEqual( self.store.useNonce('server1', timestamp, 'salt1'), True) self.assertEqual( self.store.useNonce('server2', timestamp, 'salt2'), True) self.assertEqual( self.store.useNonce('server3', timestamp, 'salt3'), True) self.assertEqual(Nonce.objects.count(), 3) self.assertEqual( self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 3) self.assertEqual(Nonce.objects.count(), 0) # The nonces have now been cleared: self.assertEqual( self.store.useNonce('server1', timestamp, 'salt1'), True) self.assertEqual( self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 1) self.assertEqual( self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 0) def test_cleanupAssociations(self): timestamp = int(time.time()) - 100 self.store.storeAssociation( 'server-url', OIDAssociation('handle1', 'secret', timestamp, 50, 'HMAC-SHA1')) self.store.storeAssociation( 'server-url', OIDAssociation('handle2', 'secret', timestamp, 200, 'HMAC-SHA1')) self.assertEquals(self.store.cleanupAssociations(), 1) # The second (non-expired) association is left behind. self.assertNotEqual(self.store.getAssociation('server-url', 'handle2'), None) def suite(): return unittest.TestLoader().loadTestsFromName(__name__) django-openid-auth-0.5/django_openid_auth/tests/urls.py0000664000175000017500000000316612120172604024542 0ustar anthonyanthony00000000000000# 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 django.http import HttpResponse from django.conf.urls.defaults import * def get_user(request): return HttpResponse(request.user.username) urlpatterns = patterns('', (r'^getuser/$', get_user), (r'^openid/', include('django_openid_auth.urls')), ) django-openid-auth-0.5/django_openid_auth/tests/test_auth.py0000664000175000017500000001704212120172600025547 0ustar anthonyanthony00000000000000# 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. import unittest from django.conf import settings from django.contrib.auth.models import User from django.test import TestCase from django_openid_auth.auth import OpenIDBackend from openid.consumer.consumer import SuccessResponse from openid.consumer.discover import OpenIDServiceEndpoint from openid.message import Message, OPENID2_NS SREG_NS = "http://openid.net/sreg/1.0" AX_NS = "http://openid.net/srv/ax/1.0" class OpenIDBackendTests(TestCase): def setUp(self): super(OpenIDBackendTests, self).setUp() self.backend = OpenIDBackend() self.old_openid_use_email_for_username = getattr(settings, 'OPENID_USE_EMAIL_FOR_USERNAME', False) def tearDown(self): settings.OPENID_USE_EMAIL_FOR_USERNAME = \ self.old_openid_use_email_for_username def test_extract_user_details_sreg(self): endpoint = OpenIDServiceEndpoint() message = Message(OPENID2_NS) message.setArg(SREG_NS, "nickname", "someuser") message.setArg(SREG_NS, "fullname", "Some User") message.setArg(SREG_NS, "email", "foo@example.com") response = SuccessResponse( endpoint, message, signed_fields=message.toPostArgs().keys()) data = self.backend._extract_user_details(response) self.assertEqual(data, {"nickname": "someuser", "first_name": "Some", "last_name": "User", "email": "foo@example.com"}) def make_response_ax(self, schema="http://axschema.org/", fullname="Some User", nickname="someuser", email="foo@example.com", first=None, last=None): endpoint = OpenIDServiceEndpoint() message = Message(OPENID2_NS) attributes = [ ("nickname", schema + "namePerson/friendly", nickname), ("fullname", schema + "namePerson", fullname), ("email", schema + "contact/email", email), ] 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 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"}) 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"}) 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"}) def test_update_user_details_long_names(self): response = self.make_response_ax() user = User.objects.create_user('someuser', 'someuser@example.com', password=None) data = dict(first_name=u"Some56789012345678901234567890123", last_name=u"User56789012345678901234567890123", email=u"someotheruser@example.com") self.backend.update_user_details(user, data, response) self.assertEqual("Some56789012345678901234567890", user.first_name) self.assertEqual("User56789012345678901234567890", user.last_name) 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']) def test_preferred_username_email_munging(self): settings.OPENID_USE_EMAIL_FOR_USERNAME = True 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)) def suite(): return unittest.TestLoader().loadTestsFromName(__name__) django-openid-auth-0.5/django_openid_auth/signals.py0000664000175000017500000000301512120172574024052 0ustar anthonyanthony00000000000000# 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. import django.dispatch openid_login_complete = django.dispatch.Signal(providing_args=[ 'request', 'openid_response']) django-openid-auth-0.5/django_openid_auth/admin.py0000664000175000017500000000712612120173203023477 0ustar anthonyanthony00000000000000# 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 django.conf import settings from django.contrib import admin from django_openid_auth.models import Nonce, Association, UserOpenID from django_openid_auth.store import DjangoOpenIDStore class NonceAdmin(admin.ModelAdmin): list_display = ('server_url', 'timestamp') actions = ['cleanup_nonces'] def cleanup_nonces(self, request, queryset): store = DjangoOpenIDStore() count = store.cleanupNonces() self.message_user(request, "%d expired nonces removed" % count) cleanup_nonces.short_description = "Clean up expired nonces" admin.site.register(Nonce, NonceAdmin) class AssociationAdmin(admin.ModelAdmin): list_display = ('server_url', 'assoc_type') list_filter = ('assoc_type',) search_fields = ('server_url',) actions = ['cleanup_associations'] def cleanup_associations(self, request, queryset): store = DjangoOpenIDStore() count = store.cleanupAssociations() self.message_user(request, "%d expired associations removed" % count) cleanup_associations.short_description = "Clean up expired associations" admin.site.register(Association, AssociationAdmin) class UserOpenIDAdmin(admin.ModelAdmin): raw_id_fields = ('user',) list_display = ('user', 'claimed_id') search_fields = ('claimed_id',) admin.site.register(UserOpenID, UserOpenIDAdmin) # Support for allowing openid authentication for /admin (django.contrib.admin) if getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False): from django.http import HttpResponseRedirect from django_openid_auth import views def _openid_login(self, request, error_message='', extra_context=None): if request.user.is_authenticated(): if not request.user.is_staff: return views.default_render_failure( request, "User %s does not have admin access." % request.user.username) assert error_message, "Unknown Error: %s" % error_message else: # Redirect to openid login path, return HttpResponseRedirect( settings.LOGIN_URL + "?next=" + request.get_full_path()) # Overide the standard admin login form. admin.sites.AdminSite.login = _openid_login django-openid-auth-0.5/django_openid_auth/auth.py0000664000175000017500000003266212120172560023360 0ustar anthonyanthony00000000000000# 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.""" __metaclass__ = type from django.conf import settings from django.contrib.auth.models import User, Group from openid.consumer.consumer import SUCCESS from openid.extensions import 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) if pape_response is None or \ pape.AUTH_MULTI_FACTOR_PHYSICAL 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) return user def _extract_user_details(self, openid_response): email = fullname = first_name = last_name = nickname = None 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) 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 = u'' last_name = fullname return dict(email=email, nickname=nickname, 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 = 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 = 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 if updated: user.save() def update_groups_from_teams(self, user, teams_response): teams_mapping_auto = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO', False) teams_mapping_auto_blacklist = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST', []) teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {}) if teams_mapping_auto: #ignore teams_mapping. use all django-groups teams_mapping = dict() all_groups = Group.objects.exclude(name__in=teams_mapping_auto_blacklist) for group in all_groups: teams_mapping[group.name] = group.name if len(teams_mapping) == 0: return current_groups = set(user.groups.filter( name__in=teams_mapping.values())) desired_groups = set(Group.objects.filter( name__in=[teams_mapping[lp_team] for lp_team in teams_response.is_member if lp_team in teams_mapping])) for group in current_groups - desired_groups: user.groups.remove(group) for group in desired_groups - current_groups: user.groups.add(group) 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-0.5/django_openid_auth/exceptions.py0000664000175000017500000000501612120172555024575 0ustar anthonyanthony00000000000000# 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.""" 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-0.5/django_openid_auth/views.py0000664000175000017500000003115212120172553023547 0ustar anthonyanthony00000000000000# 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. 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 ( RequiredAttributeNotReturned, 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) return HttpResponseRedirect(redirect_url) else: form_html = openid_request.htmlMarkup( trust_root, return_to, form_tag_attrs={'id': 'openid_message'}) return HttpResponse(form_html, content_type='text/html;charset=UTF-8') def 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: return render_to_response(template_name, { 'form': login_form, redirect_field_name: redirect_to }, context_instance=RequestContext(request)) error = None consumer = make_consumer(request) try: openid_request = consumer.begin(openid_url) except DiscoveryFailure, exc: return render_failure( request, "OpenID discovery error: %s" % (str(exc),), status=500, exception=exc) # Request some user details. If the provider advertises support # for attribute exchange, use that. if openid_request.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)) 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 ( not field 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-0.5/django_openid_auth/__init__.py0000664000175000017500000000262112120172550024145 0ustar anthonyanthony00000000000000# 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-0.5/django_openid_auth/store.py0000664000175000017500000001132612120172546023551 0ustar anthonyanthony00000000000000# 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. import base64 import time from openid.association import Association as OIDAssociation from openid.store.interface import OpenIDStore from openid.store.nonce import SKEW from django_openid_auth.models import Association, Nonce class DjangoOpenIDStore(OpenIDStore): def __init__(self): self.max_nonce_age = 6 * 60 * 60 # Six hours def storeAssociation(self, server_url, association): try: assoc = Association.objects.get( server_url=server_url, handle=association.handle) except Association.DoesNotExist: assoc = Association( server_url=server_url, handle=association.handle, secret=base64.encodestring(association.secret), issued=association.issued, lifetime=association.lifetime, assoc_type=association.assoc_type) else: assoc.secret = base64.encodestring(association.secret) assoc.issued = association.issued assoc.lifetime = association.lifetime assoc.assoc_type = association.assoc_type assoc.save() def getAssociation(self, server_url, handle=None): assocs = [] if handle is not None: assocs = Association.objects.filter( server_url=server_url, handle=handle) else: assocs = Association.objects.filter(server_url=server_url) associations = [] expired = [] for assoc in assocs: association = OIDAssociation( assoc.handle, base64.decodestring(assoc.secret), assoc.issued, assoc.lifetime, assoc.assoc_type ) if association.getExpiresIn() == 0: expired.append(assoc) else: associations.append((association.issued, association)) for assoc in expired: assoc.delete() if not associations: return None associations.sort() return associations[-1][1] def removeAssociation(self, server_url, handle): assocs = list(Association.objects.filter( server_url=server_url, handle=handle)) assocs_exist = len(assocs) > 0 for assoc in assocs: assoc.delete() return assocs_exist def useNonce(self, server_url, timestamp, salt): if abs(timestamp - time.time()) > SKEW: return False try: ononce = Nonce.objects.get( server_url__exact=server_url, timestamp__exact=timestamp, salt__exact=salt) except Nonce.DoesNotExist: ononce = Nonce( server_url=server_url, timestamp=timestamp, salt=salt) ononce.save() return True return False def cleanupNonces(self, _now=None): if _now is None: _now = int(time.time()) expired = Nonce.objects.filter(timestamp__lt=_now - SKEW) count = expired.count() if count: expired.delete() return count def cleanupAssociations(self): now = int(time.time()) expired = Association.objects.extra( where=['issued + lifetime < %d' % now]) count = expired.count() if count: expired.delete() return count django-openid-auth-0.5/django_openid_auth/models.py0000664000175000017500000000446412120172544023703 0ustar anthonyanthony00000000000000# 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.models import User from django.db import models class Nonce(models.Model): server_url = models.CharField(max_length=2047) timestamp = models.IntegerField() salt = models.CharField(max_length=40) def __unicode__(self): return u"Nonce: %s, %s" % (self.server_url, self.salt) class Association(models.Model): server_url = models.TextField(max_length=2047) handle = models.CharField(max_length=255) secret = models.TextField(max_length=255) # Stored base64 encoded issued = models.IntegerField() lifetime = models.IntegerField() assoc_type = models.TextField(max_length=64) def __unicode__(self): return u"Association: %s, %s" % (self.server_url, self.handle) class UserOpenID(models.Model): user = models.ForeignKey(User) claimed_id = models.TextField(max_length=2047, unique=True) display_id = models.TextField(max_length=2047) django-openid-auth-0.5/django_openid_auth/urls.py0000664000175000017500000000324012120172542023372 0ustar anthonyanthony00000000000000# 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.defaults import * 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-0.5/django_openid_auth/teams.py0000664000175000017500000003365312120172536023534 0ustar anthonyanthony00000000000000# 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 openid.message import registerNamespaceAlias, \ NamespaceAliasRegistrationError from openid.extension import Extension from openid import oidutil try: basestring #pylint:disable-msg=W0104 except NameError: # For Python 2.2 basestring = (str, unicode) #pylint:disable-msg=W0622 __all__ = [ 'TeamsRequest', 'TeamsResponse', 'ns_uri', 'supportsTeams', ] ns_uri = 'http://ns.launchpad.net/2007/openid-teams' try: registerNamespaceAlias(ns_uri, 'lp') except NamespaceAliasRegistrationError, e: oidutil.log('registerNamespaceAlias(%r, %r) failed: %s' % (ns_uri, 'lp', str(e),)) def supportsTeams(endpoint): """Does the given endpoint advertise support for Launchpad Teams? @param endpoint: The endpoint object as returned by OpenID discovery @type endpoint: openid.consumer.discover.OpenIDEndpoint @returns: Whether an lp type was advertised by the endpoint @rtype: bool """ return endpoint.usesExtension(ns_uri) class TeamsNamespaceError(ValueError): """The Launchpad teams namespace was not found and could not be created using the expected name (there's another extension using the name 'lp') This is not I{illegal}, for OpenID 2, although it probably indicates a problem, since it's not expected that other extensions will re-use the alias that is in use for OpenID 1. If this is an OpenID 1 request, then there is no recourse. This should not happen unless some code has modified the namespaces for the message that is being processed. """ def getTeamsNS(message): """Extract the Launchpad teams namespace URI from the given OpenID message. @param message: The OpenID message from which to parse Launchpad teams. This may be a request or response message. @type message: C{L{openid.message.Message}} @returns: the lp namespace URI for the supplied message. The message may be modified to define a Launchpad teams namespace. @rtype: C{str} @raise ValueError: when using OpenID 1 if the message defines the 'lp' alias to be something other than a Launchpad teams type. """ # See if there exists an alias for the Launchpad teams type. alias = message.namespaces.getAlias(ns_uri) if alias is None: # There is no alias, so try to add one. (OpenID version 1) try: message.namespaces.addAlias(ns_uri, 'lp') except KeyError, why: # An alias for the string 'lp' already exists, but it's # defined for something other than Launchpad teams raise TeamsNamespaceError(why[0]) # we know that ns_uri defined, because it's defined in the # else clause of the loop as well, so disable the warning return ns_uri #pylint:disable-msg=W0631 class TeamsRequest(Extension): """An object to hold the state of a Launchpad teams request. @ivar query_membership: A comma separated list of Launchpad team names that the RP is interested in. @type required: [str] @group Consumer: requestField, requestTeams, getExtensionArgs, addToOpenIDRequest @group Server: fromOpenIDRequest, parseExtensionArgs """ ns_alias = 'lp' def __init__(self, query_membership=None, lp_ns_uri=ns_uri): """Initialize an empty Launchpad teams request""" Extension.__init__(self) self.query_membership = [] self.ns_uri = lp_ns_uri if query_membership: self.requestTeams(query_membership) # Assign getTeamsNS to a static method so that it can be # overridden for testing. _getTeamsNS = staticmethod(getTeamsNS) def fromOpenIDRequest(cls, request): """Create a Launchpad teams request that contains the fields that were requested in the OpenID request with the given arguments @param request: The OpenID request @type request: openid.server.CheckIDRequest @returns: The newly created Launchpad teams request @rtype: C{L{TeamsRequest}} """ self = cls() # Since we're going to mess with namespace URI mapping, don't # mutate the object that was passed in. message = request.message.copy() self.ns_uri = self._getTeamsNS(message) args = message.getArgs(self.ns_uri) self.parseExtensionArgs(args) return self fromOpenIDRequest = classmethod(fromOpenIDRequest) def parseExtensionArgs(self, args, strict=False): """Parse the unqualified Launchpad teams request parameters and add them to this object. This method is essentially the inverse of C{L{getExtensionArgs}}. This method restores the serialized Launchpad teams request fields. If you are extracting arguments from a standard OpenID checkid_* request, you probably want to use C{L{fromOpenIDRequest}}, which will extract the lp namespace and arguments from the OpenID request. This method is intended for cases where the OpenID server needs more control over how the arguments are parsed than that method provides. >>> args = message.getArgs(ns_uri) >>> request.parseExtensionArgs(args) @param args: The unqualified Launchpad teams arguments @type args: {str:str} @param strict: Whether requests with fields that are not defined in the Launchpad teams specification should be tolerated (and ignored) @type strict: bool @returns: None; updates this object """ items = args.get('query_membership') if items: for team_name in items.split(','): try: self.requestTeam(team_name, strict) except ValueError: if strict: raise def allRequestedTeams(self): """A list of all of the Launchpad teams that were requested. @rtype: [str] """ return self.query_membership def wereTeamsRequested(self): """Have any Launchpad teams been requested? @rtype: bool """ return bool(self.allRequestedTeams()) def __contains__(self, team_name): """Was this team in the request?""" return team_name in self.query_membership def requestTeam(self, team_name, strict=False): """Request the specified team from the OpenID user @param team_name: the unqualified Launchpad team name @type team_name: str @param strict: whether to raise an exception when a team is added to a request more than once @raise ValueError: when strict is set and the team was requested more than once """ if strict: if team_name in self.query_membership: raise ValueError('That team has already been requested') else: if team_name in self.query_membership: return self.query_membership.append(team_name) def requestTeams(self, query_membership, strict=False): """Add the given list of teams to the request @param query_membership: The Launchpad teams request @type query_membership: [str] @raise ValueError: when a team requested is not a string or strict is set and a team was requested more than once """ if isinstance(query_membership, basestring): raise TypeError('Teams should be passed as a list of ' 'strings (not %r)' % (type(query_membership),)) for team_name in query_membership: self.requestTeam(team_name, strict=strict) def getExtensionArgs(self): """Get a dictionary of unqualified Launchpad teams arguments representing this request. This method is essentially the inverse of C{L{parseExtensionArgs}}. This method serializes the Launchpad teams request fields. @rtype: {str:str} """ args = {} if self.query_membership: args['query_membership'] = ','.join(self.query_membership) return args class TeamsResponse(Extension): """Represents the data returned in a Launchpad teams response inside of an OpenID C{id_res} response. This object will be created by the OpenID server, added to the C{id_res} response object, and then extracted from the C{id_res} message by the Consumer. @ivar data: The Launchpad teams data, an array. @ivar ns_uri: The URI under which the Launchpad teams data was stored in the response message. @group Server: extractResponse @group Consumer: fromSuccessResponse @group Read-only dictionary interface: keys, iterkeys, items, iteritems, __iter__, get, __getitem__, keys, has_key """ ns_alias = 'lp' def __init__(self, is_member=None, lp_ns_uri=ns_uri): Extension.__init__(self) if is_member is None: self.is_member = [] else: self.is_member = is_member self.ns_uri = lp_ns_uri def addTeam(self, team_name): if team_name not in self.is_member: self.is_member.append(team_name) def extractResponse(cls, request, is_member_str): """Take a C{L{TeamsRequest}} and a list of Launchpad team values and create a C{L{TeamsResponse}} object containing that data. @param request: The Launchpad teams request object @type request: TeamsRequest @param is_member: The Launchpad teams data for this response, as a list of strings. @type is_member: {str:str} @returns: a Launchpad teams response object @rtype: TeamsResponse """ self = cls() self.ns_uri = request.ns_uri self.is_member = is_member_str.split(',') return self extractResponse = classmethod(extractResponse) # Assign getTeamsNS to a static method so that it can be # overridden for testing _getTeamsNS = staticmethod(getTeamsNS) def fromSuccessResponse(cls, success_response, signed_only=True): """Create a C{L{TeamsResponse}} object from a successful OpenID library response (C{L{openid.consumer.consumer.SuccessResponse}}) response message @param success_response: A SuccessResponse from consumer.complete() @type success_response: C{L{openid.consumer.consumer.SuccessResponse}} @param signed_only: Whether to process only data that was signed in the id_res message from the server. @type signed_only: bool @rtype: TeamsResponse @returns: A Launchpad teams response containing the data that was supplied with the C{id_res} response. """ self = cls() self.ns_uri = self._getTeamsNS(success_response.message) if signed_only: args = success_response.getSignedNS(self.ns_uri) else: args = success_response.message.getArgs(self.ns_uri) if "is_member" in args: is_member_str = args["is_member"] self.is_member = is_member_str.split(',') #self.is_member = args["is_member"] return self fromSuccessResponse = classmethod(fromSuccessResponse) def getExtensionArgs(self): """Get the fields to put in the Launchpad teams namespace when adding them to an id_res message. @see: openid.extension """ ns_args = {'is_member': ','.join(self.is_member),} return ns_args django-openid-auth-0.5/django_openid_auth/templates/0000775000175000017500000000000012120431074024030 5ustar anthonyanthony00000000000000django-openid-auth-0.5/django_openid_auth/templates/openid/0000775000175000017500000000000012120431074025306 5ustar anthonyanthony00000000000000django-openid-auth-0.5/django_openid_auth/templates/openid/login.html0000664000175000017500000000243412017423116027312 0ustar anthonyanthony00000000000000{% load i18n %} Sign in with your OpenID

Sign in with your OpenID

{% if form.errors %}

{% trans "Please correct errors below:" %}
{% if form.openid_identifier.errors %} {{ form.openid_identifier.errors|join:", " }} {% endif %} {% if form.next.errors %} {{ form.next.errors|join:", " }} {% endif %}

{% endif %}
{% csrf_token %}
{% trans "Sign In Using Your OpenID" %}

{{ form.openid_identifier }}
{% if next %} {% endif %}
django-openid-auth-0.5/django_openid_auth/templates/openid/failure.html0000664000175000017500000000034612017423116027631 0ustar anthonyanthony00000000000000 OpenID failed

OpenID failed

{{ message|escape }}

django-openid-auth-0.5/django_openid_auth/management/0000775000175000017500000000000012120431074024146 5ustar anthonyanthony00000000000000django-openid-auth-0.5/django_openid_auth/management/__init__.py0000664000175000017500000000255412120172533026267 0ustar anthonyanthony00000000000000# 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-0.5/django_openid_auth/management/commands/0000775000175000017500000000000012120431074025747 5ustar anthonyanthony00000000000000django-openid-auth-0.5/django_openid_auth/management/commands/__init__.py0000664000175000017500000000255412120172531030066 0ustar anthonyanthony00000000000000# 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-0.5/django_openid_auth/management/commands/openid_cleanup.py0000664000175000017500000000323312120172526031313 0ustar anthonyanthony00000000000000# 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 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() django-openid-auth-0.5/django_openid_auth/forms.py0000664000175000017500000000677612120172517023556 0ustar anthonyanthony00000000000000# 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 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-0.5/setup.py0000664000175000017500000000633212120172653017734 0ustar anthonyanthony00000000000000#!/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.5' setup( name='django-openid-auth', version=VERSION, author='Canonical Ltd', 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.1.2)', 'openid (>=2.2.0)'], ) django-openid-auth-0.5/PKG-INFO0000664000175000017500000000263412120431074017313 0ustar anthonyanthony00000000000000Metadata-Version: 1.1 Name: django-openid-auth Version: 0.5 Summary: OpenID integration for django.contrib.auth Home-page: https://launchpad.net/django-openid-auth Author: Canonical Ltd Author-email: UNKNOWN License: BSD Download-URL: http://launchpad.net/django-openid-auth/trunk/0.5/+download/django-openid-auth-0.5.tar.gz Description: 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. Platform: any Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires: django (>=1.1.2) Requires: openid (>=2.2.0) Provides: django_openid_auth django-openid-auth-0.5/MANIFEST.in0000664000175000017500000000027412017423116017755 0ustar anthonyanthony00000000000000include 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 django-openid-auth-0.5/Makefile0000664000175000017500000000046112017423116017655 0ustar anthonyanthony00000000000000 check: PYTHONPATH=$(shell pwd) python example_consumer/manage.py test \ --verbosity=2 django_openid_auth run-example-consumer: PYTHONPATH=$(shell pwd) python example_consumer/manage.py syncdb PYTHONPATH=$(shell pwd) python example_consumer/manage.py runserver .PHONY: check run-example-consumer django-openid-auth-0.5/LICENSE.txt0000664000175000017500000000243312017423116020041 0ustar anthonyanthony00000000000000Copyright (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. django-openid-auth-0.5/example_consumer/0000775000175000017500000000000012120431074021557 5ustar anthonyanthony00000000000000django-openid-auth-0.5/example_consumer/manage.py0000775000175000017500000000104212017423116023364 0ustar anthonyanthony00000000000000#!/usr/bin/env python from django.core.management import execute_manager try: import settings # Assumed to be in the same directory. except ImportError: import sys sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) sys.exit(1) if __name__ == "__main__": execute_manager(settings) django-openid-auth-0.5/example_consumer/views.py0000664000175000017500000000435312120172500023267 0ustar anthonyanthony00000000000000# 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 = ['

'] if request.user.is_authenticated(): s.append('You are signed in as %s (%s)' % ( escape(request.user.username), escape(request.user.get_full_name()))) s.append(' | Sign out') else: s.append('Sign in with OpenID') s.append('

') s.append('

This requires authentication

') return HttpResponse('\n'.join(s)) def next_works(request): return HttpResponse('?next= bit works. Home') @login_required def require_authentication(request): return HttpResponse('This page requires authentication') django-openid-auth-0.5/example_consumer/__init__.py0000664000175000017500000000262012120172476023700 0ustar anthonyanthony00000000000000# 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-0.5/example_consumer/urls.py0000664000175000017500000000340612120172474023127 0ustar anthonyanthony00000000000000# 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.defaults import * from django.contrib import admin import views admin.autodiscover() urlpatterns = patterns('', (r'^$', views.index), (r'^openid/', include('django_openid_auth.urls')), (r'^logout/$', 'django.contrib.auth.views.logout'), (r'^private/$', views.require_authentication), (r'^admin/', include(admin.site.urls)), ) django-openid-auth-0.5/example_consumer/settings.py0000664000175000017500000001254312120406430023774 0ustar anthonyanthony00000000000000# 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 project. import django django_version = django.get_version() DEBUG = True TEMPLATE_DEBUG = DEBUG ADMINS = ( # ('Your Name', 'your_email@domain.com'), ) MANAGERS = ADMINS if django_version >= "1.2": csrf_middleware = 'django.middleware.csrf.CsrfViewMiddleware' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'sqlite.db' } } TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ) else: csrf_middleware = 'django.contrib.csrf.middleware.CsrfViewMiddleware' TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.load_template_source', 'django.template.loaders.app_directories.load_template_source', ) DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'. DATABASE_NAME = 'sqlite.db' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. # Local time zone for this installation. Choices can be found here: # http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE # although not all variations may be possible on all operating systems. # If running in a Windows environment this must be set to the same as your # system time zone. TIME_ZONE = 'America/Chicago' # Language code for this installation. All choices can be found here: # http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes # http://blogs.law.harvard.edu/tech/stories/storyReader$15 LANGUAGE_CODE = 'en-us' SITE_ID = 1 # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = True # Absolute path to the directory that holds media. # Example: "/home/media/media.lawrence.com/" MEDIA_ROOT = '' # URL that handles the media served from MEDIA_ROOT. # Example: "http://media.lawrence.com" MEDIA_URL = '' # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". ADMIN_MEDIA_PREFIX = '/media/' # Make this unique, and don't share it with anybody. SECRET_KEY = '34958734985734985734985798437' MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', csrf_middleware, ) ROOT_URLCONF = 'example_consumer.urls' TEMPLATE_DIRS = ( # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. ) INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.admin', 'django_openid_auth', ) 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 # 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.launchpad.net/' # 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-0.5/TODO.txt0000664000175000017500000000000012017423116017510 0ustar anthonyanthony00000000000000