summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRob Crittenden <rcritten@redhat.com>2015-06-25 11:00:59 -0400
committerPatrick Uiterwijk <puiterwijk@redhat.com>2015-07-16 15:04:36 +0200
commit2751451f4158417e66974d6415d2da84f612ab3c (patch)
treeb2f9237ab99aa563b9922d073dfcff73e188e994
parent426f03dffc8f648a12b5f8a4b2ab30c8b4498be3 (diff)
downloadipsilon-2751451f4158417e66974d6415d2da84f612ab3c.tar.gz
ipsilon-2751451f4158417e66974d6415d2da84f612ab3c.tar.xz
ipsilon-2751451f4158417e66974d6415d2da84f612ab3c.zip
Add support for logout over SOAP
As each login session comes in, store the supported logout mechanisms in the SP metadata. Upon a logout request, loop through all of those SP's that support SOAP and log those out first, then log out any remaining sessions using HTTP Redirect. https://fedorahosted.org/ipsilon/ticket/59 Signed-off-by: Rob Crittenden <rcritten@redhat.com> Reviewed-by: Patrick Uiterwijk <puiterwijk@redhat.com>
-rwxr-xr-xipsilon/install/ipsilon-client-install1
-rw-r--r--ipsilon/providers/saml2/auth.py5
-rw-r--r--ipsilon/providers/saml2/logout.py89
-rw-r--r--ipsilon/providers/saml2/provider.py11
-rw-r--r--ipsilon/providers/saml2/sessions.py82
-rw-r--r--ipsilon/providers/saml2idp.py14
-rwxr-xr-xipsilon/tools/saml2metadata.py2
-rw-r--r--ipsilon/util/data.py8
8 files changed, 167 insertions, 45 deletions
diff --git a/ipsilon/install/ipsilon-client-install b/ipsilon/install/ipsilon-client-install
index 49d9e78..d8a310c 100755
--- a/ipsilon/install/ipsilon-client-install
+++ b/ipsilon/install/ipsilon-client-install
@@ -97,6 +97,7 @@ def saml2():
m.set_entity_id(url_sp)
m.add_certs(c)
m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout)
+ m.add_service(SAML2_SERVICE_MAP['slo-soap'], url_logout)
m.add_service(SAML2_SERVICE_MAP['response-post'], url_post, index="0")
m.add_allowed_name_format(SAML2_NAMEID_MAP[args['saml_nameid']])
sp_metafile = os.path.join(path, 'metadata.xml')
diff --git a/ipsilon/providers/saml2/auth.py b/ipsilon/providers/saml2/auth.py
index c46d604..d856220 100644
--- a/ipsilon/providers/saml2/auth.py
+++ b/ipsilon/providers/saml2/auth.py
@@ -278,10 +278,13 @@ class AuthenticateRequest(ProviderPageBase):
lasso_session = lasso.Session()
lasso_session.addAssertion(login.remoteProviderId, login.assertion)
+ provider = ServiceProvider(self.cfg, login.remoteProviderId)
saml_sessions.add_session(login.assertion.id,
login.remoteProviderId,
user.name,
- lasso_session.dump())
+ lasso_session.dump(),
+ None,
+ provider.logout_mechs)
def saml2error(self, login, code, message):
status = lasso.Samlp2Status()
diff --git a/ipsilon/providers/saml2/logout.py b/ipsilon/providers/saml2/logout.py
index cc9b777..374e885 100644
--- a/ipsilon/providers/saml2/logout.py
+++ b/ipsilon/providers/saml2/logout.py
@@ -4,8 +4,10 @@ from ipsilon.providers.common import ProviderPageBase
from ipsilon.providers.common import InvalidRequest
from ipsilon.providers.saml2.auth import UnknownProvider
from ipsilon.util.user import UserSession
+from ipsilon.util.constants import SOAP_MEDIA_TYPE
import cherrypy
import lasso
+import requests
class LogoutRequest(ProviderPageBase):
@@ -58,7 +60,7 @@ class LogoutRequest(ProviderPageBase):
# all the session indexes and mark them as logging out but only one
# is needed to handle the request.
if len(session_indexes) < 1:
- self.error('SLO empty session Indexes: %s')
+ self.error('SLO empty session Indexes')
raise cherrypy.HTTPError(400, 'Invalid logout request')
session = saml_sessions.get_session_by_id(session_indexes[0])
if session:
@@ -181,11 +183,36 @@ class LogoutRequest(ProviderPageBase):
else:
raise cherrypy.HTTPError(400, 'Not logged in')
+ def _soap_logout(self, logout):
+ """
+ Send a SOAP logout request over HTTP and return the result.
+ """
+ headers = {'Content-Type': SOAP_MEDIA_TYPE}
+ try:
+ response = requests.post(logout.msgUrl, data=logout.msgBody,
+ headers=headers)
+ except Exception as e: # pylint: disable=broad-except
+ self.error('SOAP HTTP request failed: (%s) (on %s)' %
+ (e, logout.msgUrl))
+ raise
+
+ if response.status_code != 200:
+ self.error('SOAP error (%s) (on %s)' %
+ (response.status, logout.msgUrl))
+ raise InvalidRequest('SOAP HTTP error code', response.status_code)
+
+ if not response.text:
+ self.error('Empty SOAP response')
+ raise InvalidRequest('No content in SOAP response')
+
+ return response.text
+
def logout(self, message, relaystate=None, samlresponse=None):
"""
- Handle HTTP Redirect logout. This is an asynchronous logout
- request process that relies on the HTTP agent to forward
- logout requests to any other SP's that are also logged in.
+ Handle HTTP logout. The supported logout methods are stored
+ in each session. First all the SOAP sessions are logged out
+ then the HTTP Redirect method is used for any remaining
+ sessions.
The basic process is this:
1. A logout request is received. It is processed and the response
@@ -198,6 +225,8 @@ class LogoutRequest(ProviderPageBase):
Repeat steps 2-3 until only the initial logout request is
left unhandled, at which time the pre-generated response is sent
back to the SP that originated the logout request.
+
+ The final logout response is always a redirect.
"""
logout = self.cfg.idp.get_logout_handler()
@@ -217,8 +246,13 @@ class LogoutRequest(ProviderPageBase):
# Fall through to handle any remaining sessions.
# Find the next SP to logout and send a LogoutRequest
- session = saml_sessions.get_next_logout()
- if session:
+ logout_order = [
+ lasso.SAML2_METADATA_BINDING_SOAP,
+ lasso.SAML2_METADATA_BINDING_REDIRECT,
+ ]
+ (logout_mech, session) = saml_sessions.get_next_logout(
+ logout_mechs=logout_order)
+ while session:
self.debug('Going to log out %s' % session.provider_id)
try:
@@ -227,8 +261,12 @@ class LogoutRequest(ProviderPageBase):
self.error('Failed to load session: %s' % e)
raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
% e)
-
- logout.initRequest(session.provider_id, lasso.HTTP_METHOD_REDIRECT)
+ if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
+ logout.initRequest(session.provider_id,
+ lasso.HTTP_METHOD_REDIRECT)
+ else:
+ logout.initRequest(session.provider_id,
+ lasso.HTTP_METHOD_SOAP)
try:
logout.buildRequestMsg()
@@ -243,7 +281,7 @@ class LogoutRequest(ProviderPageBase):
indexes = saml_sessions.get_session_id_by_provider_id(
session.provider_id
)
- self.debug('Requesting logout for sessions %s' % indexes)
+ self.debug('Requesting logout for sessions %s' % (indexes,))
req = logout.get_request()
req.setSessionIndexes(indexes)
@@ -253,13 +291,34 @@ class LogoutRequest(ProviderPageBase):
self.debug('Request logout ID %s for session ID %s' %
(logout.request.id, session.session_id))
- self.debug('Redirecting to another SP to logout on %s at %s' %
- (logout.remoteProviderId, logout.msgUrl))
-
- raise cherrypy.HTTPRedirect(logout.msgUrl)
- # Otherwise we're done, respond to the original request using the
- # response we cached earlier.
+ if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
+ self.debug('Redirecting to another SP to logout on %s at %s' %
+ (logout.remoteProviderId, logout.msgUrl))
+ raise cherrypy.HTTPRedirect(logout.msgUrl)
+ else:
+ self.debug('SOAP request to another SP to logout on %s at %s' %
+ (logout.remoteProviderId, logout.msgUrl))
+ if logout.msgBody:
+ message = self._soap_logout(logout)
+ try:
+ self._handle_logout_response(us,
+ logout,
+ saml_sessions,
+ message,
+ samlresponse)
+ except Exception as e: # pylint: disable=broad-except
+ self.error('SOAP SLO failed %s' % e)
+ else:
+ self.error('Provider does not support SOAP')
+
+ (logout_mech, session) = saml_sessions.get_next_logout(
+ logout_mechs=logout_order)
+
+ # done while
+
+ # All sessions should be logged out now. Respond to the
+ # original request using the response we cached earlier.
try:
session = saml_sessions.get_initial_logout()
diff --git a/ipsilon/providers/saml2/provider.py b/ipsilon/providers/saml2/provider.py
index 3dea631..b70582e 100644
--- a/ipsilon/providers/saml2/provider.py
+++ b/ipsilon/providers/saml2/provider.py
@@ -3,8 +3,9 @@
from ipsilon.providers.common import ProviderException
from ipsilon.util import config as pconfig
from ipsilon.util.config import ConfigHelper
-from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP
+from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP, NSMAP
from ipsilon.util.log import Log
+from lxml import etree
import lasso
import re
@@ -49,6 +50,14 @@ class ServiceProvider(ServiceProviderConfig):
self._properties = data[idval]
self._staging = dict()
self.load_config()
+ self.logout_mechs = []
+ xmldoc = etree.XML(str(data[idval]['metadata']))
+ logout = xmldoc.xpath('//md:EntityDescriptor'
+ '/md:SPSSODescriptor'
+ '/md:SingleLogoutService',
+ namespaces=NSMAP)
+ for service in logout:
+ self.logout_mechs.append(service.values()[0])
def load_config(self):
self.new_config(
diff --git a/ipsilon/providers/saml2/sessions.py b/ipsilon/providers/saml2/sessions.py
index 1000a87..d3ed7e2 100644
--- a/ipsilon/providers/saml2/sessions.py
+++ b/ipsilon/providers/saml2/sessions.py
@@ -4,6 +4,10 @@ from cherrypy import config as cherrypy_config
from ipsilon.util.log import Log
from ipsilon.util.data import SAML2SessionStore
import datetime
+from lasso import (
+ SAML2_METADATA_BINDING_SOAP,
+ SAML2_METADATA_BINDING_REDIRECT,
+)
LOGGED_IN = 1
INIT_LOGOUT = 2
@@ -29,11 +33,13 @@ class SAMLSession(Log):
which matches this.
logout_request - the Logout request object
expiration_time - the time the login session expires
+ supported_logout_mechs - logout mechanisms supported by this session
"""
def __init__(self, uuidval, session_id, provider_id, user,
login_session, logoutstate=None, relaystate=None,
logout_request=None, request_id=None,
- expiration_time=None):
+ expiration_time=None,
+ supported_logout_mechs=None):
self.uuidval = uuidval
self.session_id = session_id
@@ -45,6 +51,9 @@ class SAMLSession(Log):
self.request_id = request_id
self.logout_request = logout_request
self.expiration_time = expiration_time
+ if supported_logout_mechs is None:
+ supported_logout_mechs = []
+ self.supported_logout_mechs = supported_logout_mechs
def set_logoutstate(self, relaystate=None, request=None, request_id=None):
"""
@@ -66,6 +75,7 @@ class SAMLSession(Log):
self.debug('provider_id %s' % self.provider_id)
self.debug('login session %s' % self.login_session)
self.debug('logoutstate %s' % self.logoutstate)
+ self.debug('logout mech %s' % self.supported_logout_mechs)
def convert(self):
"""
@@ -118,12 +128,20 @@ class SAMLSessionFactory(Log):
data.get('relaystate'),
data.get('logout_request'),
data.get('request_id'),
- data.get('expiration_time'))
+ data.get('expiration_time'),
+ data.get('supported_logout_mechs'))
def add_session(self, session_id, provider_id, user, login_session,
- request_id=None):
+ request_id, supported_logout_mechs):
"""
Add a new login session to the table.
+
+ :param session_id: The login session ID
+ :param provider_id: The URL of the SP
+ :param user: The NameID username
+ :param login_session: The lasso Login session
+ :param request_id: The request ID of the Logout
+ :param supported_logout_mechs: A list of logout protocols supported
"""
self.user = user
@@ -136,9 +154,9 @@ class SAMLSessionFactory(Log):
'user': user,
'login_session': login_session,
'logoutstate': LOGGED_IN,
- 'expiration_time': expiration_time}
- if request_id:
- data['request_id'] = request_id
+ 'expiration_time': expiration_time,
+ 'request_id': request_id,
+ 'supported_logout_mechs': supported_logout_mechs}
uuidval = self._ss.new_session(data)
@@ -209,7 +227,8 @@ class SAMLSessionFactory(Log):
datum = samlsession.convert()
self._ss.update_session(datum)
- def get_next_logout(self, peek=False):
+ def get_next_logout(self, peek=False,
+ logout_mechs=None):
"""
Get the next session in the logged-in state and move
it to the logging_out state. Return the session that is
@@ -218,24 +237,34 @@ class SAMLSessionFactory(Log):
:param peek: for IdP-initiated logout we can't remove the
session otherwise when the request comes back
in the user won't be seen as being logged-on.
-
- Return None if no more sessions in LOGGED_IN state.
+ :param logout_mechs: An ordered list of logout mechanisms
+ you're looking for. For each mechanism in order
+ loop through all sessions. If If no sessions of
+ this method are available then try the next mechanism
+ until exhausted. In that case None is returned.
+
+ Returns a tuple of (mechanism, session) or
+ (None, None) if no more sessions in LOGGED_IN state.
"""
candidates = self._ss.get_user_sessions(self.user)
-
- for c in candidates:
- key = c.keys()[0]
- if int(c[key].get('logoutstate', 0)) == LOGGED_IN:
- samlsession = self._data_to_samlsession(key, c[key])
- self.start_logout(samlsession, initial=False)
- return samlsession
- return None
+ if logout_mechs is None:
+ logout_mechs = [SAML2_METADATA_BINDING_REDIRECT, ]
+
+ for mech in logout_mechs:
+ for c in candidates:
+ key = c.keys()[0]
+ if ((int(c[key].get('logoutstate', 0)) == LOGGED_IN) and
+ (mech in c[key].get('supported_logout_mechs'))):
+ samlsession = self._data_to_samlsession(key, c[key])
+ self.start_logout(samlsession, initial=False)
+ return (mech, samlsession)
+ return (None, None)
def get_initial_logout(self):
"""
Get the initial logout request.
- Return None if no sessions in INIT_LOGOUT state.
+ Raises ValueError if no sessions in INIT_LOGOUT state.
"""
candidates = self._ss.get_user_sessions(self.user)
@@ -248,7 +277,7 @@ class SAMLSessionFactory(Log):
if int(c[key].get('logoutstate', 0)) == INIT_LOGOUT:
samlsession = self._data_to_samlsession(key, c[key])
return samlsession
- return None
+ raise ValueError()
def wipe_data(self):
self._ss.wipe_data()
@@ -276,14 +305,21 @@ if __name__ == '__main__':
factory = SAMLSessionFactory('/tmp/saml2sessions.sqlite')
factory.wipe_data()
- sess1 = factory.add_session('_123456', provider1, "admin", "<Login/>")
- sess2 = factory.add_session('_789012', provider2, "testuser", "<Login/>")
+ sess1 = factory.add_session('_123456', provider1, "admin",
+ "<Login/>", '_1234',
+ [SAML2_METADATA_BINDING_REDIRECT])
+ sess2 = factory.add_session('_789012', provider2, "testuser",
+ "<Login/>", '_7890',
+ [SAML2_METADATA_BINDING_SOAP,
+ SAML2_METADATA_BINDING_REDIRECT])
# Test finding sessions by provider
ids = factory.get_session_id_by_provider_id(provider2)
assert(len(ids) == 1)
- sess3 = factory.add_session('_345678', provider2, "testuser", "<Login/>")
+ sess3 = factory.add_session('_345678', provider2, "testuser",
+ "<Login/>", '_3456',
+ [SAML2_METADATA_BINDING_REDIRECT])
ids = factory.get_session_id_by_provider_id(provider2)
assert(len(ids) == 2)
@@ -307,7 +343,7 @@ if __name__ == '__main__':
test2 = factory.get_session_by_id('_789012')
factory.start_logout(test2, initial=True)
- test3 = factory.get_next_logout()
+ (lmech, test3) = factory.get_next_logout()
assert(test3.session_id == '_345678')
test4 = factory.get_initial_logout()
diff --git a/ipsilon/providers/saml2idp.py b/ipsilon/providers/saml2idp.py
index f771ef7..5ac83dd 100644
--- a/ipsilon/providers/saml2idp.py
+++ b/ipsilon/providers/saml2idp.py
@@ -131,7 +131,7 @@ class Continue(AuthenticateRequest):
return self.auth(login)
-class RedirectLogout(LogoutRequest):
+class Logout(LogoutRequest):
def GET(self, *args, **kwargs):
query = cherrypy.request.query_string
@@ -159,7 +159,7 @@ class SLO(ProviderPageBase):
def __init__(self, *args, **kwargs):
super(SLO, self).__init__(*args, **kwargs)
self.debug('SLO init')
- self.Redirect = RedirectLogout(*args, **kwargs)
+ self.Redirect = Logout(*args, **kwargs)
# one week
@@ -394,13 +394,18 @@ Provides SAML 2.0 authentication infrastructure. """
Logout all SP sessions when the logout comes from the IdP.
For the current user only.
+
+ Only use HTTP-Redirect to start the logout. This is guaranteed
+ to be supported in SAML 2.
"""
self.debug("IdP-initiated SAML2 logout")
us = UserSession()
user = us.get_user()
saml_sessions = self.sessionfactory
- session = saml_sessions.get_next_logout()
+ # pylint: disable=unused-variable
+ (mech, session) = saml_sessions.get_next_logout(
+ logout_mechs=[lasso.SAML2_METADATA_BINDING_REDIRECT])
if session is None:
return
@@ -418,7 +423,8 @@ Provides SAML 2.0 authentication infrastructure. """
# be redirected to when all SP's are logged out.
idpurl = self._root.instance_base_url()
session_id = "_" + uuid.uuid4().hex.upper()
- saml_sessions.add_session(session_id, idpurl, user.name, "")
+ saml_sessions.add_session(session_id, idpurl, user.name, "", "",
+ [lasso.SAML2_METADATA_BINDING_REDIRECT])
init_session = saml_sessions.get_session_by_id(session_id)
saml_sessions.start_logout(init_session, relaystate=idpurl)
diff --git a/ipsilon/tools/saml2metadata.py b/ipsilon/tools/saml2metadata.py
index 99857bf..98e7c67 100755
--- a/ipsilon/tools/saml2metadata.py
+++ b/ipsilon/tools/saml2metadata.py
@@ -29,6 +29,8 @@ SAML2_SERVICE_MAP = {
lasso.SAML2_METADATA_BINDING_SOAP),
'logout-redirect': ('SingleLogoutService',
lasso.SAML2_METADATA_BINDING_REDIRECT),
+ 'slo-soap': ('SingleLogoutService',
+ lasso.SAML2_METADATA_BINDING_SOAP),
'response-post': ('AssertionConsumerService',
lasso.SAML2_METADATA_BINDING_POST)
}
diff --git a/ipsilon/util/data.py b/ipsilon/util/data.py
index 53a1756..e0cd6e1 100644
--- a/ipsilon/util/data.py
+++ b/ipsilon/util/data.py
@@ -551,6 +551,10 @@ class SAML2SessionStore(Store):
return self.get_unique_data(self.table, idval, name, value)
def new_session(self, datum):
+ if 'supported_logout_mechs' in datum:
+ datum['supported_logout_mechs'] = ','.join(
+ datum['supported_logout_mechs']
+ )
return self.new_unique_data(self.table, datum)
def get_session(self, session_id=None, request_id=None):
@@ -567,7 +571,7 @@ class SAML2SessionStore(Store):
def get_user_sessions(self, user):
"""
- Retrun a list of all sessions for a given user.
+ Return a list of all sessions for a given user.
"""
rows = self.get_unique_data(self.table, name='user', value=user)
@@ -575,6 +579,8 @@ class SAML2SessionStore(Store):
logged_in = []
for r in rows:
data = self.get_unique_data(self.table, uuidval=r)
+ data[r]['supported_logout_mechs'] = data[r].get(
+ 'supported_logout_mechs', '').split(',')
logged_in.append(data)
return logged_in