-# Copyright (C) 2014 Simo Sorce <simo@redhat.com>
-#
-# see file 'COPYING' for use and warranty information
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
+# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
from ipsilon.providers.common import ProviderBase, ProviderPageBase, \
ProviderInstaller
from ipsilon.providers.saml2.admin import Saml2AdminPage
from ipsilon.providers.saml2.rest import Saml2RestBase
from ipsilon.providers.saml2.provider import IdentityProvider
+from ipsilon.providers.saml2.sessions import SAMLSessionFactory
+from ipsilon.util.data import SAML2SessionStore
from ipsilon.tools.certs import Certificate
from ipsilon.tools import saml2metadata as metadata
from ipsilon.tools import files
+from ipsilon.util.http import require_content_type
+from ipsilon.util.constants import SOAP_MEDIA_TYPE, XML_MEDIA_TYPE
from ipsilon.util.user import UserSession
from ipsilon.util.plugin import PluginObject
from ipsilon.util import config as pconfig
import time
import uuid
+cherrypy.tools.require_content_type = cherrypy.Tool('before_request_body',
+ require_content_type)
+
+
+def is_lasso_ecp_enabled():
+ # Full ECP support appeared in lasso version 2.4.2
+ return lasso.checkVersion(2, 4, 2, lasso.CHECK_VERSION_NUMERIC)
+
+
+class SSO_SOAP(AuthenticateRequest):
+
+ def __init__(self, *args, **kwargs):
+ super(SSO_SOAP, self).__init__(*args, **kwargs)
+ self.binding = metadata.SAML2_SERVICE_MAP['sso-soap'][1]
+
+ @cherrypy.tools.require_content_type(
+ required=[SOAP_MEDIA_TYPE, XML_MEDIA_TYPE])
+ @cherrypy.tools.accept(media=[SOAP_MEDIA_TYPE, XML_MEDIA_TYPE])
+ @cherrypy.tools.response_headers(
+ headers=[('Content-Type', 'SOAP_MEDIA_TYPE')])
+ def POST(self, *args, **kwargs):
+ self.debug("SSO_SOAP.POST() begin")
+
+ self.debug("SSO_SOAP transaction provider=%s id=%s" %
+ (self.trans.provider, self.trans.transaction_id))
+
+ us = UserSession()
+ us.remote_login()
+ user = us.get_user()
+ self.debug("SSO_SOAP user=%s" % (user.name))
+
+ if not user:
+ raise cherrypy.HTTPError(403, 'No user specified for SSO_SOAP')
+
+ soap_xml_doc = cherrypy.request.rfile.read()
+ soap_xml_doc = soap_xml_doc.strip()
+ self.debug("SSO_SOAP soap_xml_doc=%s" % soap_xml_doc)
+ login = self.saml2login(soap_xml_doc)
+
+ return self.auth(login)
+
class Redirect(AuthenticateRequest):
+ def __init__(self, *args, **kwargs):
+ super(Redirect, self).__init__(*args, **kwargs)
+ self.binding = metadata.SAML2_SERVICE_MAP['sso-redirect'][1]
+
def GET(self, *args, **kwargs):
query = cherrypy.request.query_string
class POSTAuth(AuthenticateRequest):
+ def __init__(self, *args, **kwargs):
+ super(POSTAuth, self).__init__(*args, **kwargs)
+ self.binding = metadata.SAML2_SERVICE_MAP['sso-post'][1]
+
def POST(self, *args, **kwargs):
request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
self.stage = transdata['saml2_stage']
if user.is_anonymous:
- self._debug("User is marked anonymous?!")
+ self.debug("User is marked anonymous?!")
# TODO: Return to SP with auth failed error
raise cherrypy.HTTPError(401)
- self._debug('Continue auth for %s' % user.name)
+ self.debug('Continue auth for %s' % user.name)
if 'saml2_request' not in transdata:
- self._debug("Couldn't find Request dump?!")
+ self.debug("Couldn't find Request dump?!")
# TODO: Return to SP with auth failed error
raise cherrypy.HTTPError(400)
dump = transdata['saml2_request']
try:
login = self.cfg.idp.get_login_handler(dump)
except Exception, e: # pylint: disable=broad-except
- self._debug('Failed to load status from dump: %r' % e)
+ self.debug('Failed to load status from dump: %r' % e)
if not login:
- self._debug("Empty Request dump?!")
+ self.debug("Empty Request dump?!")
# TODO: Return to SP with auth failed error
raise cherrypy.HTTPError(400)
return self.auth(login)
-class RedirectLogout(LogoutRequest):
+class Logout(LogoutRequest):
def GET(self, *args, **kwargs):
query = cherrypy.request.query_string
self.Redirect = Redirect(*args, **kwargs)
self.POST = POSTAuth(*args, **kwargs)
self.Continue = Continue(*args, **kwargs)
+ self.SOAP = SSO_SOAP(*args, **kwargs)
class SLO(ProviderPageBase):
def __init__(self, *args, **kwargs):
super(SLO, self).__init__(*args, **kwargs)
- self._debug('SLO init')
- self.Redirect = RedirectLogout(*args, **kwargs)
+ self.debug('SLO init')
+ self.Redirect = Logout(*args, **kwargs)
# one week
def GET(self, *args, **kwargs):
body = self._get_metadata()
- cherrypy.response.headers["Content-Type"] = "text/xml"
+ cherrypy.response.headers["Content-Type"] = XML_MEDIA_TYPE
cherrypy.response.headers["Content-Disposition"] = \
'attachment; filename="metadata.xml"'
return body
self.rest = None
self.page = None
self.idp = None
+ self.sessionfactory = None
self.description = """
Provides SAML 2.0 authentication infrastructure. """
'default allowed attributes',
'Defines a list of allowed attributes, applied after mapping',
['*']),
+ pconfig.String(
+ 'session database url',
+ 'Database URL for SAML2 sessions',
+ 'saml2.sessions.db.sqlite'),
)
if cherrypy.config.get('debug', False):
import logging
logger.addHandler(lh)
logger.setLevel(logging.DEBUG)
+ store = SAML2SessionStore(
+ database_url=self.get_config_value('session database url')
+ )
+ bt = cherrypy.process.plugins.BackgroundTask(
+ 60, store.remove_expired_sessions
+ )
+ bt.start()
+
@property
def allow_self_registration(self):
return self.get_config_value('allow self registration')
def init_idp(self):
idp = None
+ self.sessionfactory = SAMLSessionFactory(
+ database_url=self.get_config_value('session database url')
+ )
# Init IDP data
try:
- idp = IdentityProvider(self)
+ idp = IdentityProvider(self,
+ sessionfactory=self.sessionfactory)
except Exception, e: # pylint: disable=broad-except
- self._debug('Failed to init SAML2 provider: %r' % e)
+ self.debug('Failed to init SAML2 provider: %r' % e)
return None
self._root.logout.add_handler(self.name, self.idp_initiated_logout)
try:
idp.add_provider(sp)
except Exception, e: # pylint: disable=broad-except
- self._debug('Failed to add SP %s: %r' % (sp['name'], e))
+ self.debug('Failed to add SP %s: %r' % (sp['name'], e))
return idp
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")
+ self.debug("IdP-initiated SAML2 logout")
us = UserSession()
+ user = us.get_user()
- saml_sessions = us.get_provider_data('saml2')
- if saml_sessions is None:
- self._debug("No SAML2 sessions to logout")
- return
- session = saml_sessions.get_next_logout(remove=False)
+ saml_sessions = self.sessionfactory
+ # pylint: disable=unused-variable
+ (mech, session) = saml_sessions.get_next_logout(
+ logout_mechs=[lasso.SAML2_METADATA_BINDING_REDIRECT])
if session is None:
return
- # Add a fake session to indicate where the user should
- # be redirected to when all SP's are logged out.
- idpurl = self._root.instance_base_url()
- saml_sessions.add_session("_idp_initiated_logout",
- idpurl,
- "")
- init_session = saml_sessions.find_session_by_provider(idpurl)
- init_session.set_logoutstate(idpurl, "idp_initiated_logout", None)
- saml_sessions.start_logout(init_session)
-
logout = self.idp.get_logout_handler()
- logout.setSessionFromDump(session.session.dump())
+ logout.setSessionFromDump(session.login_session)
logout.initRequest(session.provider_id)
try:
logout.buildRequestMsg()
raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
% e)
+ # Add a fake session to indicate where the user should
+ # 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, "", "",
+ [lasso.SAML2_METADATA_BINDING_REDIRECT])
+ init_session = saml_sessions.get_session_by_id(session_id)
+ saml_sessions.start_logout(init_session, relaystate=idpurl)
+
+ # Add the logout request id we just created to the session to be
+ # logged out so that when it responds we can find the right
+ # session.
+ session.set_logoutstate(request_id=logout.request.id)
+ saml_sessions.start_logout(session, initial=False)
+
+ self.debug('Sending initial logout request to %s' % logout.msgUrl)
raise cherrypy.HTTPRedirect(logout.msgUrl)
'%s/saml2/SSO/POST' % url)
self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
'%s/saml2/SSO/Redirect' % url)
+ if is_lasso_ecp_enabled():
+ self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-soap'],
+ '%s/saml2/SSO/SOAP' % url)
self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
'%s/saml2/SLO/Redirect' % url)
self.meta.add_allowed_name_format(
help=('Metadata validity period in days '
'(default - %d)' %
METADATA_DEFAULT_VALIDITY_PERIOD))
+ group.add_argument('--saml2-session-dburl',
+ help='session database URL')
- def configure(self, opts):
+ def configure(self, opts, changes):
if opts['saml2'] != 'yes':
return
'idp certificate file': cert.cert,
'idp key file': cert.key,
'idp nameid salt': uuid.uuid4().hex,
- 'idp metadata validity': opts['saml2_metadata_validity']}
+ 'idp metadata validity': opts['saml2_metadata_validity'],
+ 'session database url': opts['saml2_session_dburl'] or
+ opts['database_url'] % {
+ 'datadir': opts['data_dir'],
+ 'dbname': 'saml2.sessions.db'}}
po.save_plugin_config(config)
# Update global config to add login plugin