diff --git a/example/idp2/idp_conf.py.example b/example/idp2/idp_conf.py.example index 87bf659..c7730a6 100644 --- a/example/idp2/idp_conf.py.example +++ b/example/idp2/idp_conf.py.example @@ -97,7 +97,7 @@ CONFIG = { "key_file": full_path("pki/mykey.pem"), "cert_file": full_path("pki/mycert.pem"), "metadata": { - "local": [full_path("../sp/sp.xml")], + "local": [full_path("../sp-wsgi/sp.xml")], }, "organization": { "display_name": "Rolands Identiteter", diff --git a/example/sp/attributemaps/basic.py b/example/sp-repoze/attributemaps/basic.py similarity index 100% rename from example/sp/attributemaps/basic.py rename to example/sp-repoze/attributemaps/basic.py diff --git a/example/sp/attributemaps/saml_uri.py b/example/sp-repoze/attributemaps/saml_uri.py similarity index 100% rename from example/sp/attributemaps/saml_uri.py rename to example/sp-repoze/attributemaps/saml_uri.py diff --git a/example/sp/attributemaps/shibboleth_uri.py b/example/sp-repoze/attributemaps/shibboleth_uri.py similarity index 100% rename from example/sp/attributemaps/shibboleth_uri.py rename to example/sp-repoze/attributemaps/shibboleth_uri.py diff --git a/example/sp/pki/certgeneration.py b/example/sp-repoze/pki/certgeneration.py similarity index 100% rename from example/sp/pki/certgeneration.py rename to example/sp-repoze/pki/certgeneration.py diff --git a/example/sp/pki/mycert.pem b/example/sp-repoze/pki/mycert.pem similarity index 100% rename from example/sp/pki/mycert.pem rename to example/sp-repoze/pki/mycert.pem diff --git a/example/sp/pki/mykey.pem b/example/sp-repoze/pki/mykey.pem similarity index 100% rename from example/sp/pki/mykey.pem rename to example/sp-repoze/pki/mykey.pem diff --git a/example/sp/sp.py b/example/sp-repoze/sp.py similarity index 100% rename from example/sp/sp.py rename to example/sp-repoze/sp.py diff --git a/example/sp-repoze/sp.xml b/example/sp-repoze/sp.xml new file mode 100644 index 0000000..9fbb178 --- /dev/null +++ b/example/sp-repoze/sp.xml @@ -0,0 +1,34 @@ + +MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV +BAYTAlNFMQ0wCwYDVQQHEwRVbWVhMRgwFgYDVQQKEw9VbWVhIFVuaXZlcnNpdHkx +EDAOBgNVBAsTB0lUIFVuaXQxEDAOBgNVBAMTB1Rlc3QgU1AwHhcNMDkxMDI2MTMz +MTE1WhcNMTAxMDI2MTMzMTE1WjBaMQswCQYDVQQGEwJTRTENMAsGA1UEBxMEVW1l +YTEYMBYGA1UEChMPVW1lYSBVbml2ZXJzaXR5MRAwDgYDVQQLEwdJVCBVbml0MRAw +DgYDVQQDEwdUZXN0IFNQMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkJWP7 +bwOxtH+E15VTaulNzVQ/0cSbM5G7abqeqSNSs0l0veHr6/ROgW96ZeQ57fzVy2MC +FiQRw2fzBs0n7leEmDJyVVtBTavYlhAVXDNa3stgvh43qCfLx+clUlOvtnsoMiiR +mo7qf0BoPKTj7c0uLKpDpEbAHQT4OF1HRYVxMwIDAQABo4G/MIG8MB0GA1UdDgQW +BBQ7RgbMJFDGRBu9o3tDQDuSoBy7JjCBjAYDVR0jBIGEMIGBgBQ7RgbMJFDGRBu9 +o3tDQDuSoBy7JqFepFwwWjELMAkGA1UEBhMCU0UxDTALBgNVBAcTBFVtZWExGDAW +BgNVBAoTD1VtZWEgVW5pdmVyc2l0eTEQMA4GA1UECxMHSVQgVW5pdDEQMA4GA1UE +AxMHVGVzdCBTUIIJAJHg2V5J31I8MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF +BQADgYEAMuRwwXRnsiyWzmRikpwinnhTmbooKm5TINPE7A7gSQ710RxioQePPhZO +zkM27NnHTrCe2rBVg0EGz7QTd1JIwLPvgoj4VTi/fSha/tXrYUaqc9AqU1kWI4WN ++vffBGQ09mo+6CffuFTZYeOhzP/2stAPwCTU4kxEoiy0KpZMANI= +MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV +BAYTAlNFMQ0wCwYDVQQHEwRVbWVhMRgwFgYDVQQKEw9VbWVhIFVuaXZlcnNpdHkx +EDAOBgNVBAsTB0lUIFVuaXQxEDAOBgNVBAMTB1Rlc3QgU1AwHhcNMDkxMDI2MTMz +MTE1WhcNMTAxMDI2MTMzMTE1WjBaMQswCQYDVQQGEwJTRTENMAsGA1UEBxMEVW1l +YTEYMBYGA1UEChMPVW1lYSBVbml2ZXJzaXR5MRAwDgYDVQQLEwdJVCBVbml0MRAw +DgYDVQQDEwdUZXN0IFNQMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkJWP7 +bwOxtH+E15VTaulNzVQ/0cSbM5G7abqeqSNSs0l0veHr6/ROgW96ZeQ57fzVy2MC +FiQRw2fzBs0n7leEmDJyVVtBTavYlhAVXDNa3stgvh43qCfLx+clUlOvtnsoMiiR +mo7qf0BoPKTj7c0uLKpDpEbAHQT4OF1HRYVxMwIDAQABo4G/MIG8MB0GA1UdDgQW +BBQ7RgbMJFDGRBu9o3tDQDuSoBy7JjCBjAYDVR0jBIGEMIGBgBQ7RgbMJFDGRBu9 +o3tDQDuSoBy7JqFepFwwWjELMAkGA1UEBhMCU0UxDTALBgNVBAcTBFVtZWExGDAW +BgNVBAoTD1VtZWEgVW5pdmVyc2l0eTEQMA4GA1UECxMHSVQgVW5pdDEQMA4GA1UE +AxMHVGVzdCBTUIIJAJHg2V5J31I8MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF +BQADgYEAMuRwwXRnsiyWzmRikpwinnhTmbooKm5TINPE7A7gSQ710RxioQePPhZO +zkM27NnHTrCe2rBVg0EGz7QTd1JIwLPvgoj4VTi/fSha/tXrYUaqc9AqU1kWI4WN ++vffBGQ09mo+6CffuFTZYeOhzP/2stAPwCTU4kxEoiy0KpZMANI= +Exempel ABExempel ABExample Co.http://www.example.com/rolandJohnSmithjohn.smith@example.com diff --git a/example/sp-repoze/sp_conf.py b/example/sp-repoze/sp_conf.py new file mode 100644 index 0000000..d324427 --- /dev/null +++ b/example/sp-repoze/sp_conf.py @@ -0,0 +1,51 @@ +from saml2 import BINDING_HTTP_REDIRECT +from saml2.saml import NAME_FORMAT_URI + +BASE= "http://localhost:8087" +#BASE= "http://lingon.catalogix.se:8087" + +CONFIG = { + "entityid": "%s/sp.xml" % BASE, + "description": "My SP", + "service": { + "sp": { + "name": "Rolands SP", + "endpoints": { + "assertion_consumer_service": [BASE], + "single_logout_service": [(BASE + "/slo", + BINDING_HTTP_REDIRECT)], + }, + "required_attributes": ["surname", "givenname", + "edupersonaffiliation"], + "optional_attributes": ["title"], + } + }, + "debug": 1, + "key_file": "pki/mykey.pem", + "cert_file": "pki/mycert.pem", + "attribute_map_dir": "./attributemaps", + "metadata": {"local": ["../idp2/idp.xml"]}, + # -- below used by make_metadata -- + "organization": { + "name": "Exempel AB", + "display_name": [("Exempel AB", "se"), ("Example Co.", "en")], + "url": "http://www.example.com/roland", + }, + "contact_person": [{ + "given_name":"John", + "sur_name": "Smith", + "email_address": ["john.smith@example.com"], + "contact_type": "technical", + }, + ], + #"xmlsec_binary":"/opt/local/bin/xmlsec1", + "name_form": NAME_FORMAT_URI, + "logger": { + "rotating": { + "filename": "sp.log", + "maxBytes": 100000, + "backupCount": 5, + }, + "loglevel": "debug", + } +} diff --git a/example/sp/sp_conf.py.example b/example/sp-repoze/sp_conf.py.example similarity index 100% rename from example/sp/sp_conf.py.example rename to example/sp-repoze/sp_conf.py.example diff --git a/example/sp/who.ini b/example/sp-repoze/who.ini similarity index 100% rename from example/sp/who.ini rename to example/sp-repoze/who.ini diff --git a/example/sp-wsgi/conf.py b/example/sp-wsgi/conf.py new file mode 100644 index 0000000..b821e6d --- /dev/null +++ b/example/sp-wsgi/conf.py @@ -0,0 +1,41 @@ +from saml2.entity_category.edugain import COC +from saml2 import BINDING_HTTP_REDIRECT +from saml2 import BINDING_HTTP_POST +from saml2.saml import NAME_FORMAT_URI + +try: + from saml2.sigver import get_xmlsec_binary +except ImportError: + get_xmlsec_binary = None + + +if get_xmlsec_binary: + xmlsec_path = get_xmlsec_binary(["/opt/local/bin","/usr/local/bin"]) +else: + xmlsec_path = '/usr/local/bin/xmlsec1' + +# Make sure the same port number appear in service_conf.py +BASE = "http://localhost:8087" + +CONFIG = { + "entityid": "%s/%ssp.xml" % (BASE, ""), + 'entity_category': [COC], + "description": "Example SP", + "service": { + "sp": { + "endpoints": { + "assertion_consumer_service": [ + ("%s/acs/redirect" % BASE, BINDING_HTTP_REDIRECT), + ("%s/acs/post" % BASE, BINDING_HTTP_POST) + ], + } + }, + }, + "key_file": "pki/mykey.pem", + "cert_file": "pki/mycert.pem", + "xmlsec_binary": xmlsec_path, + "metadata": {"local": ["../idp2/idp.xml"]}, + #"metadata": {"mdfile": ["./swamid2.md"]}, + #"metadata": {"local": ["./swamid-2.0.xml"]}, + "name_form": NAME_FORMAT_URI, +} diff --git a/example/sp-wsgi/pki/mycert.pem b/example/sp-wsgi/pki/mycert.pem new file mode 100644 index 0000000..d4a0873 --- /dev/null +++ b/example/sp-wsgi/pki/mycert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV +BAYTAlNFMQ0wCwYDVQQHEwRVbWVhMRgwFgYDVQQKEw9VbWVhIFVuaXZlcnNpdHkx +EDAOBgNVBAsTB0lUIFVuaXQxEDAOBgNVBAMTB1Rlc3QgU1AwHhcNMDkxMDI2MTMz +MTE1WhcNMTAxMDI2MTMzMTE1WjBaMQswCQYDVQQGEwJTRTENMAsGA1UEBxMEVW1l +YTEYMBYGA1UEChMPVW1lYSBVbml2ZXJzaXR5MRAwDgYDVQQLEwdJVCBVbml0MRAw +DgYDVQQDEwdUZXN0IFNQMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkJWP7 +bwOxtH+E15VTaulNzVQ/0cSbM5G7abqeqSNSs0l0veHr6/ROgW96ZeQ57fzVy2MC +FiQRw2fzBs0n7leEmDJyVVtBTavYlhAVXDNa3stgvh43qCfLx+clUlOvtnsoMiiR +mo7qf0BoPKTj7c0uLKpDpEbAHQT4OF1HRYVxMwIDAQABo4G/MIG8MB0GA1UdDgQW +BBQ7RgbMJFDGRBu9o3tDQDuSoBy7JjCBjAYDVR0jBIGEMIGBgBQ7RgbMJFDGRBu9 +o3tDQDuSoBy7JqFepFwwWjELMAkGA1UEBhMCU0UxDTALBgNVBAcTBFVtZWExGDAW +BgNVBAoTD1VtZWEgVW5pdmVyc2l0eTEQMA4GA1UECxMHSVQgVW5pdDEQMA4GA1UE +AxMHVGVzdCBTUIIJAJHg2V5J31I8MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF +BQADgYEAMuRwwXRnsiyWzmRikpwinnhTmbooKm5TINPE7A7gSQ710RxioQePPhZO +zkM27NnHTrCe2rBVg0EGz7QTd1JIwLPvgoj4VTi/fSha/tXrYUaqc9AqU1kWI4WN ++vffBGQ09mo+6CffuFTZYeOhzP/2stAPwCTU4kxEoiy0KpZMANI= +-----END CERTIFICATE----- diff --git a/example/sp-wsgi/pki/mykey.pem b/example/sp-wsgi/pki/mykey.pem new file mode 100644 index 0000000..d9ec5f8 --- /dev/null +++ b/example/sp-wsgi/pki/mykey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDkJWP7bwOxtH+E15VTaulNzVQ/0cSbM5G7abqeqSNSs0l0veHr +6/ROgW96ZeQ57fzVy2MCFiQRw2fzBs0n7leEmDJyVVtBTavYlhAVXDNa3stgvh43 +qCfLx+clUlOvtnsoMiiRmo7qf0BoPKTj7c0uLKpDpEbAHQT4OF1HRYVxMwIDAQAB +AoGAbx9rKH91DCw/ZEPhHsVXJ6cYHxGcMoAWvnMMC9WUN+bNo4gNL205DLfsxXA1 +jqXFXZj3+38vSFumGPA6IvXrN+Wyp3+Lz3QGc4K5OdHeBtYlxa6EsrxPgvuxYDUB +vx3xdWPMjy06G/ML+pR9XHnRaPNubXQX3UxGBuLjwNXVmyECQQD2/D84tYoCGWoq +5FhUBxFUy2nnOLKYC/GGxBTX62iLfMQ3fbQcdg2pJsB5rrniyZf7UL+9FOsAO9k1 +8DO7G12DAkEA7Hkdg1KEw4ZfjnnjEa+KqpyLTLRQ91uTVW6kzR+4zY719iUJ/PXE +PxJqm1ot7mJd1LW+bWtjLpxs7jYH19V+kQJBAIEpn2JnxdmdMuFlcy/WVmDy09pg +0z0imdexeXkFmjHAONkQOv3bWv+HzYaVMo8AgCOksfEPHGqN4eUMTfFeuUMCQF+5 +E1JSd/2yCkJhYqKJHae8oMLXByNqRXTCyiFioutK4JPYIHfugJdLfC4QziD+Xp85 +RrGCU+7NUWcIJhqfiJECQAIgUAzfzhdj5AyICaFPaOQ+N8FVMLcTyqeTXP0sIlFk +JStVibemTRCbxdXXM7OVipz1oW3PBVEO3t/VyjiaGGg= +-----END RSA PRIVATE KEY----- diff --git a/example/sp-wsgi/service_conf.py b/example/sp-wsgi/service_conf.py new file mode 100644 index 0000000..e5dbf17 --- /dev/null +++ b/example/sp-wsgi/service_conf.py @@ -0,0 +1,16 @@ +from saml2.assertion import Policy + +PORT = 8087 +HTTPS = False + +# Which groups of entity categories to use +POLICY = Policy( + { + "default": {"entity_categories": ["swamid", "edugain"]} + } +) + +# HTTPS cert information +SERVER_CERT = "pki/ssl.crt" +SERVER_KEY = "pki/ssl.pem" +CERT_CHAIN = "" \ No newline at end of file diff --git a/example/sp-wsgi/sp.py b/example/sp-wsgi/sp.py new file mode 100755 index 0000000..53137e7 --- /dev/null +++ b/example/sp-wsgi/sp.py @@ -0,0 +1,740 @@ +#!/usr/bin/env python +import logging +import re +import argparse +import service_conf + +from Cookie import SimpleCookie +from urlparse import parse_qs +import sys + +from saml2 import BINDING_HTTP_REDIRECT +from saml2 import BINDING_SOAP +from saml2 import time_util +from saml2 import ecp +from saml2 import BINDING_HTTP_ARTIFACT +from saml2 import BINDING_HTTP_POST +from saml2.client import Saml2Client +from saml2.ecp_client import PAOS_HEADER_INFO +from saml2.httputil import geturl, make_cookie, parse_cookie +from saml2.httputil import get_post +from saml2.httputil import Response +from saml2.httputil import BadRequest +from saml2.httputil import ServiceError +from saml2.httputil import SeeOther +from saml2.httputil import Unauthorized +from saml2.httputil import NotFound +from saml2.httputil import Redirect +from saml2.httputil import NotImplemented +from saml2.response import StatusError +from saml2.response import VerificationError +from saml2.s_utils import UnknownPrincipal +from saml2.s_utils import UnsupportedBinding +from saml2.s_utils import sid +from saml2.s_utils import rndstr +#from srtest import exception_trace + +logger = logging.getLogger("") +hdlr = logging.FileHandler('spx.log') +base_formatter = logging.Formatter( + "%(asctime)s %(name)s:%(levelname)s %(message)s") + +hdlr.setFormatter(base_formatter) +logger.addHandler(hdlr) +logger.setLevel(logging.INFO) + + +SP = None +SEED = "" +POLICY = None + + +def dict_to_table(ava, lev=0, width=1): + txt = ['\n' % width] + for prop, valarr in ava.items(): + txt.append("\n") + if isinstance(valarr, basestring): + txt.append("\n" % str(prop)) + try: + txt.append("\n" % valarr.encode("utf8")) + except AttributeError: + txt.append("\n" % valarr) + elif isinstance(valarr, list): + i = 0 + n = len(valarr) + for val in valarr: + if not i: + txt.append("\n") + if isinstance(val, dict): + txt.append("\n") + else: + try: + txt.append("\n" % val.encode("utf8")) + except AttributeError: + txt.append("\n" % val) + if n > 1: + txt.append("\n") + n -= 1 + i += 1 + elif isinstance(valarr, dict): + txt.append("\n" % prop) + txt.append("\n") + txt.append("\n") + txt.append('
%s%s%s%s\n" % (len(valarr), prop)) + else: + txt.append("
\n") + txt.extend(dict_to_table(val, lev + 1, width - 1)) + txt.append("%s%s
%s\n") + txt.extend(dict_to_table(valarr, lev + 1, width - 1)) + txt.append("
\n') + return txt + + +def handle_static(environ, start_response, path): + """ + Creates a response for a static file. There might be a longer path + then just /static/... if so strip the path leading up to static. + + :param environ: wsgi enviroment + :param start_response: wsgi start response + :param path: the static file and path to the file. + :return: wsgi response for the static file. + """ + try: + text = open(path).read() + if path.endswith(".ico"): + resp = Response(text, headers=[('Content-Type', "image/x-icon")]) + elif path.endswith(".html"): + resp = Response(text, headers=[('Content-Type', 'text/html')]) + elif path.endswith(".txt"): + resp = Response(text, headers=[('Content-Type', 'text/plain')]) + elif path.endswith(".css"): + resp = Response(text, headers=[('Content-Type', 'text/css')]) + elif path.endswith(".js"): + resp = Response(text, headers=[('Content-Type', 'text/javascript')]) + elif path.endswith(".png"): + resp = Response(text, headers=[('Content-Type', 'image/png')]) + else: + resp = Response(text) + except IOError: + resp = NotFound() + return resp(environ, start_response) + + +class ECPResponse(object): + code = 200 + title = 'OK' + + def __init__(self, content): + self.content = content + + #noinspection PyUnusedLocal + def __call__(self, environ, start_response): + start_response('%s %s' % (self.code, self.title), + [('Content-Type', "text/xml")]) + return [self.content] + + +def _expiration(timeout, tformat=None): + # Wed, 06-Jun-2012 01:34:34 GMT + if not tformat: + tformat = '%a, %d-%b-%Y %T GMT' + + if timeout == "now": + return time_util.instant(tformat) + else: + # validity time should match lifetime of assertions + return time_util.in_a_while(minutes=timeout, format=tformat) + + +class Cache(object): + def __init__(self): + self.uid2user = {} + self.cookie_name = "spauthn" + self.outstanding_queries = {} + self.relay_state = {} + self.user = {} + self.result = {} + + def kaka2user(self, kaka): + logger.debug("KAKA: %s" % kaka) + if kaka: + cookie_obj = SimpleCookie(kaka) + morsel = cookie_obj.get(self.cookie_name, None) + if morsel: + try: + return self.uid2user[morsel.value] + except KeyError: + return None + else: + logger.debug("No spauthn cookie") + return None + + def delete_cookie(self, environ=None, kaka=None): + if not kaka: + kaka = environ.get("HTTP_COOKIE", '') + logger.debug("delete KAKA: %s" % kaka) + if kaka: + _name = self.cookie_name + cookie_obj = SimpleCookie(kaka) + morsel = cookie_obj.get(_name, None) + cookie = SimpleCookie() + cookie[_name] = "" + cookie[_name]['path'] = "/" + logger.debug("Expire: %s" % morsel) + cookie[_name]["expires"] = _expiration("dawn") + return tuple(cookie.output().split(": ", 1)) + return None + + def user2kaka(self, user): + uid = rndstr(32) + self.uid2user[uid] = user + cookie = SimpleCookie() + cookie[self.cookie_name] = uid + cookie[self.cookie_name]['path'] = "/" + cookie[self.cookie_name]["expires"] = _expiration(480) + logger.debug("Cookie expires: %s" % cookie[self.cookie_name]["expires"]) + return tuple(cookie.output().split(": ", 1)) + + +# ----------------------------------------------------------------------------- +# RECEIVERS +# ----------------------------------------------------------------------------- + + +class Service(object): + def __init__(self, environ, start_response, user=None): + self.environ = environ + logger.debug("ENVIRON: %s" % environ) + self.start_response = start_response + self.user = user + self.sp = None + + def unpack_redirect(self): + if "QUERY_STRING" in self.environ: + _qs = self.environ["QUERY_STRING"] + return dict([(k, v[0]) for k, v in parse_qs(_qs).items()]) + else: + return None + + def unpack_post(self): + _dict = parse_qs(get_post(self.environ)) + logger.debug("unpack_post:: %s" % _dict) + try: + return dict([(k, v[0]) for k, v in _dict.items()]) + except Exception: + return None + + def unpack_soap(self): + try: + query = get_post(self.environ) + return {"SAMLResponse": query, "RelayState": ""} + except Exception: + return None + + def unpack_either(self): + if self.environ["REQUEST_METHOD"] == "GET": + _dict = self.unpack_redirect() + elif self.environ["REQUEST_METHOD"] == "POST": + _dict = self.unpack_post() + else: + _dict = None + logger.debug("_dict: %s" % _dict) + return _dict + + def operation(self, _dict, binding): + logger.debug("_operation: %s" % _dict) + if not _dict: + resp = BadRequest('Error parsing request or no request') + return resp(self.environ, self.start_response) + else: + try: + _relay_state = _dict["RelayState"] + except KeyError: + _relay_state = "" + if "SAMLResponse" in _dict: + return self.do(_dict["SAMLResponse"], binding, + _relay_state, mtype="response") + elif "SAMLRequest" in _dict: + return self.do(_dict["SAMLRequest"], binding, + _relay_state, mtype="request") + + def artifact_operation(self, _dict): + if not _dict: + resp = BadRequest("Missing query") + return resp(self.environ, self.start_response) + else: + # exchange artifact for response + request = self.sp.artifact2message(_dict["SAMLart"], "spsso") + return self.do(request, BINDING_HTTP_ARTIFACT, _dict["RelayState"]) + + def response(self, binding, http_args): + if binding == BINDING_HTTP_ARTIFACT: + resp = Redirect() + else: + resp = Response(http_args["data"], headers=http_args["headers"]) + return resp(self.environ, self.start_response) + + def do(self, query, binding, relay_state="", mtype="response"): + pass + + def redirect(self): + """ Expects a HTTP-redirect response """ + + _dict = self.unpack_redirect() + return self.operation(_dict, BINDING_HTTP_REDIRECT) + + def post(self): + """ Expects a HTTP-POST response """ + + _dict = self.unpack_post() + return self.operation(_dict, BINDING_HTTP_POST) + + def artifact(self): + # Can be either by HTTP_Redirect or HTTP_POST + _dict = self.unpack_either() + return self.artifact_operation(_dict) + + def soap(self): + """ + Single log out using HTTP_SOAP binding + """ + logger.debug("- SOAP -") + _dict = self.unpack_soap() + logger.debug("_dict: %s" % _dict) + return self.operation(_dict, BINDING_SOAP) + + def uri(self): + _dict = self.unpack_either() + return self.operation(_dict, BINDING_SOAP) + + def not_authn(self): + resp = Unauthorized('Unknown user') + return resp(self.environ, self.start_response) + + +# ----------------------------------------------------------------------------- +# Attribute Consuming service +# ----------------------------------------------------------------------------- + + +class ACS(Service): + def __init__(self, sp, environ, start_response, cache=None, **kwargs): + Service.__init__(self, environ, start_response) + self.sp = sp + self.outstanding_queries = cache.outstanding_queries + self.cache = cache + self.response = None + self.kwargs = kwargs + + def do(self, response, binding, relay_state="", mtype="response"): + """ + :param response: The SAML response, transport encoded + :param binding: Which binding the query came in over + """ + #tmp_outstanding_queries = dict(self.outstanding_queries) + if not response: + logger.info("Missing Response") + resp = Unauthorized('Unknown user') + return resp(self.environ, self.start_response) + + try: + self.response = self.sp.parse_authn_request_response( + response, binding, self.outstanding_queries) + except UnknownPrincipal, excp: + logger.error("UnknownPrincipal: %s" % (excp,)) + resp = ServiceError("UnknownPrincipal: %s" % (excp,)) + return resp(self.environ, self.start_response) + except UnsupportedBinding, excp: + logger.error("UnsupportedBinding: %s" % (excp,)) + resp = ServiceError("UnsupportedBinding: %s" % (excp,)) + return resp(self.environ, self.start_response) + except VerificationError, err: + resp = ServiceError("Verification error: %s" % (err,)) + return resp(self.environ, self.start_response) + except Exception, err: + resp = ServiceError("Other error: %s" % (err,)) + return resp(self.environ, self.start_response) + + logger.info("AVA: %s" % self.response.ava) + resp = Response(dict_to_table(self.response.ava)) + return resp(self.environ, self.start_response) + + def verify_attributes(self, ava): + logger.info("SP: %s" % self.sp.config.entityid) + rest = POLICY.get_entity_categories_restriction( + self.sp.config.entityid, self.sp.metadata) + + akeys = [k.lower() for k in ava.keys()] + + res = {"less": [], "more": []} + for key, attr in rest.items(): + if key not in ava: + if key not in akeys: + res["less"].append(key) + + for key, attr in ava.items(): + _key = key.lower() + if _key not in rest: + res["more"].append(key) + + return res + +# ----------------------------------------------------------------------------- +# REQUESTERS +# ----------------------------------------------------------------------------- + + +class SSO(object): + def __init__(self, sp, environ, start_response, cache=None, + wayf=None, discosrv=None, bindings=None): + self.sp = sp + self.environ = environ + self.start_response = start_response + self.cache = cache + self.idp_query_param = "IdpQuery" + self.wayf = wayf + self.discosrv = discosrv + if bindings: + self.bindings = bindings + else: + self.bindings = [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST, + BINDING_HTTP_ARTIFACT] + logger.debug("--- SSO ---") + + def response(self, binding, http_args, do_not_start_response=False): + if binding == BINDING_HTTP_ARTIFACT: + resp = Redirect() + elif binding == BINDING_HTTP_REDIRECT: + for param, value in http_args["headers"]: + if param == "Location": + resp = SeeOther(str(value)) + break + else: + resp = ServiceError("Parameter error") + else: + resp = Response(http_args["data"], headers=http_args["headers"]) + + if do_not_start_response: + return resp + else: + return resp(self.environ, self.start_response) + + def _wayf_redirect(self, came_from): + sid_ = sid() + self.cache.outstanding_queries[sid_] = came_from + logger.debug("Redirect to WAYF function: %s" % self.wayf) + return -1, SeeOther(headers=[('Location', "%s?%s" % (self.wayf, sid_))]) + + def _pick_idp(self, came_from): + """ + If more than one idp and if none is selected, I have to do wayf or + disco + """ + + _cli = self.sp + + logger.debug("[_pick_idp] %s" % self.environ) + if "HTTP_PAOS" in self.environ: + if self.environ["HTTP_PAOS"] == PAOS_HEADER_INFO: + if 'application/vnd.paos+xml' in self.environ["HTTP_ACCEPT"]: + # Where should I redirect the user to + # entityid -> the IdP to use + # relay_state -> when back from authentication + + logger.debug("- ECP client detected -") + + _rstate = rndstr() + self.cache.relay_state[_rstate] = geturl(self.environ) + _entityid = _cli.config.ecp_endpoint( + self.environ["REMOTE_ADDR"]) + + if not _entityid: + return -1, ServiceError("No IdP to talk to") + logger.debug("IdP to talk to: %s" % _entityid) + return ecp.ecp_auth_request(_cli, _entityid, _rstate) + else: + return -1, ServiceError('Faulty Accept header') + else: + return -1, ServiceError('unknown ECP version') + + # Find all IdPs + idps = self.sp.metadata.with_descriptor("idpsso") + + idp_entity_id = None + + kaka = self.environ.get("HTTP_COOKIE", '') + if kaka: + try: + (idp_entity_id, _) = parse_cookie("ve_disco", "SEED_SAW", kaka) + except ValueError: + pass + except TypeError: + pass + + # Any specific IdP specified in a query part + query = self.environ.get("QUERY_STRING") + if not idp_entity_id and query: + try: + _idp_entity_id = dict(parse_qs(query))[ + self.idp_query_param][0] + if _idp_entity_id in idps: + idp_entity_id = _idp_entity_id + except KeyError: + logger.debug("No IdP entity ID in query: %s" % query) + pass + + if not idp_entity_id: + + if self.wayf: + if query: + try: + wayf_selected = dict(parse_qs(query))[ + "wayf_selected"][0] + except KeyError: + return self._wayf_redirect(came_from) + idp_entity_id = wayf_selected + else: + return self._wayf_redirect(came_from) + elif self.discosrv: + if query: + idp_entity_id = _cli.parse_discovery_service_response( + query=self.environ.get("QUERY_STRING")) + if not idp_entity_id: + sid_ = sid() + self.cache.outstanding_queries[sid_] = came_from + logger.debug("Redirect to Discovery Service function") + eid = _cli.config.entityid + ret = _cli.config.getattr("endpoints", + "sp")["discovery_response"][0][0] + ret += "?sid=%s" % sid_ + loc = _cli.create_discovery_service_request( + self.discosrv, eid, **{"return": ret}) + return -1, SeeOther(loc) + elif len(idps) == 1: + # idps is a dictionary + idp_entity_id = idps.keys()[0] + elif not len(idps): + return -1, ServiceError('Misconfiguration') + else: + return -1, NotImplemented("No WAYF or DS present!") + + logger.info("Chosen IdP: '%s'" % idp_entity_id) + return 0, idp_entity_id + + def redirect_to_auth(self, _cli, entity_id, came_from, vorg_name=""): + try: + _binding, destination = _cli.pick_binding( + "single_sign_on_service", self.bindings, "idpsso", + entity_id=entity_id) + logger.debug("binding: %s, destination: %s" % (_binding, + destination)) + req = _cli.create_authn_request(destination, vorg=vorg_name) + _rstate = rndstr() + self.cache.relay_state[_rstate] = came_from + ht_args = _cli.apply_binding(_binding, "%s" % req, destination, + relay_state=_rstate) + _sid = req.id + logger.debug("ht_args: %s" % ht_args) + except Exception, exc: + logger.exception(exc) + resp = ServiceError( + "Failed to construct the AuthnRequest: %s" % exc) + return resp(self.environ, self.start_response) + + # remember the request + self.cache.outstanding_queries[_sid] = came_from + return self.response(_binding, ht_args, do_not_start_response=True) + + def do(self): + _cli = self.sp + + # Which page was accessed to get here + came_from = geturl(self.environ) + logger.debug("[sp.challenge] RelayState >> '%s'" % came_from) + + # Am I part of a virtual organization or more than one ? + try: + vorg_name = _cli.vorg._name + except AttributeError: + vorg_name = "" + + logger.debug("[sp.challenge] VO: %s" % vorg_name) + + # If more than one idp and if none is selected, I have to do wayf + (done, response) = self._pick_idp(came_from) + # Three cases: -1 something went wrong or Discovery service used + # 0 I've got an IdP to send a request to + # >0 ECP in progress + logger.debug("_idp_pick returned: %s" % done) + if done == -1: + return response(self.environ, self.start_response) + elif done > 0: + self.cache.outstanding_queries[done] = came_from + return ECPResponse(response) + else: + entity_id = response + # Do the AuthnRequest + resp = self.redirect_to_auth(_cli, entity_id, came_from, vorg_name) + return resp(self.environ, self.start_response) + + +# ---------------------------------------------------------------------------- + + +#noinspection PyUnusedLocal +def not_found(environ, start_response): + """Called if no URL matches.""" + resp = NotFound('Not Found') + return resp(environ, start_response) + + +# ---------------------------------------------------------------------------- + + +#noinspection PyUnusedLocal +def main(environ, start_response, _sp): + _sso = SSO(_sp, environ, start_response, cache=CACHE, **ARGS) + return _sso.do() + + +#noinspection PyUnusedLocal +def verify_login_cookie(environ, start_response, _sp): + _sso = SSO(_sp, environ, start_response, cache=CACHE, **ARGS) + return _sso.do() + + +def disco(environ, start_response, _sp): + query = parse_qs(environ["QUERY_STRING"]) + entity_id = query["entityID"][0] + _sid = query["sid"][0] + came_from = CACHE.outstanding_queries[_sid] + _sso = SSO(_sp, environ, start_response, cache=CACHE, **ARGS) + resp = _sso.redirect_to_auth(_sso.sp, entity_id, came_from) + + # Add cookie + kaka = make_cookie("ve_disco", entity_id, "SEED_SAW") + resp.headers.append(kaka) + return resp(environ, start_response) + +# ---------------------------------------------------------------------------- + +# map urls to functions +urls = [ + # Hmm, place holder, NOT used + ('place', ("holder", None)), + (r'^$', main), + (r'^login', verify_login_cookie), + (r'^disco', disco) +] + + +def add_urls(): + base = "acs" + + urls.append(("%s/post$" % base, (ACS, "post", SP))) + urls.append(("%s/post/(.*)$" % base, (ACS, "post", SP))) + urls.append(("%s/redirect$" % base, (ACS, "redirect", SP))) + urls.append(("%s/redirect/(.*)$" % base, (ACS, "redirect", SP))) + +# ---------------------------------------------------------------------------- + + +def application(environ, start_response): + """ + The main WSGI application. Dispatch the current request to + the functions from above. + + If nothing matches call the `not_found` function. + + :param environ: The HTTP application environment + :param start_response: The application to run when the handling of the + request is done + :return: The response as a list of lines + """ + path = environ.get('PATH_INFO', '').lstrip('/') + logger.debug(" PATH: '%s'" % path) + + logger.debug("Finding callback to run") + try: + for regex, spec in urls: + match = re.search(regex, path) + if match is not None: + if isinstance(spec, tuple): + callback, func_name, _sp = spec + cls = callback(_sp, environ, start_response, cache=CACHE) + func = getattr(cls, func_name) + return func() + else: + return spec(environ, start_response, SP) + if re.match(".*static/.*", path): + return handle_static(environ, start_response, path) + return not_found(environ, start_response) + except StatusError, err: + logging.error("StatusError: %s" % err) + resp = BadRequest("%s" % err) + return resp(environ, start_response) + except Exception, err: + #_err = exception_trace("RUN", err) + #logging.error(exception_trace("RUN", _err)) + print >> sys.stderr, err + resp = ServiceError("%s" % err) + return resp(environ, start_response) + +# ---------------------------------------------------------------------------- + +PORT = service_conf.PORT +# ------- HTTPS ------- +# These should point to relevant files +SERVER_CERT = service_conf.SERVER_CERT +SERVER_KEY = service_conf.SERVER_KEY +# This is of course the certificate chain for the CA that signed +# you cert and all the way up to the top +CERT_CHAIN = service_conf.CERT_CHAIN + +if __name__ == '__main__': + from cherrypy import wsgiserver + from cherrypy.wsgiserver import ssl_pyopenssl + + _parser = argparse.ArgumentParser() + _parser.add_argument('-d', dest='debug', action='store_true', + help="Print debug information") + _parser.add_argument('-D', dest='discosrv', + help="Which disco server to use") + _parser.add_argument('-s', dest='seed', + help="Cookie seed") + _parser.add_argument('-W', dest='wayf', action='store_true', + help="Which WAYF url to use") + _parser.add_argument("config", help="SAML client config") + + ARGS = {} + _args = _parser.parse_args() + if _args.discosrv: + ARGS["discosrv"] = _args.discosrv + if _args.wayf: + ARGS["wayf"] = _args.wayf + + CACHE = Cache() + CNFBASE = _args.config + if _args.seed: + SEED = _args.seed + else: + SEED = "SnabbtInspel" + + SP = Saml2Client(config_file="%s" % CNFBASE) + + POLICY = service_conf.POLICY + + add_urls() + + SRV = wsgiserver.CherryPyWSGIServer(('0.0.0.0', PORT), application) + + if service_conf.HTTPS: + SRV.ssl_adapter = ssl_pyopenssl.pyOpenSSLAdapter(SERVER_CERT, + SERVER_KEY, CERT_CHAIN) + logger.info("Server starting") + print "SP listening on port: %s" % PORT + try: + SRV.start() + except KeyboardInterrupt: + SRV.stop() diff --git a/example/sp-wsgi/sp.xml b/example/sp-wsgi/sp.xml new file mode 100644 index 0000000..4054ecc --- /dev/null +++ b/example/sp-wsgi/sp.xml @@ -0,0 +1,34 @@ + +http://www.geant.net/uri/dataprotection-code-of-conduct/v1MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV +BAYTAlNFMQ0wCwYDVQQHEwRVbWVhMRgwFgYDVQQKEw9VbWVhIFVuaXZlcnNpdHkx +EDAOBgNVBAsTB0lUIFVuaXQxEDAOBgNVBAMTB1Rlc3QgU1AwHhcNMDkxMDI2MTMz +MTE1WhcNMTAxMDI2MTMzMTE1WjBaMQswCQYDVQQGEwJTRTENMAsGA1UEBxMEVW1l +YTEYMBYGA1UEChMPVW1lYSBVbml2ZXJzaXR5MRAwDgYDVQQLEwdJVCBVbml0MRAw +DgYDVQQDEwdUZXN0IFNQMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkJWP7 +bwOxtH+E15VTaulNzVQ/0cSbM5G7abqeqSNSs0l0veHr6/ROgW96ZeQ57fzVy2MC +FiQRw2fzBs0n7leEmDJyVVtBTavYlhAVXDNa3stgvh43qCfLx+clUlOvtnsoMiiR +mo7qf0BoPKTj7c0uLKpDpEbAHQT4OF1HRYVxMwIDAQABo4G/MIG8MB0GA1UdDgQW +BBQ7RgbMJFDGRBu9o3tDQDuSoBy7JjCBjAYDVR0jBIGEMIGBgBQ7RgbMJFDGRBu9 +o3tDQDuSoBy7JqFepFwwWjELMAkGA1UEBhMCU0UxDTALBgNVBAcTBFVtZWExGDAW +BgNVBAoTD1VtZWEgVW5pdmVyc2l0eTEQMA4GA1UECxMHSVQgVW5pdDEQMA4GA1UE +AxMHVGVzdCBTUIIJAJHg2V5J31I8MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF +BQADgYEAMuRwwXRnsiyWzmRikpwinnhTmbooKm5TINPE7A7gSQ710RxioQePPhZO +zkM27NnHTrCe2rBVg0EGz7QTd1JIwLPvgoj4VTi/fSha/tXrYUaqc9AqU1kWI4WN ++vffBGQ09mo+6CffuFTZYeOhzP/2stAPwCTU4kxEoiy0KpZMANI= +MIIC8jCCAlugAwIBAgIJAJHg2V5J31I8MA0GCSqGSIb3DQEBBQUAMFoxCzAJBgNV +BAYTAlNFMQ0wCwYDVQQHEwRVbWVhMRgwFgYDVQQKEw9VbWVhIFVuaXZlcnNpdHkx +EDAOBgNVBAsTB0lUIFVuaXQxEDAOBgNVBAMTB1Rlc3QgU1AwHhcNMDkxMDI2MTMz +MTE1WhcNMTAxMDI2MTMzMTE1WjBaMQswCQYDVQQGEwJTRTENMAsGA1UEBxMEVW1l +YTEYMBYGA1UEChMPVW1lYSBVbml2ZXJzaXR5MRAwDgYDVQQLEwdJVCBVbml0MRAw +DgYDVQQDEwdUZXN0IFNQMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkJWP7 +bwOxtH+E15VTaulNzVQ/0cSbM5G7abqeqSNSs0l0veHr6/ROgW96ZeQ57fzVy2MC +FiQRw2fzBs0n7leEmDJyVVtBTavYlhAVXDNa3stgvh43qCfLx+clUlOvtnsoMiiR +mo7qf0BoPKTj7c0uLKpDpEbAHQT4OF1HRYVxMwIDAQABo4G/MIG8MB0GA1UdDgQW +BBQ7RgbMJFDGRBu9o3tDQDuSoBy7JjCBjAYDVR0jBIGEMIGBgBQ7RgbMJFDGRBu9 +o3tDQDuSoBy7JqFepFwwWjELMAkGA1UEBhMCU0UxDTALBgNVBAcTBFVtZWExGDAW +BgNVBAoTD1VtZWEgVW5pdmVyc2l0eTEQMA4GA1UECxMHSVQgVW5pdDEQMA4GA1UE +AxMHVGVzdCBTUIIJAJHg2V5J31I8MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF +BQADgYEAMuRwwXRnsiyWzmRikpwinnhTmbooKm5TINPE7A7gSQ710RxioQePPhZO +zkM27NnHTrCe2rBVg0EGz7QTd1JIwLPvgoj4VTi/fSha/tXrYUaqc9AqU1kWI4WN ++vffBGQ09mo+6CffuFTZYeOhzP/2stAPwCTU4kxEoiy0KpZMANI= +