* add saml2/SSO/SOAP endpoint.
* add check for lasso version, ECP endpoint only exposed in metadata
if lasso has full ECP support.
* add SSO_SOAP soap authentication handler (used for ECP).
* add SAML binding to transaction so we can determine if cookies
and other HTTP concepts are expected. Each handler is responsible
for setting the binding.
* add some constants needed for ECP
https://fedorahosted.org/ipsilon/ticket/4
Signed-off-by: John Dennis <jdennis@redhat.com>
Reviewed-by: Rob Crittenden <rcritten@redhat.com>
from ipsilon.providers.saml2.provider import InvalidProviderId
from ipsilon.providers.saml2.provider import NameIdNotAllowed
from ipsilon.providers.saml2.sessions import SAMLSessionsContainer
+from ipsilon.tools import saml2metadata as metadata
from ipsilon.util.policy import Policy
from ipsilon.util.user import UserSession
from ipsilon.util.trans import Transaction
super(AuthenticateRequest, self).__init__(*args, **kwargs)
self.stage = 'init'
self.trans = None
+ self.binding = None
def _preop(self, *args, **kwargs):
try:
# generate a new id or get current one
self.trans = Transaction('saml2', **kwargs)
- if self.trans.cookie.value != self.trans.provider:
- self.debug('Invalid transaction, %s != %s' % (
- self.trans.cookie.value, self.trans.provider))
+
+ self.debug('self.binding=%s, transdata=%s' %
+ (self.binding, self.trans.retrieve()))
+ if self.binding is None:
+ # SAML binding is unknown, try to get it from transaction
+ transdata = self.trans.retrieve()
+ self.binding = transdata.get('saml2_binding')
+ else:
+ # SAML binding known, store in transaction
+ data = {'saml2_binding': self.binding}
+ self.trans.store(data)
+
+ # Only check for cookie for those bindings which use one
+ if self.binding not in (metadata.SAML2_SERVICE_MAP['sso-soap'][1]):
+ if self.trans.cookie.value != self.trans.provider:
+ self.debug('Invalid transaction, %s != %s' % (
+ self.trans.cookie.value, self.trans.provider))
except Exception, e: # pylint: disable=broad-except
self.debug('Transaction initialization failed: %s' % repr(e))
raise cherrypy.HTTPError(400, 'Invalid transaction id')
}
return self._template('saml2/post_response.html', **context)
+ elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP:
+ login.buildResponseMsg()
+ self.debug("Returning ECP: %s" % login.msgBody)
+ return login.msgBody
+
else:
raise cherrypy.HTTPError(500)
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.Redirect = Redirect(*args, **kwargs)
self.POST = POSTAuth(*args, **kwargs)
self.Continue = Continue(*args, **kwargs)
+ self.SOAP = SSO_SOAP(*args, **kwargs)
class SLO(ProviderPageBase):
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
'%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(
lasso.SAML2_METADATA_BINDING_POST),
'sso-redirect': ('SingleSignOnService',
lasso.SAML2_METADATA_BINDING_REDIRECT),
+ 'sso-soap': ('SingleSignOnService',
+ lasso.SAML2_METADATA_BINDING_SOAP),
'logout-redirect': ('SingleLogoutService',
lasso.SAML2_METADATA_BINDING_REDIRECT),
'response-post': ('AssertionConsumerService',
--- /dev/null
+# Copyright (C) 2015 Ipsilon project Contributors, for license see COPYING
+
+SOAP_MEDIA_TYPE = 'application/soap+xml'
+XML_MEDIA_TYPE = 'text/xml'
--- /dev/null
+# Copyright (C) 2015 Ipsilon project Contributors, for license see COPYING
+
+import cherrypy
+import fnmatch
+
+
+def require_content_type(required=None, absent_ok=True, debug=False):
+ '''CherryPy Tool that validates request Content-Type.
+
+ This is a CherryPy Tool that checks the Content-Type in a request and
+ raises HTTP Error 415 "Unsupported Media Type" if it does not match.
+
+ The tool accepts a glob style pattern or list of patterns (see fnmatch)
+ and verifies the Content-Type in the request matches at least one of
+ the patterns, if not a HTTP Error 415 "Unsupported Media Type" is raised.
+
+ If absent_ok is False and if the request does not contain a
+ Content-Type header a HTTP Error 415 "Unsupported Media Type" is
+ raised.
+
+ The tool may be deployed use any of the standard methods for
+ invoking CherryPy tools, for example as a decorator:
+
+ @cherrypy.tools.require_content_type(required='text/xml')
+ def POST(self, *args, **kwargs):
+ pass
+
+ :param required: May be a single string or a list of strings. Each
+ string is interpreted as a glob style pattern (see fnmatch).
+ The Content-Type must match at least one pattern.
+
+ :param absent_ok: Boolean specifying if the Content-Type header
+ must be present or if it is OK to be absent.
+
+ '''
+ if required is None:
+ return
+
+ if isinstance(required, basestring):
+ required = [required]
+
+ content_type = cherrypy.request.body.content_type.value
+ pattern = None
+ match = False
+ if content_type:
+ for pattern in required:
+ if fnmatch.fnmatch(content_type, pattern):
+ match = True
+ break
+ else:
+ if absent_ok:
+ return
+
+ if debug:
+ cherrypy.log('require_content_type: required=%s, absent_ok=%s '
+ 'content_type=%s match=%s pattern=%s' %
+ required, absent_ok, content_type, match, pattern)
+
+ if not match:
+ acceptable = ', '.join(['"%s"' % x for x in required])
+ if content_type:
+ content_type = '"%s"' % content_type
+ else:
+ content_type = 'not specified'
+ message = ('Content-Type must match one of following patterns [%s], '
+ 'but the Content-Type was %s' %
+ (acceptable, content_type))
+ raise cherrypy.HTTPError(415, message=message)