diff --git a/thirdpart/shibboleth/__init__.py b/thirdpart/shibboleth/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/thirdpart/shibboleth/app_settings.py b/thirdpart/shibboleth/app_settings.py new file mode 100755 index 0000000000..e64b19fc1a --- /dev/null +++ b/thirdpart/shibboleth/app_settings.py @@ -0,0 +1,29 @@ + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +#At a minimum you will need username, +default_shib_attributes = { + "Shibboleth-eppn": (True, "username"), +} + +SHIB_ATTRIBUTE_MAP = getattr(settings, 'SHIBBOLETH_ATTRIBUTE_MAP', default_shib_attributes) +#Set to true if you are testing and want to insert sample headers. +SHIB_MOCK_HEADERS = getattr(settings, 'SHIBBOLETH_MOCK_HEADERS', False) + +LOGIN_URL = getattr(settings, 'LOGIN_URL', None) + +if not LOGIN_URL: + raise ImproperlyConfigured("A LOGIN_URL is required. Specify in settings.py") + +#Optional logout parameters +#This should look like: https://sso.school.edu/idp/logout.jsp?return=%s +#The return url variable will be replaced in the LogoutView. +LOGOUT_URL = getattr(settings, 'SHIBBOLETH_LOGOUT_URL', None) +#LOGOUT_REDIRECT_URL specifies a default logout page that will always be used when +#users logout from Shibboleth. +LOGOUT_REDIRECT_URL = getattr(settings, 'SHIBBOLETH_LOGOUT_REDIRECT_URL', None) +#Name of key. Probably no need to change this. +LOGOUT_SESSION_KEY = getattr(settings, 'SHIBBOLETH_FORCE_REAUTH_SESSION_KEY', 'shib_force_reauth') + + diff --git a/thirdpart/shibboleth/backends.py b/thirdpart/shibboleth/backends.py new file mode 100644 index 0000000000..68d0c7c806 --- /dev/null +++ b/thirdpart/shibboleth/backends.py @@ -0,0 +1,46 @@ +from django.db import connection +from django.contrib.auth.models import User, Permission +from django.contrib.auth.backends import RemoteUserBackend + +class ShibbolethRemoteUserBackend(RemoteUserBackend): + """ + This backend is to be used in conjunction with the ``RemoteUserMiddleware`` + found in the middleware module of this package, and is used when the server + is handling authentication outside of Django. + + By default, the ``authenticate`` method creates ``User`` objects for + usernames that don't already exist in the database. Subclasses can disable + this behavior by setting the ``create_unknown_user`` attribute to + ``False``. + """ + + # Create a User object if not already in the database? + create_unknown_user = True + + def authenticate(self, remote_user, shib_meta): + """ + The username passed as ``remote_user`` is considered trusted. This + method simply returns the ``User`` object with the given username, + creating a new ``User`` object if ``create_unknown_user`` is ``True``. + + Returns None if ``create_unknown_user`` is ``False`` and a ``User`` + object with the given username is not found in the database. + """ + if not remote_user: + return + user = None + username = self.clean_username(remote_user) + shib_user_params = dict([(k, shib_meta[k]) for k in User._meta.get_all_field_names() if k in shib_meta]) + # Note that this could be accomplished in one try-except clause, but + # instead we use get_or_create when creating unknown users since it has + # built-in safeguards for multiple threads. + if self.create_unknown_user: + user, created = User.objects.get_or_create(username=shib_user_params.get('username'), defaults=shib_user_params) + if created: + user = self.configure_user(user) + else: + try: + user = User.objects.get(**shib_user_params) + except User.DoesNotExist: + pass + return user diff --git a/thirdpart/shibboleth/context_processors.py b/thirdpart/shibboleth/context_processors.py new file mode 100755 index 0000000000..820369563d --- /dev/null +++ b/thirdpart/shibboleth/context_processors.py @@ -0,0 +1,26 @@ +from django.core.urlresolvers import reverse +from urllib import quote + +def login_link(request): + """ + This assumes your login link is the Shibboleth login page for your server + and uses the 'target' url parameter. + """ + full_path = quote(request.get_full_path()) + login = reverse('shibboleth:login') + ll = "%s?target=%s" % (login, full_path) + return { 'login_link': ll } + +def logout_link(request, *args): + """ + This assumes your login link is the Shibboleth login page for your server + and uses the 'target' url parameter. + e.g: https://school.edu/Shibboleth.sso/Login + """ + from app_settings import LOGOUT_URL, LOGOUT_REDIRECT_URL + #LOGOUT_REDIRECT_URL specifies a default logout page that will always be used when + #users logout from Shibboleth. + target = LOGOUT_REDIRECT_URL or quote(request.build_absolute_uri()) + logout = reverse('shibboleth:logout') + ll = "%s?target=%s" % (logout, target) + return { 'logout_link': ll } \ No newline at end of file diff --git a/thirdpart/shibboleth/decorators.py b/thirdpart/shibboleth/decorators.py new file mode 100755 index 0000000000..1e96b0a7f6 --- /dev/null +++ b/thirdpart/shibboleth/decorators.py @@ -0,0 +1,23 @@ +""" +Decorators to use with Shibboleth. +""" +from django.conf import settings +from django.contrib import auth +from middleware import ShibbolethRemoteUserMiddleware + +def login_optional(func): + """ + Decorator to pull Shib attributes and log user in, if possible. Does not + enforce login. + """ + def decorator(request,*args, **kwargs): + #Do nothing if the remoteuser backend isn't activated + if 'shibboleth.backends.ShibbolethRemoteUserBackend' not in settings.AUTHENTICATION_BACKENDS: + pass + else: + shib = ShibbolethRemoteUserMiddleware() + #Proccess the request with the Shib middlemare, which will log the + #user in if we can. + proc = shib.process_request(request) + return func(request, *args, **kwargs) + return decorator diff --git a/thirdpart/shibboleth/middleware.py b/thirdpart/shibboleth/middleware.py new file mode 100755 index 0000000000..98945a04e5 --- /dev/null +++ b/thirdpart/shibboleth/middleware.py @@ -0,0 +1,102 @@ +from django.contrib.auth.middleware import RemoteUserMiddleware +from django.contrib import auth +from django.core.exceptions import ImproperlyConfigured + +from shibboleth.app_settings import SHIB_ATTRIBUTE_MAP, LOGOUT_SESSION_KEY + +class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware): + """ + Authentication Middleware for use with Shibboleth. Uses the recommended pattern + for remote authentication from: http://code.djangoproject.com/svn/django/tags/releases/1.3/django/contrib/auth/middleware.py + """ + def process_request(self, request): + # AuthenticationMiddleware is required so that request.user exists. + if not hasattr(request, 'user'): + raise ImproperlyConfigured( + "The Django remote user auth middleware requires the" + " authentication middleware to be installed. Edit your" + " MIDDLEWARE_CLASSES setting to insert" + " 'django.contrib.auth.middleware.AuthenticationMiddleware'" + " before the RemoteUserMiddleware class.") + + #To support logout. If this variable is True, do not + #authenticate user and return now. + if request.session.get(LOGOUT_SESSION_KEY) == True: + return + else: + #Delete the shib reauth session key if present. + request.session.pop(LOGOUT_SESSION_KEY, None) + + #Locate the remote user header. + try: + username = request.META[self.header] + except KeyError: + # If specified header doesn't exist then return (leaving + # request.user set to AnonymousUser by the + # AuthenticationMiddleware). + return + # If the user is already authenticated and that user is the user we are + # getting passed in the headers, then the correct user is already + # persisted in the session and we don't need to continue. + if request.user.is_authenticated(): + if request.user.username == self.clean_username(username, request): + return + + # Make sure we have all required Shiboleth elements before proceeding. + shib_meta, error = self.parse_attributes(request) + # Add parsed attributes to the session. + request.session['shib'] = shib_meta + if error: + raise ShibbolethValidationError("All required Shibboleth elements" + " not found. %s" % shib_meta) + + # We are seeing this user for the first time in this session, attempt + # to authenticate the user. + user = auth.authenticate(remote_user=username, shib_meta=shib_meta) + if user: + # User is valid. Set request.user and persist user in the session + # by logging the user in. + request.user = user + auth.login(request, user) + user.set_unusable_password() + user.save() + # call make profile. + self.make_profile(user, shib_meta) + #setup session. + self.setup_session(request) + + def make_profile(self, user, shib_meta): + """ + This is here as a stub to allow subclassing of ShibbolethRemoteUserMiddleware + to include a make_profile method that will create a Django user profile + from the Shib provided attributes. By default it does nothing. + """ + return + + def setup_session(self, request): + """ + If you want to add custom code to setup user sessions, you + can extend this. + """ + return + + def parse_attributes(self, request): + """ + Parse the incoming Shibboleth attributes. + From: https://github.com/russell/django-shibboleth/blob/master/django_shibboleth/utils.py + Pull the mapped attributes from the apache headers. + """ + shib_attrs = {} + error = False + meta = request.META + for header, attr in SHIB_ATTRIBUTE_MAP.items(): + required, name = attr + value = meta.get(header, None) + shib_attrs[name] = value + if not value or value == '': + if required: + error = True + return shib_attrs, error + +class ShibbolethValidationError(Exception): + pass diff --git a/thirdpart/shibboleth/models.py b/thirdpart/shibboleth/models.py new file mode 100755 index 0000000000..e482fb34e3 --- /dev/null +++ b/thirdpart/shibboleth/models.py @@ -0,0 +1 @@ +#intentionally left blank diff --git a/thirdpart/shibboleth/templates/shibboleth/user_info.html b/thirdpart/shibboleth/templates/shibboleth/user_info.html new file mode 100755 index 0000000000..135acf818a --- /dev/null +++ b/thirdpart/shibboleth/templates/shibboleth/user_info.html @@ -0,0 +1,18 @@ + + +
+ + +The mapped shib attributes will display here.
+