Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,73 @@ default ``users`` and ``admins,managers``
``[',', ';']`` ``users``, ``admins``, and ``managers``
=========================== =======================================


Single Logout
~~~~~~~~~~~~~

`Single Logout`_ (SLO), if initiated by the Django webapp (called front-channel logout), is supported by pointing ``LOGOUT_REDIRECT_URL`` to the Shibboleth SP SLO endpoint (``/Shibboleth.sso/Logout``), if you are using the provided ShibbolethLogoutView for logout.

If you want to support SLO initiated by another app or the IdP (back-channel logout), you need to enable it using the ``SINGLE_LOGOUT_BACKCHANNEL`` setting, but this feature requires additional dependencies. For more details, see the following sections.

SLO is supported by Shibboleth IdP since 3.2.0 (with fixes in 3.2.1) and Shibboleth SP (version >=2.4 recommended).

Additional Requirements
+++++++++++++++++++++++

* lxml (tested with 4.1.0)
* spyne (tested with 2.12.14)


Configuration
+++++++++++++

* Add shibboleth to installed apps.

.. code-block:: python

INSTALLED_APPS += (
'shibboleth',
)

* Run migrations.

.. code-block:: bash

django-admin migrate


* Add back-channel SLO endpoint to ``urlpatterns``, if you don't already include ``shibboleth.urls``.

.. code-block:: python

if SINGLE_LOGOUT_BACKCHANNEL:
from spyne.protocol.soap import Soap11
from spyne.server.django import DjangoView
from .slo_view import LogoutNotificationService

urlpatterns += [
url(r'^logoutNotification/', DjangoView.as_view(
services=[LogoutNotificationService],
tns='urn:mace:shibboleth:2.0:sp:notify',
in_protocol=Soap11(validator='lxml'), out_protocol=Soap11())),
]

* Enable SLO in ``shibboleth2.xml`` of Shibboleth SP.

.. code-block:: xml

<Logout>SAML2 Local</Logout>

* Configure SLO notification in ``shibboleth2.xml`` of Shibboleth SP.

.. code-block:: xml

<Notify
Channel="back"
Location="https://<yourserver>/shib/logoutNotification/" />


.. |build-status| image:: https://travis-ci.org/Brown-University-Library/django-shibboleth-remoteuser.svg?branch=master&style=flat
:target: https://travis-ci.org/Brown-University-Library/django-shibboleth-remoteuser
:alt: Build status
.. _`Single Logout`: https://wiki.shibboleth.net/confluence/display/SHIB2/SLOWebappAdaptation
1 change: 1 addition & 0 deletions quicktest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class QuickDjangoTest(object):
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'shibboleth',
)

def __init__(self, *args, **kwargs):
Expand Down
3 changes: 3 additions & 0 deletions shibboleth/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@
#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)

# back-channel SLO
SINGLE_LOGOUT_BACKCHANNEL = getattr(settings, 'SHIBBOLETH_SINGLE_LOGOUT_BACKCHANNEL', False)
21 changes: 19 additions & 2 deletions shibboleth/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
from django.contrib.auth.models import Group
from django.contrib import auth
from django.core.exceptions import ImproperlyConfigured
import django.utils.version
import re

from shibboleth.app_settings import SHIB_ATTRIBUTE_MAP, GROUP_ATTRIBUTES, GROUP_DELIMITERS
from shibboleth.app_settings import SHIB_ATTRIBUTE_MAP, GROUP_ATTRIBUTES, GROUP_DELIMITERS, SINGLE_LOGOUT_BACKCHANNEL
from shibboleth.models import ShibSession


class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware):
Expand Down Expand Up @@ -56,8 +58,23 @@ def process_request(self, request):
# User is valid. Set request.user and persist user in the session
# by logging the user in.
request.user = user

if SINGLE_LOGOUT_BACKCHANNEL:
# workaround for bug in Django < 1.10
# fixed in Django commit 3389c5ea2
if request.session.session_key is None:
django_version = django.utils.version.get_complete_version()
if django_version[0] == 1 and django_version[1] < 10:
request.session.create()

auth.login(request, user)


if SINGLE_LOGOUT_BACKCHANNEL:
# store session mapping
ShibSession.objects.get_or_create(
shib=request.META['Shib_Session_ID'],
session_id=request.session.session_key)

# Upgrade user groups if configured in the settings.py
# If activated, the user will be associated with those groups.
if GROUP_ATTRIBUTES:
Expand Down
8 changes: 7 additions & 1 deletion shibboleth/models.py
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#intentionally left blank
from django.db import models
from django.contrib.sessions.models import Session


class ShibSession(models.Model):
shib = models.CharField(max_length=100, primary_key=True)
session = models.ForeignKey(Session, on_delete=models.CASCADE)
52 changes: 52 additions & 0 deletions shibboleth/slo_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from django.contrib.sessions.models import Session
from shibboleth.models import ShibSession
#SLO (back-channel) / spyne stuff
from spyne.model.primitive import Unicode
from spyne.model import XmlAttribute
from spyne.model.enum import Enum
try:
from spyne.service import Service
except ImportError:
from spyne.service import ServiceBase as Service

from spyne.decorator import rpc
from spyne import ComplexModel
from spyne.model.fault import Fault


class OKType(ComplexModel):
pass


class MandatoryUnicode(Unicode):
class Attributes(Unicode.Attributes):
nullable = False
min_occurs = 1


class LogoutRequest(ComplexModel):
__namespace__ = 'urn:mace:shibboleth:2.0:sp:notify'
SessionID = MandatoryUnicode
type = XmlAttribute(Enum("global", "local",
type_name="LogoutNotificationType"))


class LogoutResponse(ComplexModel):
__namespace__ = 'urn:mace:shibboleth:2.0:sp:notify'
OK = OKType


class LogoutNotificationService(Service):
@rpc(LogoutRequest, _returns=LogoutResponse, _body_style='bare')
def LogoutNotification(ctx, req):
# delete user session based on shib session
try:
session_mapping = ShibSession.objects.get(shib=req.SessionID)
except:
# Can't delete session
raise Fault(faultcode='Client', faultstring='Invalid session id')
else:
# Deleting session
Session.objects.filter(
session_key=session_mapping.session_id).delete()
return LogoutResponse
43 changes: 42 additions & 1 deletion shibboleth/tests/test_shib.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.contrib.auth.middleware import RemoteUserMiddleware
from django.contrib.auth.models import User, Group
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from django.test import TestCase, RequestFactory, Client


SAMPLE_HEADERS = {
Expand All @@ -17,6 +17,7 @@
"Shib-AuthnContext-Class": "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified",
"Shib-Identity-Provider": "https://sso.college.edu/idp/shibboleth",
"Shib-Session-ID": "1",
"Shib_Session_ID": "1",
"Shib-Session-Index": "12",
"Shibboleth-affiliation": "[email protected];[email protected]",
"Shibboleth-schoolBarCode": "12345678",
Expand Down Expand Up @@ -60,6 +61,7 @@

settings.SHIBBOLETH_LOGOUT_URL = 'https://sso.school.edu/logout?next=%s'
settings.SHIBBOLETH_LOGOUT_REDIRECT_URL = 'http://school.edu/'
settings.SHIBBOLETH_SINGLE_LOGOUT_BACKCHANNEL = True

# MUST be imported after the settings above
from shibboleth import app_settings
Expand Down Expand Up @@ -277,3 +279,42 @@ def test_logout(self):
self.assertEqual(resp.status_code, 302)
# Make sure the context is empty.
self.assertEqual(resp.context, None)


class SingleLogoutBackchannelTest(TestCase):
# separate client for post request; check logout
logout_req = """
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<LogoutNotification xmlns="urn:mace:shibboleth:2.0:sp:notify" type="global">
<SessionID>%s</SessionID>
</LogoutNotification>
</s:Body>
</s:Envelope>
"""

def test_logout(self):
idp = Client()
# Login
login = self.client.get('/', **SAMPLE_HEADERS)
self.assertEqual(login.status_code, 200)
# back-channel logout
soap_resp = idp.post('/logoutNotification/', data=(self.logout_req % 1), content_type='text/xml;charset=UTF-8')
self.assertContains(soap_resp, "<tns:LogoutNotificationResponse><tns:OK/></tns:LogoutNotificationResponse>", status_code=200)
# Load root url to see if user is in fact logged out.
resp = self.client.get('/')
self.assertEqual(resp.status_code, 302)
# Make sure the context is empty.
self.assertEqual(resp.context, None)

def test_invalid_logout(self):
idp = Client()
# Login
login = self.client.get('/', **SAMPLE_HEADERS)
self.assertEqual(login.status_code, 200)
# invalid back-channel logout (raises Exception and returns Internal Server Error!)
soap_resp = idp.post('/logoutNotification/', data=(self.logout_req % 'NonExistentSession'), content_type='text/xml;charset=UTF-8')
self.assertContains(soap_resp, "Invalid session id", status_code=500)
# Load root url to see if user is still logged in
resp = self.client.get('/')
self.assertEqual(resp.status_code, 200)
13 changes: 13 additions & 0 deletions shibboleth/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import django
from django.conf.urls import url
from shibboleth.app_settings import SINGLE_LOGOUT_BACKCHANNEL

from .views import ShibbolethView, ShibbolethLogoutView, ShibbolethLoginView

Expand All @@ -8,3 +9,15 @@
url(r'^logout/$', ShibbolethLogoutView.as_view(), name='logout'),
url(r'^$', ShibbolethView.as_view(), name='info'),
]

if SINGLE_LOGOUT_BACKCHANNEL:
from spyne.protocol.soap import Soap11
from spyne.server.django import DjangoView
from .slo_view import LogoutNotificationService

urlpatterns += [
url(r'^logoutNotification/', DjangoView.as_view(
services=[LogoutNotificationService],
tns='urn:mace:shibboleth:2.0:sp:notify',
in_protocol=Soap11(validator='lxml'), out_protocol=Soap11())),
]