├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── README.rst ├── admin_sso ├── __init__.py ├── admin.py ├── apps.py ├── auth.py ├── default_settings.py ├── models.py ├── openid │ ├── __init__.py │ ├── auth.py │ ├── models.py │ ├── store.py │ └── views.py ├── templates │ └── admin_sso │ │ ├── login.html │ │ └── request_form.html ├── tests │ ├── __init__.py │ ├── runner.py │ ├── test_auth.py │ └── test_views.py └── views.py ├── example ├── __init__.py ├── settings.py ├── settings_openid.py ├── urls.py └── wsgi.py ├── manage.py ├── setup.cfg ├── setup.py └── test_requirements.txt /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = admin_sso 3 | omit = *migrations*, *tests* 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | .*.swp 4 | \#*# 5 | .DS_Store 6 | ._* 7 | /pennyblack.egg-info 8 | *.sqlite 9 | /dist 10 | /build 11 | .coverage 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "2.6" 5 | env: 6 | - DJANGO_VERSION="django>=1.4,<1.5" 7 | - DJANGO_VERSION="django>=1.5,<1.6" 8 | - DJANGO_VERSION="django>=1.6,<1.7" 9 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 10 | install: 11 | - pip install $DJANGO_VERSION --use-mirrors 12 | - python setup.py install 13 | - pip install -r test_requirements.txt 14 | - pip install coveralls 15 | # command to run tests, e.g. python setup.py test 16 | script: 17 | - "coverage run manage.py test" 18 | - "DJANGO_SETTINGS_MODULE=example.settings_openid coverage run -a manage.py test" 19 | - "coverage report -m" 20 | after_success: 21 | - coveralls 22 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The authors of django-admin-sso are: 2 | 3 | * Marc Egli 4 | * George Hickman 5 | * Marc Tamlyn 6 | * Charlie Denton 7 | * Adam Thomas 8 | * Matthias Kestenholz 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, allink GmbH and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of allink GmbH nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.rst 4 | recursive-include admin_sso/templates *.html 5 | recursive-include admin_sso/locale *.po 6 | recursive-include admin_sso/locale *.mo 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Django admin SSO 3 | ================ 4 | 5 | .. image:: https://travis-ci.org/frog32/django-admin-sso.png?branch=master 6 | :target: https://travis-ci.org/frog32/django-admin-sso 7 | 8 | .. image:: https://coveralls.io/repos/frog32/django-admin-sso/badge.png?branch=master 9 | :target: https://coveralls.io/r/frog32/django-admin-sso 10 | 11 | .. image:: https://pypip.in/v/django-admin-sso/badge.png 12 | :target: https://pypi.python.org/pypi/django-admin-sso/ 13 | 14 | Django admin SSO lets users login to a django admin using an OAuth2 or an 15 | openid provider. It then looks up the email address of the new user and looks 16 | up the rights for them. 17 | 18 | Installation 19 | ------------ 20 | 21 | 1. Make sure you have a working django project setup. 22 | 2. Install django-admin-sso using pip:: 23 | 24 | pip install django-admin-sso 25 | 26 | 3. Add ``admin_sso`` to ``INSTALLED_APPS`` in your ``settings.py`` file:: 27 | 28 | INSTALLED_APPS = ( 29 | ... 30 | 'admin_sso', 31 | ... 32 | ) 33 | 34 | 4. Add the django-admin authentication backend:: 35 | 36 | AUTHENTICATION_BACKENDS = ( 37 | 'admin_sso.auth.DjangoSSOAuthBackend', 38 | 'django.contrib.auth.backends.ModelBackend', 39 | ) 40 | 41 | 5. Insert your oauth client id and secret key into your settings file:: 42 | 43 | DJANGO_ADMIN_SSO_OAUTH_CLIENT_ID = 'your client id here' 44 | DJANGO_ADMIN_SSO_OAUTH_CLIENT_SECRET = 'your client secret here' 45 | 46 | Navigate to Google's 47 | `Developer Console `_, create a 48 | new project, and create a new client ID under the menu point "APIs & AUTH", 49 | "Credentials". The redirect URI should be of the form 50 | ``http://example.com/admin/admin_sso/assignment/end/`` 51 | 52 | If you don't specify a client id django-admin-sso will fallback to openid. 53 | 54 | 6. Run syncdb to create the needed database tables. 55 | 56 | 7. Log into the admin and add an Assignment. 57 | 58 | 59 | Assignments 60 | ----------- 61 | 62 | Any Remote User -> Local User X 63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | * Select Username mode "any". 65 | * Set Domain to your authenticating domain. 66 | * Select your local user from the User drop down. 67 | 68 | 69 | Remote User -> Local User 70 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 71 | * Select Username mode "matches" *or* "don't match". 72 | * Set username to [not] match by. 73 | * Set Domain to your authenticating domain. 74 | * Select your local user from the User drop down. 75 | 76 | 77 | Changelog 78 | --------- 79 | 80 | 1.0 81 | ~~~ 82 | 83 | * Add support for OAuth2.0 since google closes its OpenID endpoint https://developers.google.com/accounts/docs/OpenID 84 | * Using OpenID is now deprecated and OpenID support will be removed in a future release. 85 | * Add more tests to get a decent coverage. 86 | -------------------------------------------------------------------------------- /admin_sso/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (1, 0, 0,) 2 | __version__ = '.'.join(map(str, VERSION)) 3 | 4 | default_app_config = 'admin_sso.apps.AdminSSOConfig' 5 | 6 | # Do not use Django settings at module level as recommended 7 | try: 8 | from django.utils.functional import LazyObject 9 | except ImportError: 10 | pass 11 | else: 12 | class LazySettings(LazyObject): 13 | def _setup(self): 14 | from admin_sso import default_settings 15 | self._wrapped = Settings(default_settings) 16 | 17 | class Settings(object): 18 | def __init__(self, settings_module): 19 | for setting in dir(settings_module): 20 | if setting == setting.upper(): 21 | setattr(self, setting, getattr(settings_module, setting)) 22 | 23 | settings = LazySettings() 24 | -------------------------------------------------------------------------------- /admin_sso/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | from django.contrib import admin 3 | 4 | from admin_sso import settings 5 | from admin_sso.models import Assignment 6 | 7 | 8 | class AssignmentAdmin(admin.ModelAdmin): 9 | list_display = ('__unicode__', 'username', 'username_mode', 'domain', 10 | 'user', 'weight') 11 | list_editable = ('username', 'username_mode', 'domain', 'user', 'weight') 12 | 13 | def get_urls(self): 14 | urls = super(AssignmentAdmin, self).get_urls() 15 | info = self.model._meta.app_label, self.model._meta.module_name 16 | if settings.DJANGO_ADMIN_SSO_USE_OAUTH: 17 | my_urls = patterns('admin_sso.views', 18 | url(r'^start/$', 'start', 19 | name='%s_%s_start' % info), 20 | url(r'^end/$', 'end', 21 | name='%s_%s_end' % info), 22 | ) 23 | else: 24 | from admin_sso.openid.views import StartOpenIDView, FinishOpenIDView 25 | my_urls = patterns('', 26 | url(r'^start/$', StartOpenIDView.as_view(), 27 | name='%s_%s_start' % info), 28 | url(r'^end/$', FinishOpenIDView.as_view(), 29 | name='%s_%s_return' % info), 30 | ) 31 | return my_urls + urls 32 | 33 | admin.site.register(Assignment, AssignmentAdmin) 34 | 35 | 36 | class OpenIDUserAdmin(admin.ModelAdmin): 37 | list_display = ('__unicode__', 'email', 'user') 38 | 39 | if not settings.DJANGO_ADMIN_SSO_USE_OAUTH: 40 | from .openid.models import OpenIDUser 41 | admin.site.register(OpenIDUser, OpenIDUserAdmin) 42 | 43 | if settings.DJANGO_ADMIN_SSO_ADD_LOGIN_BUTTON: 44 | admin.site.login_template = 'admin_sso/login.html' 45 | -------------------------------------------------------------------------------- /admin_sso/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | class AdminSSOConfig(AppConfig): 6 | name = 'admin_sso' 7 | verbose_name = _("Admin Single Sign-On") 8 | -------------------------------------------------------------------------------- /admin_sso/auth.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.contrib.auth import get_user_model 3 | except ImportError: # django < 1.5 4 | from django.contrib.auth.models import User 5 | else: 6 | User = get_user_model() 7 | 8 | from admin_sso import settings 9 | from admin_sso.models import Assignment 10 | 11 | 12 | class DjangoSSOAuthBackend(object): 13 | 14 | def get_user(self, user_id): 15 | try: 16 | return User.objects.get(pk=user_id) 17 | except User.DoesNotExist: 18 | return None 19 | 20 | def authenticate(self, **kwargs): 21 | if not settings.DJANGO_ADMIN_SSO_USE_OAUTH: 22 | from .openid.auth import authenticate as authenticate_openid 23 | return authenticate_openid(self, **kwargs) 24 | 25 | sso_email = kwargs.pop('sso_email', None) 26 | 27 | assignment = Assignment.objects.for_email(sso_email) 28 | if assignment is None: 29 | return None 30 | return assignment.user 31 | -------------------------------------------------------------------------------- /admin_sso/default_settings.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from django.conf import settings 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | # use oauth is set if settings has a client id 6 | DJANGO_ADMIN_SSO_USE_OAUTH = hasattr(settings, 'DJANGO_ADMIN_SSO_OAUTH_CLIENT_ID') 7 | 8 | if not DJANGO_ADMIN_SSO_USE_OAUTH: 9 | warnings.warn( 10 | "OpenID support is deprecated add DJANGO_ADMIN_SSO_OAUTH_CLIENT_ID and DJANGO_ADMIN_SSO_OAUTH_CLIENT_SECRET to your settings to use OAuth", 11 | DeprecationWarning) 12 | 13 | ASSIGNMENT_ANY = 0 14 | ASSIGNMENT_MATCH = 1 15 | ASSIGNMENT_EXCEPT = 2 16 | ASSIGNMENT_CHOICES = ((ASSIGNMENT_ANY, _('any')), 17 | (ASSIGNMENT_MATCH, _("matches")), 18 | (ASSIGNMENT_EXCEPT, _("don't match"))) 19 | 20 | DJANGO_ADMIN_SSO_ADD_LOGIN_BUTTON = getattr(settings, 'DJANGO_ADMIN_SSO_ADD_LOGIN_BUTTON', True) 21 | 22 | AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') 23 | 24 | DJANGO_ADMIN_SSO_OAUTH_CLIENT_ID = getattr(settings, 'DJANGO_ADMIN_SSO_OAUTH_CLIENT_ID', None) 25 | DJANGO_ADMIN_SSO_OAUTH_CLIENT_SECRET = getattr(settings, 'DJANGO_ADMIN_SSO_OAUTH_CLIENT_SECRET', None) 26 | 27 | DJANGO_ADMIN_SSO_AUTH_URI = getattr( 28 | settings, 'DJANGO_ADMIN_SSO_AUTH_URI', 29 | 'https://accounts.google.com/o/oauth2/auth') 30 | DJANGO_ADMIN_SSO_TOKEN_URI = getattr( 31 | settings, 'DJANGO_ADMIN_SSO_TOKEN_URI', 32 | 'https://accounts.google.com/o/oauth2/token') 33 | DJANGO_ADMIN_SSO_REVOKE_URI = getattr( 34 | settings, 'DJANGO_ADMIN_SSO_REVOKE_URI', 35 | 'https://accounts.google.com/o/oauth2/revoke') 36 | 37 | 38 | # settings for deprecated openid part 39 | AX_MAPPING = (('http://schema.openid.net/contact/email', 'email'), 40 | ('http://schema.openid.net/namePerson', 'fullname'), 41 | ('http://axschema.org/contact/email', 'email'), 42 | ('http://axschema.org/namePerson', 'fullname'), 43 | ('http://axschema.org/namePerson/first', 'firstname'), 44 | ('http://axschema.org/namePerson/last', 'lastname')) 45 | 46 | DJANGO_ADMIN_SSO_OPENID_ENDPOINT = getattr(settings, 'DJANGO_ADMIN_SSO_OPENID_ENDPOINT', 'https://www.google.com/accounts/o8/id') 47 | -------------------------------------------------------------------------------- /admin_sso/models.py: -------------------------------------------------------------------------------- 1 | import fnmatch 2 | from django.db import models 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from admin_sso import settings 6 | 7 | 8 | class AssignmentManager(models.Manager): 9 | def for_email(self, email): 10 | if not email: 11 | return None 12 | 13 | try: 14 | username, domain = email.split('@') 15 | except ValueError: 16 | return None 17 | possible_assignments = self.filter(domain=domain) 18 | used_assignment = None 19 | for assignment in possible_assignments: 20 | if assignment.username_mode == settings.ASSIGNMENT_ANY: 21 | used_assignment = assignment 22 | break 23 | elif assignment.username_mode == settings.ASSIGNMENT_MATCH: 24 | if fnmatch.fnmatch(username, assignment.username): 25 | used_assignment = assignment 26 | break 27 | elif assignment.username_mode == settings.ASSIGNMENT_EXCEPT: 28 | if not fnmatch.fnmatch(username, assignment.username): 29 | used_assignment = assignment 30 | break 31 | if used_assignment is None: 32 | return None 33 | return used_assignment 34 | 35 | 36 | class Assignment(models.Model): 37 | username_mode = models.IntegerField(choices=settings.ASSIGNMENT_CHOICES) 38 | username = models.CharField(max_length=255, blank=True) 39 | domain = models.CharField(max_length=255) 40 | copy = models.BooleanField(default=False) 41 | weight = models.PositiveIntegerField(default=0) 42 | user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True) 43 | 44 | class Meta: 45 | verbose_name = _('Assignment') 46 | verbose_name_plural = _('Assignments') 47 | ordering = ('-weight',) 48 | 49 | def __unicode__(self): 50 | return u"%s(%s) @%s" % (dict(settings.ASSIGNMENT_CHOICES)[self.username_mode], self.username, self.domain) 51 | 52 | objects = AssignmentManager() 53 | 54 | 55 | if not settings.DJANGO_ADMIN_SSO_USE_OAUTH: 56 | from .openid.models import OpenIDUser, Association, Nonce # noqa 57 | -------------------------------------------------------------------------------- /admin_sso/openid/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frog32/django-admin-sso/04935aa31fa701ad1dd62c0aa1b9625235b555ad/admin_sso/openid/__init__.py -------------------------------------------------------------------------------- /admin_sso/openid/auth.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.contrib.auth import get_user_model 3 | except ImportError: # django < 1.5 4 | from django.contrib.auth.models import User 5 | else: 6 | User = get_user_model() 7 | 8 | from openid.extensions import ax, sreg 9 | 10 | from admin_sso import settings 11 | from admin_sso.models import Assignment, OpenIDUser 12 | 13 | 14 | def authenticate(self, **kwargs): 15 | response = kwargs.pop('openid_response', None) 16 | if not response: 17 | return None 18 | user_data = {} 19 | user_data['claimed_id'] = response.getDisplayIdentifier() 20 | sreg_response = sreg.SRegResponse.fromSuccessResponse(response) 21 | if sreg_response: 22 | for field_name in ('email', 'fullname',): 23 | user_data[field_name] = sreg_response.get(field_name, None) 24 | 25 | ax_response = ax.FetchResponse.fromSuccessResponse(response) 26 | if ax_response: 27 | for ax_name, field_name in settings.AX_MAPPING: 28 | value = ax_response.getSingle(ax_name) 29 | user_data[field_name] = value or user_data.get(field_name) 30 | try: 31 | openid_user = OpenIDUser.objects.get( 32 | claimed_id=user_data['claimed_id']) 33 | except OpenIDUser.DoesNotExist: 34 | pass 35 | else: 36 | openid_user.user.active_openid_user = openid_user 37 | return openid_user.user 38 | 39 | email = user_data.get('email') 40 | assignment = Assignment.objects.for_email(email) 41 | if assignment is None: 42 | return None 43 | 44 | first_and_lastname = u"%s %s" % ( 45 | user_data.pop('firstname', ""), user_data.pop('lastname', "")) 46 | user_data['fullname'] = user_data['fullname'] or first_and_lastname 47 | user_data.update(user=assignment.user) 48 | openid_user = OpenIDUser.objects.create(**user_data) 49 | openid_user.user.active_openid_user = openid_user 50 | return openid_user.user 51 | -------------------------------------------------------------------------------- /admin_sso/openid/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.timezone import now 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from admin_sso import settings 6 | 7 | 8 | class OpenIDUser(models.Model): 9 | claimed_id = models.TextField(max_length=2047) 10 | email = models.EmailField() 11 | fullname = models.CharField(max_length=255) 12 | user = models.ForeignKey(settings.AUTH_USER_MODEL) 13 | last_login = models.DateTimeField(_('last login'), default=now) 14 | 15 | class Meta: 16 | verbose_name = _('OpenIDUser') 17 | verbose_name_plural = _('OpenIDUsers') 18 | app_label = 'admin_sso' 19 | 20 | def __unicode__(self): 21 | return self.claimed_id 22 | 23 | def update_last_login(self): 24 | self.last_login = now() 25 | self.save() 26 | 27 | 28 | class Nonce(models.Model): 29 | server_url = models.CharField(max_length=2047) 30 | timestamp = models.IntegerField() 31 | salt = models.CharField(max_length=40) 32 | 33 | class Meta: 34 | app_label = 'admin_sso' 35 | 36 | 37 | class Association(models.Model): 38 | server_url = models.CharField(max_length=2047) 39 | handle = models.CharField(max_length=255) 40 | secret = models.CharField(max_length=255) 41 | issued = models.IntegerField() 42 | lifetime = models.IntegerField() 43 | assoc_type = models.CharField(max_length=64) 44 | 45 | class Meta: 46 | app_label = 'admin_sso' 47 | -------------------------------------------------------------------------------- /admin_sso/openid/store.py: -------------------------------------------------------------------------------- 1 | # this file is from django-openid-auth 2 | # django-openid-auth - OpenID integration for django.contrib.auth 3 | # 4 | # Copyright (C) 2007 Simon Willison 5 | # Copyright (C) 2008-2010 Canonical Ltd. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # 14 | # * Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | # POSSIBILITY OF SUCH DAMAGE. 30 | 31 | import base64 32 | import time 33 | 34 | from openid.association import Association as OIDAssociation 35 | from openid.store.interface import OpenIDStore 36 | from openid.store.nonce import SKEW 37 | 38 | from admin_sso.models import Association, Nonce 39 | 40 | 41 | class DjangoOpenIDStore(OpenIDStore): 42 | def __init__(self): 43 | self.max_nonce_age = 6 * 60 * 60 # Six hours 44 | 45 | def storeAssociation(self, server_url, association): 46 | try: 47 | assoc = Association.objects.get( 48 | server_url=server_url, handle=association.handle) 49 | except Association.DoesNotExist: 50 | assoc = Association( 51 | server_url=server_url, 52 | handle=association.handle, 53 | secret=base64.encodestring(association.secret), 54 | issued=association.issued, 55 | lifetime=association.lifetime, 56 | assoc_type=association.assoc_type) 57 | else: 58 | assoc.secret = base64.encodestring(association.secret) 59 | assoc.issued = association.issued 60 | assoc.lifetime = association.lifetime 61 | assoc.assoc_type = association.assoc_type 62 | assoc.save() 63 | 64 | def getAssociation(self, server_url, handle=None): 65 | assocs = [] 66 | if handle is not None: 67 | assocs = Association.objects.filter( 68 | server_url=server_url, handle=handle) 69 | else: 70 | assocs = Association.objects.filter(server_url=server_url) 71 | associations = [] 72 | expired = [] 73 | for assoc in assocs: 74 | association = OIDAssociation( 75 | assoc.handle, base64.decodestring(assoc.secret), assoc.issued, 76 | assoc.lifetime, assoc.assoc_type 77 | ) 78 | if association.getExpiresIn() == 0: 79 | expired.append(assoc) 80 | else: 81 | associations.append((association.issued, association)) 82 | for assoc in expired: 83 | assoc.delete() 84 | if not associations: 85 | return None 86 | associations.sort() 87 | return associations[-1][1] 88 | 89 | def removeAssociation(self, server_url, handle): 90 | assocs = list(Association.objects.filter( 91 | server_url=server_url, handle=handle)) 92 | assocs_exist = len(assocs) > 0 93 | for assoc in assocs: 94 | assoc.delete() 95 | return assocs_exist 96 | 97 | def useNonce(self, server_url, timestamp, salt): 98 | if abs(timestamp - time.time()) > SKEW: 99 | return False 100 | 101 | try: 102 | ononce = Nonce.objects.get( 103 | server_url__exact=server_url, 104 | timestamp__exact=timestamp, 105 | salt__exact=salt) 106 | except Nonce.DoesNotExist: 107 | ononce = Nonce( 108 | server_url=server_url, 109 | timestamp=timestamp, 110 | salt=salt) 111 | ononce.save() 112 | return True 113 | 114 | return False 115 | 116 | def cleanupNonces(self, _now=None): 117 | if _now is None: 118 | _now = int(time.time()) 119 | expired = Nonce.objects.filter(timestamp__lt=_now - SKEW) 120 | count = expired.count() 121 | if count: 122 | expired.delete() 123 | return count 124 | 125 | def cleanupAssociations(self): 126 | now = int(time.time()) 127 | expired = Association.objects.extra( 128 | where=['issued + lifetime < %d' % now]) 129 | count = expired.count() 130 | if count: 131 | expired.delete() 132 | return count 133 | -------------------------------------------------------------------------------- /admin_sso/openid/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sites.models import RequestSite 2 | from django.core.urlresolvers import reverse 3 | from django.contrib.auth import authenticate, login 4 | from django.http import HttpResponseRedirect 5 | from django.shortcuts import render 6 | from django.views.generic import View 7 | 8 | from openid.consumer import consumer 9 | from openid.extensions import ax, sreg 10 | 11 | from admin_sso import settings 12 | from .store import DjangoOpenIDStore 13 | 14 | openid_store = DjangoOpenIDStore() 15 | 16 | 17 | class OpenIDMixin(object): 18 | return_to_url = 'admin:admin_sso_assignment_return' 19 | 20 | def get_url(self, url=None): 21 | scheme = self.request.is_secure() and 'https' or 'http' 22 | primary_site = RequestSite(self.request) 23 | if url: 24 | path = reverse(url) 25 | else: 26 | path = "/" 27 | return '%s://%s%s' % (scheme, primary_site.domain, path) 28 | 29 | def get_consumer(self): 30 | return consumer.Consumer(self.request.session, self.get_openid_store()) 31 | 32 | def get_openid_store(self): 33 | return openid_store 34 | 35 | 36 | class StartOpenIDView(View, OpenIDMixin): 37 | def get(self, request, *args, **kwargs): 38 | c = self.get_consumer() 39 | auth_request = c.begin(settings.DJANGO_ADMIN_SSO_OPENID_ENDPOINT) 40 | 41 | trust_root = self.get_url() 42 | return_to = self.get_url(self.return_to_url) 43 | if auth_request.endpoint.supportsType(ax.AXMessage.ns_uri): 44 | # Add Attribute Exchange request information. 45 | ax_request = ax.FetchRequest() 46 | # XXX - uses myOpenID-compatible schema values, which are 47 | # not those listed at axschema.org. 48 | for ax_name, field_name in settings.AX_MAPPING: 49 | ax_request.add(ax.AttrInfo(ax_name, required=True)) 50 | auth_request.addExtension(ax_request) 51 | else: 52 | sreg_request = sreg.SRegRequest(required=['email', 'nickname']) 53 | auth_request.addExtension(sreg_request) 54 | 55 | if auth_request.shouldSendRedirect(): 56 | url = auth_request.redirectURL(trust_root, return_to) 57 | return HttpResponseRedirect(url) 58 | form_id = 'openid_message' 59 | form_html = auth_request.formMarkup(trust_root, return_to, 60 | False, {'id': form_id}) 61 | return render( 62 | request, 'admin_sso/request_form.html', {'html': form_html}) 63 | 64 | 65 | class FinishOpenIDView(View, OpenIDMixin): 66 | def get(self, request, *args, **kwargs): 67 | if request.REQUEST: 68 | c = self.get_consumer() 69 | return_to = self.get_url(self.return_to_url) 70 | response = c.complete(request.REQUEST, return_to) 71 | if response.status == consumer.SUCCESS: 72 | user = authenticate(openid_response=response) 73 | if user and user.is_active: 74 | user.active_openid_user.update_last_login() 75 | login(request, user) 76 | return HttpResponseRedirect(reverse('admin:index')) 77 | 78 | def post(self, *args, **kwargs): 79 | return self.get(*args, **kwargs) 80 | -------------------------------------------------------------------------------- /admin_sso/templates/admin_sso/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/login.html" %} 2 | {% load i18n %} 3 | {% load url from future %} 4 | {% block content %} 5 | {{block.super}} 6 | {% trans "Log in using SSO" %} 7 | {% endblock content %} 8 | -------------------------------------------------------------------------------- /admin_sso/templates/admin_sso/request_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{ html|safe }} 5 |
6 | 7 | -------------------------------------------------------------------------------- /admin_sso/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from django.utils.unittest import skipIf 2 | 3 | from admin_sso import settings 4 | 5 | 6 | def skipIfOAuth(test_func): 7 | """ 8 | Skip a test if a custom user model is in use. 9 | """ 10 | return skipIf(settings.DJANGO_ADMIN_SSO_USE_OAUTH, 'Using OAuth')(test_func) 11 | 12 | 13 | def skipIfOpenID(test_func): 14 | """ 15 | Skip a test if a custom user model is in use. 16 | """ 17 | return skipIf(not settings.DJANGO_ADMIN_SSO_USE_OAUTH, 'Using OpenID')(test_func) 18 | -------------------------------------------------------------------------------- /admin_sso/tests/runner.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.test.runner import DiscoverRunner 3 | except ImportError: 4 | # Django < 1.6 5 | from discover_runner import DiscoverRunner 6 | 7 | 8 | class TestRunner(DiscoverRunner): 9 | pass 10 | -------------------------------------------------------------------------------- /admin_sso/tests/test_auth.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.contrib.auth import get_user_model 3 | except ImportError: # django < 1.5 4 | from django.contrib.auth.models import User 5 | else: 6 | User = get_user_model() 7 | from django.utils import unittest 8 | 9 | from openid.consumer.consumer import SuccessResponse 10 | from openid.consumer.discover import OpenIDServiceEndpoint 11 | from openid.message import Message, OPENID2_NS 12 | 13 | from admin_sso import settings 14 | from admin_sso.auth import DjangoSSOAuthBackend 15 | from admin_sso.models import Assignment 16 | from . import skipIfOpenID, skipIfOAuth 17 | 18 | 19 | SREG_NS = "http://openid.net/sreg/1.0" 20 | 21 | 22 | class AuthModuleTests(unittest.TestCase): 23 | def setUp(self): 24 | self.auth_module = DjangoSSOAuthBackend() 25 | self.user = User.objects.create(username='admin_sso1') 26 | self.assignment1 = Assignment.objects.create(username='', 27 | username_mode=settings.ASSIGNMENT_ANY, 28 | domain='example.com', 29 | user=self.user, 30 | weight=100) 31 | 32 | def tearDown(self): 33 | self.user.delete() 34 | Assignment.objects.all().delete() 35 | 36 | def test_empty_authenticate(self): 37 | user = self.auth_module.authenticate() 38 | self.assertEqual(user, None) 39 | 40 | @skipIfOpenID 41 | def test_simple_assignment(self): 42 | email = "foo@example.com" 43 | user = self.auth_module.authenticate(sso_email=email) 44 | self.assertEqual(user, self.user) 45 | 46 | def create_sreg_response(self, fullname='', email='', identifier=''): 47 | message = Message(OPENID2_NS) 48 | message.setArg(SREG_NS, "fullname", fullname) 49 | message.setArg(SREG_NS, "email", email) 50 | endpoint = OpenIDServiceEndpoint() 51 | endpoint.display_identifier = identifier 52 | return SuccessResponse(endpoint, message, signed_fields=message.toPostArgs().keys()) 53 | 54 | @skipIfOAuth 55 | def test_domain_matches(self): 56 | response = self.create_sreg_response(fullname="User Name", email="foo@example.com", identifier='7324') 57 | user = self.auth_module.authenticate(openid_response=response) 58 | self.assertEqual(user, self.user) 59 | 60 | def test_get_user(self): 61 | user = self.auth_module.get_user(self.user.id) 62 | self.assertEqual(user, self.user) 63 | 64 | user = self.auth_module.get_user(self.user.id + 42) 65 | self.assertEqual(user, None) 66 | 67 | 68 | class AssignmentManagerTests(unittest.TestCase): 69 | def setUp(self): 70 | self.user = User.objects.create(username='admin_sso1') 71 | self.assignment1 = Assignment.objects.create(username='', 72 | username_mode=settings.ASSIGNMENT_ANY, 73 | domain='example.com', 74 | user=self.user, 75 | weight=100) 76 | self.assignment2 = Assignment.objects.create(username='*bar', 77 | username_mode=settings.ASSIGNMENT_MATCH, 78 | domain='example.com', 79 | user=self.user, 80 | weight=200) 81 | self.assignment3 = Assignment.objects.create(username='foo*', 82 | username_mode=settings.ASSIGNMENT_EXCEPT, 83 | domain='example.com', 84 | user=self.user, 85 | weight=300) 86 | 87 | def tearDown(self): 88 | self.user.delete() 89 | Assignment.objects.all().delete() 90 | 91 | def test_domain_matches(self): 92 | email = "foo@example.com" 93 | user = Assignment.objects.for_email(email) 94 | self.assertEqual(user, self.assignment1) 95 | 96 | def test_invalid_domain(self): 97 | email = 'someone@someotherdomain.com' 98 | user = Assignment.objects.for_email(email) 99 | self.assertIsNone(user) 100 | 101 | def test_domain_matches_and_username_ends_with_bar(self): 102 | email = "foobar@example.com" 103 | user = Assignment.objects.for_email(email) 104 | self.assertEqual(user, self.assignment2) 105 | 106 | def test_domain_matches_and_username_doesnt_begin_with_foo(self): 107 | email = "bar@example.com" 108 | user = Assignment.objects.for_email(email) 109 | self.assertEqual(user, self.assignment3) 110 | 111 | def test_invalid_email(self): 112 | email = 'invalid' 113 | user = Assignment.objects.for_email(email) 114 | self.assertEqual(user, None) 115 | 116 | def test_change_weight(self): 117 | self.assignment2.weight = 50 118 | self.assignment2.save() 119 | email = "foobar@example.com" 120 | user = Assignment.objects.for_email(email) 121 | self.assertEqual(user, self.assignment1) 122 | -------------------------------------------------------------------------------- /admin_sso/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.test import TestCase, Client 3 | 4 | from . import skipIfOpenID, skipIfOAuth 5 | from admin_sso.models import Assignment 6 | from admin_sso import settings 7 | 8 | try: 9 | from django.contrib.auth import get_user_model 10 | except ImportError: # django < 1.5 11 | from django.contrib.auth.models import User 12 | else: 13 | User = get_user_model() 14 | 15 | 16 | class CredentialsMock(object): 17 | def __init__(self, **kwargs): 18 | for key, value in kwargs.items(): 19 | setattr(self, key, value) 20 | 21 | 22 | class FlowMock(object): 23 | """ 24 | object to mock a flow and return all arguments given to __init__ 25 | when calling step2_exchange 26 | """ 27 | 28 | def __init__(self, id_token): 29 | self.credentials = CredentialsMock(id_token=id_token) 30 | 31 | def step2_exchange(self, *args, **kwargs): 32 | return self.credentials 33 | 34 | 35 | @skipIfOpenID 36 | class OAuthViewTest(TestCase): 37 | def setUp(self): 38 | self.client = Client() 39 | self.user = User.objects.create(username='admin_sso') 40 | self.assignment = Assignment.objects.create(username='', 41 | username_mode=settings.ASSIGNMENT_ANY, 42 | domain='example.com', 43 | user=self.user, 44 | weight=100) 45 | 46 | def test_start_view(self): 47 | start_url = reverse('admin:admin_sso_assignment_start') 48 | rv = self.client.get(start_url) 49 | self.assertEqual(rv.status_code, 302) 50 | self.assertTrue('Location' in rv) 51 | self.assertTrue(rv['Location'].startswith(settings.DJANGO_ADMIN_SSO_AUTH_URI)) 52 | 53 | def test_end_without_code(self): 54 | end_url = reverse('admin:admin_sso_assignment_end') 55 | rv = self.client.get(end_url) 56 | self.assertEqual(rv.status_code, 302) 57 | self.assertTrue('Location' in rv) 58 | self.assertTrue(rv['Location'].endswith('/admin/')) 59 | 60 | def test_end_with_invalid_code(self): 61 | end_url = reverse('admin:admin_sso_assignment_end') 62 | rv = self.client.get(end_url + '?code=xxx') 63 | self.assertEqual(rv.status_code, 302) 64 | self.assertTrue('Location' in rv) 65 | self.assertTrue(rv['Location'].endswith('/admin/')) 66 | 67 | def test_end_with_sucess(self): 68 | from admin_sso import views 69 | setattr(views, 'flow_override', FlowMock({'email_verified': True, 'email': 'test@example.com'})) 70 | end_url = reverse('admin:admin_sso_assignment_end') 71 | rv = self.client.get(end_url + '?code=xxx') 72 | self.assertEqual(rv.status_code, 302) 73 | self.assertTrue('Location' in rv) 74 | self.assertEqual(self.client.session['_auth_user_id'], self.user.id) 75 | self.assertEqual(self.client.session['_auth_user_backend'], 'admin_sso.auth.DjangoSSOAuthBackend') 76 | setattr(views, 'flow_override', None) 77 | 78 | def test_end_with_email_not_verified(self): 79 | from admin_sso import views 80 | setattr(views, 'flow_override', FlowMock({'email_verified': False, 'email': 'test@example.com'})) 81 | end_url = reverse('admin:admin_sso_assignment_end') 82 | rv = self.client.get(end_url + '?code=xxx') 83 | self.assertEqual(rv.status_code, 302) 84 | self.assertTrue('Location' in rv) 85 | self.assertFalse('_auth_user_id' in self.client.session) 86 | self.assertFalse('_auth_user_backend' in self.client.session) 87 | setattr(views, 'flow_override', None) 88 | 89 | 90 | @skipIfOAuth 91 | class OpenIDViewTest(TestCase): 92 | def setUp(self): 93 | self.client = Client() 94 | self.user = User.objects.create(username='admin_sso') 95 | self.assignment = Assignment.objects.create(username='', 96 | username_mode=settings.ASSIGNMENT_ANY, 97 | domain='example.com', 98 | user=self.user, 99 | weight=100) 100 | 101 | def test_start_view(self): 102 | start_url = reverse('admin:admin_sso_assignment_start') 103 | rv = self.client.get(start_url) 104 | self.assertContains(rv, settings.DJANGO_ADMIN_SSO_OPENID_ENDPOINT[:-2]) 105 | -------------------------------------------------------------------------------- /admin_sso/views.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.contrib.auth import authenticate, login 3 | from django.http import HttpResponseRedirect 4 | 5 | from oauth2client.client import OAuth2WebServerFlow, FlowExchangeError 6 | 7 | from admin_sso import settings 8 | 9 | flow_kwargs = { 10 | 'client_id': settings.DJANGO_ADMIN_SSO_OAUTH_CLIENT_ID, 11 | 'client_secret': settings.DJANGO_ADMIN_SSO_OAUTH_CLIENT_SECRET, 12 | 'scope': 'email', 13 | } 14 | if settings.DJANGO_ADMIN_SSO_AUTH_URI: 15 | flow_kwargs['auth_uri'] = settings.DJANGO_ADMIN_SSO_AUTH_URI 16 | 17 | if settings.DJANGO_ADMIN_SSO_TOKEN_URI: 18 | flow_kwargs['token_uri'] = settings.DJANGO_ADMIN_SSO_TOKEN_URI 19 | 20 | if settings.DJANGO_ADMIN_SSO_REVOKE_URI: 21 | flow_kwargs['revoke_uri'] = settings.DJANGO_ADMIN_SSO_REVOKE_URI 22 | 23 | redirect_uri = '' # set this after end view has been defined 24 | 25 | flow_override = None 26 | 27 | 28 | def start(request): 29 | flow = OAuth2WebServerFlow( 30 | redirect_uri=request.build_absolute_uri(redirect_uri), 31 | **flow_kwargs) 32 | 33 | return HttpResponseRedirect(flow.step1_get_authorize_url()) 34 | 35 | 36 | def end(request): 37 | if flow_override is None: 38 | flow = OAuth2WebServerFlow( 39 | redirect_uri=request.build_absolute_uri(redirect_uri), 40 | **flow_kwargs) 41 | else: 42 | flow = flow_override 43 | 44 | code = request.GET.get('code', None) 45 | if not code: 46 | return HttpResponseRedirect(reverse('admin:index')) 47 | try: 48 | credentials = flow.step2_exchange(code) 49 | except FlowExchangeError: 50 | return HttpResponseRedirect(reverse('admin:index')) 51 | 52 | if credentials.id_token['email_verified']: 53 | email = credentials.id_token['email'] 54 | user = authenticate(sso_email=email) 55 | if user and user.is_active: 56 | login(request, user) 57 | return HttpResponseRedirect(reverse('admin:index')) 58 | 59 | # if anything fails redirect to admin:index 60 | return HttpResponseRedirect(reverse('admin:index')) 61 | 62 | 63 | redirect_uri = reverse('admin:admin_sso_assignment_end') 64 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frog32/django-admin-sso/04935aa31fa701ad1dd62c0aa1b9625235b555ad/example/__init__.py -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | ) 8 | 9 | MANAGERS = ADMINS 10 | 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'django.db.backends.sqlite3', 14 | 'NAME': 'db.sqlite', 15 | 'USER': '', 16 | 'PASSWORD': '', 17 | 'HOST': '', 18 | 'PORT': '', 19 | } 20 | } 21 | 22 | TIME_ZONE = 'America/Chicago' 23 | 24 | LANGUAGE_CODE = 'en-us' 25 | 26 | SITE_ID = 1 27 | 28 | USE_I18N = True 29 | 30 | USE_L10N = True 31 | 32 | USE_TZ = True 33 | 34 | MEDIA_ROOT = '' 35 | 36 | MEDIA_URL = '' 37 | 38 | STATIC_ROOT = '' 39 | 40 | STATIC_URL = '/static/' 41 | 42 | STATICFILES_DIRS = ( 43 | ) 44 | 45 | STATICFILES_FINDERS = ( 46 | 'django.contrib.staticfiles.finders.FileSystemFinder', 47 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 48 | ) 49 | 50 | SECRET_KEY = 'd$vcdxp(who0bvg5)8-mkaejq@f58z!h4*l)98y^i3z!3)*0zh' 51 | 52 | TEMPLATE_LOADERS = ( 53 | 'django.template.loaders.filesystem.Loader', 54 | 'django.template.loaders.app_directories.Loader', 55 | ) 56 | 57 | MIDDLEWARE_CLASSES = ( 58 | 'django.middleware.common.CommonMiddleware', 59 | 'django.contrib.sessions.middleware.SessionMiddleware', 60 | 'django.middleware.csrf.CsrfViewMiddleware', 61 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 62 | 'django.contrib.messages.middleware.MessageMiddleware', 63 | ) 64 | 65 | ROOT_URLCONF = 'example.urls' 66 | 67 | WSGI_APPLICATION = 'example.wsgi.application' 68 | 69 | TEMPLATE_DIRS = ( 70 | ) 71 | 72 | # add admin_sso to INSTALLED_APPS 73 | INSTALLED_APPS = ( 74 | 'django.contrib.auth', 75 | 'django.contrib.contenttypes', 76 | 'django.contrib.sessions', 77 | 'django.contrib.sites', 78 | 'django.contrib.messages', 79 | 'django.contrib.staticfiles', 80 | 'django.contrib.admin', 81 | 'admin_sso', 82 | ) 83 | 84 | # add admin_sso.auth.DjangoSSOAuthBackend to AUTHENTICATION_BACKENDS 85 | AUTHENTICATION_BACKENDS = ( 86 | 'admin_sso.auth.DjangoSSOAuthBackend', 87 | 'django.contrib.auth.backends.ModelBackend', 88 | ) 89 | 90 | DJANGO_ADMIN_SSO_OAUTH_CLIENT_ID = 'your client id here' 91 | DJANGO_ADMIN_SSO_OAUTH_CLIENT_SECRET = 'your client secret here' 92 | 93 | # these are the default values 94 | # they are only set here because unit tests rely on them 95 | DJANGO_ADMIN_SSO_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth' 96 | DJANGO_ADMIN_SSO_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke' 97 | DJANGO_ADMIN_SSO_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token' 98 | 99 | LOGGING = { 100 | 'version': 1, 101 | 'disable_existing_loggers': False, 102 | 'filters': { 103 | 'require_debug_false': { 104 | '()': 'django.utils.log.RequireDebugFalse' 105 | } 106 | }, 107 | 'handlers': { 108 | 'mail_admins': { 109 | 'level': 'ERROR', 110 | 'filters': ['require_debug_false'], 111 | 'class': 'django.utils.log.AdminEmailHandler' 112 | } 113 | }, 114 | 'loggers': { 115 | 'django.request': { 116 | 'handlers': ['mail_admins'], 117 | 'level': 'ERROR', 118 | 'propagate': True, 119 | }, 120 | } 121 | } 122 | 123 | TEST_RUNNER = 'admin_sso.tests.runner.TestRunner' 124 | -------------------------------------------------------------------------------- /example/settings_openid.py: -------------------------------------------------------------------------------- 1 | # Django settings for example project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | ) 8 | 9 | MANAGERS = ADMINS 10 | 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'django.db.backends.sqlite3', 14 | 'NAME': 'db.sqlite', 15 | 'USER': '', 16 | 'PASSWORD': '', 17 | 'HOST': '', 18 | 'PORT': '', 19 | } 20 | } 21 | 22 | TIME_ZONE = 'America/Chicago' 23 | 24 | LANGUAGE_CODE = 'en-us' 25 | 26 | SITE_ID = 1 27 | 28 | USE_I18N = True 29 | 30 | USE_L10N = True 31 | 32 | USE_TZ = True 33 | 34 | MEDIA_ROOT = '' 35 | 36 | MEDIA_URL = '' 37 | 38 | STATIC_ROOT = '' 39 | 40 | STATIC_URL = '/static/' 41 | 42 | STATICFILES_DIRS = ( 43 | ) 44 | 45 | STATICFILES_FINDERS = ( 46 | 'django.contrib.staticfiles.finders.FileSystemFinder', 47 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 48 | ) 49 | 50 | SECRET_KEY = 'd$vcdxp(who0bvg5)8-mkaejq@f58z!h4*l)98y^i3z!3)*0zh' 51 | 52 | TEMPLATE_LOADERS = ( 53 | 'django.template.loaders.filesystem.Loader', 54 | 'django.template.loaders.app_directories.Loader', 55 | ) 56 | 57 | MIDDLEWARE_CLASSES = ( 58 | 'django.middleware.common.CommonMiddleware', 59 | 'django.contrib.sessions.middleware.SessionMiddleware', 60 | 'django.middleware.csrf.CsrfViewMiddleware', 61 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 62 | 'django.contrib.messages.middleware.MessageMiddleware', 63 | ) 64 | 65 | ROOT_URLCONF = 'example.urls' 66 | 67 | WSGI_APPLICATION = 'example.wsgi.application' 68 | 69 | TEMPLATE_DIRS = ( 70 | ) 71 | 72 | # add admin_sso to INSTALLED_APPS 73 | INSTALLED_APPS = ( 74 | 'django.contrib.auth', 75 | 'django.contrib.contenttypes', 76 | 'django.contrib.sessions', 77 | 'django.contrib.sites', 78 | 'django.contrib.messages', 79 | 'django.contrib.staticfiles', 80 | 'django.contrib.admin', 81 | 'admin_sso', 82 | ) 83 | 84 | # add admin_sso.auth.DjangoSSOAuthBackend to AUTHENTICATION_BACKENDS 85 | AUTHENTICATION_BACKENDS = ( 86 | 'admin_sso.auth.DjangoSSOAuthBackend', 87 | 'django.contrib.auth.backends.ModelBackend', 88 | ) 89 | 90 | # openid does rely on serializing complex objects 91 | SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' 92 | 93 | LOGGING = { 94 | 'version': 1, 95 | 'disable_existing_loggers': False, 96 | 'filters': { 97 | 'require_debug_false': { 98 | '()': 'django.utils.log.RequireDebugFalse' 99 | } 100 | }, 101 | 'handlers': { 102 | 'mail_admins': { 103 | 'level': 'ERROR', 104 | 'filters': ['require_debug_false'], 105 | 'class': 'django.utils.log.AdminEmailHandler' 106 | } 107 | }, 108 | 'loggers': { 109 | 'django.request': { 110 | 'handlers': ['mail_admins'], 111 | 'level': 'ERROR', 112 | 'propagate': True, 113 | }, 114 | } 115 | } 116 | 117 | TEST_RUNNER = 'admin_sso.tests.runner.TestRunner' 118 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | 8 | # Uncomment the admin/doc line below to enable admin documentation: 9 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 10 | 11 | url(r'^admin/', include(admin.site.urls)), 12 | ) 13 | -------------------------------------------------------------------------------- /example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test2 project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test2.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,E128 3 | 4 | [upload_sphinx] 5 | upload-dir = build/sphinx/html 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | import os 3 | from setuptools import setup 4 | 5 | import admin_sso 6 | setup( 7 | name='django-admin-sso', 8 | version=admin_sso.__version__, 9 | description='django sso solution', 10 | long_description=open(os.path.join(os.path.dirname(__file__), 'README.rst')).read(), 11 | author='Marc Egli', 12 | author_email='egli@allink.ch', 13 | url='http://github.com/frog32/django-admin-sso/', 14 | license='BSD License', 15 | platforms=['OS Independent'], 16 | packages=[ 17 | 'admin_sso', 18 | 'admin_sso.openid', 19 | ], 20 | # package_data={'admin_sso':'templates/*.html'}, 21 | classifiers=[ 22 | 'Development Status :: 5 - Production/Stable', 23 | 'Environment :: Web Environment', 24 | 'Framework :: Django', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: BSD License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 30 | ], 31 | install_requires=( 32 | 'Django>=1.4', 33 | 'oauth2client>=1.2', 34 | ), 35 | include_package_data=True, 36 | ) 37 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | django-discover-runner>=1.0 2 | coverage==3.7.1 3 | python-openid>=2.2.5 4 | --------------------------------------------------------------------------------