diff options
author | Roland Hedberg <roland.hedberg@adm.umu.se> | 2013-03-29 10:37:42 +0100 |
---|---|---|
committer | Roland Hedberg <roland.hedberg@adm.umu.se> | 2013-03-29 10:37:42 +0100 |
commit | b1121217f3d8afc4abe0c98554b72eca354d1b90 (patch) | |
tree | d6b1ccdcb785152f9fd8f664d08eac9c1d2c0645 | |
parent | 1e161d0d1cc37ff90feb028d4365f48bb69e6ed6 (diff) | |
download | pysaml2-b1121217f3d8afc4abe0c98554b72eca354d1b90.tar.gz |
Worked on the SP test part.
-rwxr-xr-x | script/saml2i.py | 9 | ||||
-rw-r--r-- | setup.py | 12 | ||||
-rw-r--r-- | src/sp_test/__init__.py | 158 | ||||
-rw-r--r-- | src/sp_test/base.py | 348 | ||||
-rw-r--r-- | src/sp_test/check.py | 54 | ||||
-rw-r--r-- | src/sp_test/tests.py | 136 | ||||
-rw-r--r-- | src/srtest/__init__.py | 4 | ||||
-rwxr-xr-x | tests/localhost.py | 18 | ||||
-rw-r--r-- | tests/sp.xml | 116 |
9 files changed, 743 insertions, 112 deletions
diff --git a/script/saml2i.py b/script/saml2i.py new file mode 100755 index 00000000..14f94303 --- /dev/null +++ b/script/saml2i.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +__author__ = 'rohe0002' + +from sp_test import tests +from sp_test import Client +from sp_test.check import factory + +cli = Client(tests, factory) +cli.run()
\ No newline at end of file @@ -26,17 +26,17 @@ setup( author = "Roland Hedberg", author_email = "roland.hedberg@adm.umu.se", license="Apache 2.0", - packages=["idp_test", "idp_test/package", "srtest"], + packages=["idp_test", "idp_test/package", "srtest", "sp_test"], package_dir = {"": "src"}, - classifiers = ["Development Status :: 4 - Beta", - "License :: OSI Approved :: Apache Software License", - "Topic :: Software Development :: Libraries :: Python Modules"], + classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: Apache Software License", + "Topic :: Software Development :: Libraries :: Python Modules"], install_requires = ["pysaml2", "mechanize", "argparse", "beautifulsoup4", "mako"], - zip_safe=False, - scripts=["script/saml2c.py"] + scripts=["script/saml2c.py", "script/saml2i.py"] )
\ No newline at end of file diff --git a/src/sp_test/__init__.py b/src/sp_test/__init__.py index 5440d8c7..13e685a9 100644 --- a/src/sp_test/__init__.py +++ b/src/sp_test/__init__.py @@ -1,10 +1,20 @@ -from importlib import import_module import json import argparse -from idp_test import Trace import sys +from importlib import import_module + +from idp_test import Trace, SCHEMA + +from saml2.mdstore import MetadataStore, MetaData +from saml2.saml import NAME_FORMAT_UNSPECIFIED +from saml2.server import Server from saml2.config import IdPConfig +from sp_test.base import Conversation + +from srtest import FatalError, CheckError +from srtest import exception_trace + __author__ = 'rolandh' @@ -33,7 +43,7 @@ class Client(object): self._parser.add_argument( "-l", dest="list", action="store_true", help="List all the test flows as a JSON object") - self._parser.add_argument("-c", dest="idpconfig", default="config_file", + self._parser.add_argument("-c", dest="idpconfig", default="idp_conf", help="Configuration file for the IdP") self._parser.add_argument( "-P", dest="configpath", default=".", @@ -44,9 +54,10 @@ class Client(object): self.interactions = None self.entity_id = None - self.sp_config = None self.constraints = {} self.args = None + self.idp = None + self.idp_config = None def json_config_file(self): if self.args.json_config_file == "-": @@ -56,5 +67,142 @@ class Client(object): def idp_configure(self, metadata_construction=False): sys.path.insert(0, self.args.configpath) - mod = import_module(self.args.spconfig) + mod = import_module(self.args.idpconfig) self.idp_config = IdPConfig().load(mod.CONFIG, metadata_construction) + self.idp = Server(config=self.idp_config) + + def test_summation(self, sid): + status = 0 + for item in self.test_log: + if item["status"] > status: + status = item["status"] + + if status == 0: + status = 1 + + info = { + "id": sid, + "status": status, + "tests": self.test_log + } + + if status == 5: + info["url"] = self.test_log[-1]["url"] + info["htmlbody"] = self.test_log[-1]["message"] + + return info + + def run(self): + self.args = self._parser.parse_args() + + if self.args.metadata: + return self.make_meta() + elif self.args.list: + return self.list_operations() + elif self.args.oper == "check": + return self.verify_metadata() + else: + if not self.args.oper: + raise Exception("Missing test case specification") + self.args.oper = self.args.oper.strip("'") + self.args.oper = self.args.oper.strip('"') + + self.setup() + + try: + oper = self.operations.OPERATIONS[self.args.oper] + except KeyError: + if self.tests: + try: + oper = self.tests.OPERATIONS[self.args.oper] + except ValueError: + print >> sys.stderr, "Undefined testcase" + return + else: + print >> sys.stderr, "Undefined testcase" + return + + opers = [self.operations.PHASES[flow] for flow in oper["sequence"]] + + conv = Conversation(self.idp, self.idp_config, self.trace, + self.interactions, self.json_config, + check_factory=self.check_factory, + entity_id=self.entity_id, + constraints=self.constraints) + try: + conv.do_sequence(opers, oper["tests"]) + self.test_log = conv.test_output + tsum = self.test_summation(self.args.oper) + print >>sys.stdout, json.dumps(tsum) + if tsum["status"] > 1 or self.args.debug: + print >> sys.stderr, self.trace + except CheckError, err: + self.test_log = conv.test_output + tsum = self.test_summation(self.args.oper) + print >>sys.stdout, json.dumps(tsum) + print >> sys.stderr, self.trace + except FatalError, err: + if conv: + self.test_log = conv.test_output + self.test_log.append(exception_trace("RUN", err)) + else: + self.test_log = exception_trace("RUN", err) + tsum = self.test_summation(self.args.oper) + print >>sys.stdout, json.dumps(tsum) + print >> sys.stderr, self.trace + except Exception, err: + if conv: + self.test_log = conv.test_output + self.test_log.append(exception_trace("RUN", err)) + else: + self.test_log = exception_trace("RUN", err) + tsum = self.test_summation(self.args.oper) + print >>sys.stdout, json.dumps(tsum) + + def setup(self): + self.json_config = self.json_config_file() + + _jc = self.json_config + + try: + self.interactions = _jc["interaction"] + except KeyError: + self.interactions = [] + + self.idp_configure() + + metadata = MetadataStore(SCHEMA, self.idp_config.attribute_converters, + self.idp_config.xmlsec_binary) + info = _jc["metadata"].encode("utf-8") + md = MetaData(SCHEMA, self.idp_config.attribute_converters, info) + md.load() + metadata[0] = md + self.idp_config.metadata = metadata + + if self.args.testpackage: + self.tests = import_module("sp_test.package.%s" % + self.args.testpackage) + + try: + self.entity_id = _jc["entity_id"] + # Verify its the correct metadata + assert self.entity_id in md.entity.keys() + except KeyError: + if len(md.entity.keys()) == 1: + self.entity_id = md.entity.keys()[0] + else: + raise Exception("Don't know which entity to talk to") + + if "constraints" in _jc: + self.constraints = _jc["constraints"] + if "name_format" not in self.constraints: + self.constraints["name_format"] = NAME_FORMAT_UNSPECIFIED + + def make_meta(self): + pass + + def list_operations(self): + pass + + def verify_metadata(self): + pass diff --git a/src/sp_test/base.py b/src/sp_test/base.py index a6fa87ed..4bc92546 100644 --- a/src/sp_test/base.py +++ b/src/sp_test/base.py @@ -1,19 +1,64 @@ +import base64 import cookielib -from rrtest import tool +import re +import traceback +import urllib +import sys + +from urlparse import parse_qs +from rrtest import FatalError +from saml2 import BINDING_HTTP_REDIRECT +from saml2 import BINDING_HTTP_POST +from saml2.request import SERVICE2REQUEST + +from srtest import CheckError +from srtest.check import CheckHTTPResponse +from srtest.check import ExpectedError +from srtest.check import INTERACTION +from srtest.check import STATUSCODE +from srtest.interaction import Action +from srtest.interaction import Interaction +from srtest.interaction import InteractionNeeded + +from sp_test.tests import ErrorResponse __author__ = 'rolandh' +import logging + +logger = logging.getLogger(__name__) + +camel2underscore = re.compile('((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))') + -class Conversation(tool.Conversation): - def __init__(self, client, config, trace, interaction, +class Conversation(): + def __init__(self, instance, config, trace, interaction, json_config, check_factory, entity_id, msg_factory=None, - features=None, verbose=False, constraints=None): - tool.Conversation.__init__(self, client, config, trace, - interaction, check_factory, msg_factory, - features, verbose) + features=None, verbose=False, constraints=None, + expect_exception=None): + self.instance = instance + self._config = config + self.trace = trace + self.test_output = [] + self.features = features + self.verbose = verbose + self.check_factory = check_factory + self.msg_factory = msg_factory + self.expect_exception = expect_exception + + self.cjar = {"browser": cookielib.CookieJar(), + "rp": cookielib.CookieJar(), + "service": cookielib.CookieJar()} + + self.protocol_response = [] + self.last_response = None + self.last_content = None + self.response = None + self.interaction = Interaction(self.instance, interaction) + self.exception = None + self.entity_id = entity_id self.cjar = {"rp": cookielib.CookieJar()} - self.args = {} self.qargs = {} self.response_args = {} @@ -24,9 +69,288 @@ class Conversation(tool.Conversation): self.response = None self.oper = None self.idp_constraints = constraints + self.json_config = json_config + self.start_page = json_config["start_page"] + + def check_severity(self, stat): + if stat["status"] >= 4: + self.trace.error("WHERE: %s" % stat["id"]) + self.trace.error("STATUS:%s" % STATUSCODE[stat["status"]]) + try: + self.trace.error("HTTP STATUS: %s" % stat["http_status"]) + except KeyError: + pass + try: + self.trace.error("INFO: %s" % stat["message"]) + except KeyError: + pass + + raise CheckError + + def do_check(self, test, **kwargs): + if isinstance(test, basestring): + chk = self.check_factory(test)(**kwargs) + else: + chk = test(**kwargs) + stat = chk(self, self.test_output) + self.check_severity(stat) + + def err_check(self, test, err=None, bryt=True): + if err: + self.exception = err + chk = self.check_factory(test)() + chk(self, self.test_output) + if bryt: + e = FatalError("%s" % err) + e.trace = "".join(traceback.format_exception(*sys.exc_info())) + raise e + + def test_sequence(self, sequence): + if sequence is None: + return True + + for test in sequence: + if isinstance(test, tuple): + test, kwargs = test + else: + kwargs = {} + self.do_check(test, **kwargs) + if test == ExpectedError: + return False + return True + + def my_endpoints(self): + for serv in ["aa", "aq", "idp"]: + for typ, spec in self._config.getattr("endpoints", serv).items(): + for url, binding in spec: + yield url + + def which_endpoint(self, url): + for serv in ["aa", "aq", "idp"]: + for typ, spec in self._config.getattr("endpoints", serv).items(): + for endp, binding in spec: + if url.startswith(endp): + return typ, binding + return None + + def wb_send(self): + """ + The action that starts the whole sequence, a HTTP GET on a web page + """ + self.last_response = self.instance.send(self.start_page) + + def handle_result(self): + self.do_check(CheckHTTPResponse) + _txt = self.last_response.content + assert _txt.startswith("<h2>") + + def handle_redirect(self): + if self._binding == BINDING_HTTP_REDIRECT: + url, query = self.last_response.headers["location"].split("?") + _dict = parse_qs(query) + try: + self.relay_state = _dict["RelayState"][0] + except KeyError: + self.relay_state = "" + _str = _dict["SAMLRequest"][0] + self.saml_request = self.instance._parse_request( + _str, SERVICE2REQUEST[self._endpoint], self._endpoint, + self._binding) + elif self._binding == BINDING_HTTP_POST: + pass + + def send_idp_response(self, req, resp): + """ + :param req: The expected request + :param resp: The response type to be used + :return: A response + """ + # make sure I got the request I expected + assert isinstance(self.saml_request.message, req._class) + + try: + self.test_sequence(req.tests["post"]) + except KeyError: + pass + + # Pick information from the request that should be in the response + args = self.instance.response_args(self.saml_request.message, + [resp._binding]) + args.update(resp._response_args) + + if resp == ErrorResponse: + func = getattr(self.instance, "create_error_response") + else: + _op = camel2underscore.sub(r'_\1', req._class.c_tag).lower() + func = getattr(self.instance, "create_%s_response" % _op) + + response = func(**args) + + info = self.instance.apply_binding(resp._binding, response, + response.destination, + self.relay_state, + "SAMLResponse", resp._sign) + + if resp._binding == BINDING_HTTP_REDIRECT: + url = None + for param, value in info["headers"]: + if param == "Location": + url = value + break + self.last_response = self.instance.send(url) + elif resp._binding == BINDING_HTTP_POST: + resp = base64.b64encode("%s" % response) + info["data"] = urllib.urlencode({"SAMLResponse": resp, + "RelayState": self.relay_state}) + info["method"] = "POST" + info["headers"] = [('Content-type', + 'application/x-www-form-urlencoded')] + self.last_response = self.instance.send(**info) + + def do_flow(self, flow): + """ + Solicited or 'un-solicited' flows. + + Solicited always starts with the Web client accessing a page. + Un-solicited starts with the IDP sending something. + """ + if len(flow) >= 3: + self.wb_send() + self.intermit(flow[0]._interaction) + self.handle_redirect() + self.send_idp_response(*flow[1:]) + self.handle_result() + + def do_sequence(self, oper, tests=None): + try: + self.test_sequence(tests["pre"]) + except KeyError: + pass + + for flow in oper: + try: + self.do_flow(flow) + except InteractionNeeded: + self.test_output.append({"status": INTERACTION, + "message": self.last_content, + "id": "exception", + "name": "interaction needed", + "url": self.position}) + break + except FatalError: + raise + except Exception: + #self.err_check("exception", err) + raise + + try: + self.test_sequence(tests["post"]) + except KeyError: + pass + + def intermit(self, page_types): + _response = self.last_response + _last_action = None + _same_actions = 0 + if _response.status_code >= 400: + done = True + else: + done = False + + url = _response.url + content = _response.text + while not done: + rdseq = [] + while _response.status_code in [302, 301, 303]: + url = _response.headers["location"] + if url in rdseq: + raise FatalError("Loop detected in redirects") + else: + rdseq.append(url) + if len(rdseq) > 8: + raise FatalError( + "Too long sequence of redirects: %s" % rdseq) + + self.trace.reply("REDIRECT TO: %s" % url) + logger.debug("REDIRECT TO: %s" % url) + # If back to me + for_me = False + try: + self._endpoint, self._binding = self.which_endpoint(url) + for_me = True + except TypeError: + pass + + if for_me: + done = True + break + else: + try: + _response = self.instance.send(url, "GET") + except Exception, err: + raise FatalError("%s" % err) + + content = _response.text + self.trace.reply("CONTENT: %s" % content) + self.position = url + self.last_content = content + self.response = _response + + if _response.status_code >= 400: + done = True + break + + if done or url is None: + break + + _base = url.split("?")[0] + + try: + _spec = self.interaction.pick_interaction(_base, content) + except InteractionNeeded: + self.position = url + self.trace.error("Page Content: %s" % content) + raise + except KeyError: + self.position = url + self.trace.error("Page Content: %s" % content) + self.err_check("interaction-needed") + + if _spec == _last_action: + _same_actions += 1 + if _same_actions >= 3: + raise InteractionNeeded("Interaction loop detection") + else: + _last_action = _spec + + if len(_spec) > 2: + self.trace.info(">> %s <<" % _spec["page-type"]) + if _spec["page-type"] == "login": + self.login_page = content + + _op = Action(_spec["control"]) + + try: + _response = _op(self.instance, self, self.trace, url, + _response, content, self.features) + if isinstance(_response, dict): + self.last_response = _response + self.last_content = _response + return _response + content = _response.text + self.position = url + self.last_content = content + self.response = _response - def init(self, phase): - pass + if _response.status_code >= 400: + break + except (FatalError, InteractionNeeded): + raise + except Exception, err: + self.err_check("exception", err, False) - def send(self): - pass + self.last_response = _response + try: + self.last_content = _response.text + except AttributeError: + self.last_content = None diff --git a/src/sp_test/check.py b/src/sp_test/check.py new file mode 100644 index 00000000..6a934cf0 --- /dev/null +++ b/src/sp_test/check.py @@ -0,0 +1,54 @@ +import inspect +import sys + +from srtest.check import Check +from srtest.check import CRITICAL +from srtest import check +from srtest.interaction import Interaction + +__author__ = 'rolandh' + + +class VerifyContent(Check): + """ Basic content verification class, does required and max/min checks + """ + cid = "verify-content" + + def _func(self, conv): + try: + conv.saml_request.message.verify() + except ValueError: + self._status = CRITICAL + + return {} + + +class MatchResult(Check): + cid = "match-result" + + def _func(self, conv): + interaction = Interaction(conv.instance, [conv.json_config["result"]]) + _int = interaction.pick_interaction(content=conv.last_response.content) + + return {} + +# ============================================================================= + + +CLASS_CACHE = {} + + +def factory(cid, classes=CLASS_CACHE): + if len(classes) == 0: + check.factory(cid, classes) + for name, obj in inspect.getmembers(sys.modules[__name__]): + if inspect.isclass(obj): + try: + classes[obj.cid] = obj + except AttributeError: + pass + + if cid in classes: + return classes[cid] + else: + return None diff --git a/src/sp_test/tests.py b/src/sp_test/tests.py new file mode 100644 index 00000000..6e1b8ee9 --- /dev/null +++ b/src/sp_test/tests.py @@ -0,0 +1,136 @@ +from saml2 import samlp +from saml2 import BINDING_HTTP_REDIRECT +from saml2 import BINDING_HTTP_POST + +from saml2.saml import AUTHN_PASSWORD +from saml2.samlp import STATUS_AUTHN_FAILED +from sp_test.check import VerifyContent +from sp_test.check import MatchResult + +__author__ = 'rolandh' + +USER = { + "adam": { + "given_name": "Adam", + "sn": "Andersson" + }, + "eva": { + "given_name": "Eva", + "sn": "Svensson" + } +} + +AUTHN = (AUTHN_PASSWORD, "http://lingon.catalogix.se/login") + + +class Response(object): + _args = {} + _class = samlp.Response + _sign = False + tests = {"post": [], "pre": []} + + def __init__(self, conv): + self.args = self._args.copy() + self.conv = conv + + def setup(self): + pass + + def pre_processing(self, message, args): + return message + + def post_processing(self, message): + return message + + +class Request(object): + response = "" + _class = None + tests = {"post": [VerifyContent], "pre": []} + + def __init__(self): + pass + + def __call__(self, conv, response): + pass + + +class Operation(object): + pass + + +class AuthnResponse(Response): + _response_args = { + "identity": USER["adam"], + "userid": "adam", + #"name_id": None, + "authn": AUTHN + } + _binding = BINDING_HTTP_POST + + +class AuthnResponse_redirect(AuthnResponse): + _binding = BINDING_HTTP_REDIRECT + + +class ErrorResponse(Response): + _response_args = { + "info": (STATUS_AUTHN_FAILED, "Unknown user") + } + _binding = BINDING_HTTP_POST + + +class LogoutResponse(Response): + _class = samlp.LogoutRequest + pass + + +class Login(Operation): + _interaction = ["wayf"] + + +class AuthnRequest(Request): + _class = samlp.AuthnRequest + + + +PHASES = { + "login": (Login, AuthnRequest, AuthnResponse), + "login_redirect": (Login, AuthnRequest, AuthnResponse_redirect), + "login_error": (Login, AuthnRequest, ErrorResponse) +} + +OPERATIONS = { + 'login': { + "name": 'Basic Login test', + "descr": 'Basic Login test', + "sequence": ["login"], + "tests": {"pre": [], "post": [MatchResult]} + }, + 'verify': { + "name": 'Verify various aspects of the generated AuthnRequest message', + "descr": 'Basic Login test', + "sequence": [], + "tests": {"pre": [], "post": []} + }, + 'sp-01':{ + "name": "SP should not accept a Response as valid, when the StatusCode is not success", + "sequence": ["login_error"], + "tests": {"pre": [], "post": []} + }, + 'sp-02':{ + "name": "SP should accept a NameID with Format: persistent" + }, + 'sp-03':{ + "name": "SP should accept a NameID with Format: e-mail" + }, + 'sp-04':{ + "name": "Do SP work with unknown NameID Format, such as : foo" + }, + 'sp-05':{ + "name": "SP should accept a Response without a SubjectConfirmationData element" + }, + 'sp-06':{ + "name": "SP should accept unsolicited response (no in_response_to attribute)" + }, +}
\ No newline at end of file diff --git a/src/srtest/__init__.py b/src/srtest/__init__.py index 55b9ad11..999ccdbe 100644 --- a/src/srtest/__init__.py +++ b/src/srtest/__init__.py @@ -12,6 +12,10 @@ class FatalError(Exception): pass +class CheckError(Exception): + pass + + class HTTP_ERROR(Exception): pass diff --git a/tests/localhost.py b/tests/localhost.py index 0632f248..75f62b12 100755 --- a/tests/localhost.py +++ b/tests/localhost.py @@ -28,9 +28,11 @@ info = { "url": "%s/sso/redirect" % BASE, "title": "SAML 2.0 POST" }, + "page-type": "other", "control": { - "type": "response", - "pick": {"form": {"action":"%s/acs" % BASE}} + "index": 0, + "type": "form", + "set": {} } }, { @@ -38,9 +40,11 @@ info = { "url": "%s/sso/post" % BASE, "title": "SAML 2.0 POST" }, + "page-type": "other", "control": { - "type": "response", - "pick": {"form": {"action":"%s/acs" % BASE}} + "index": 0, + "type": "form", + "set": {} } }, { @@ -48,9 +52,11 @@ info = { "url": "%s/slo/post" % BASE, "title": "SAML 2.0 POST" }, + "page-type": "other", "control": { - "type": "response", - "pick": {"form": {"action":"%s/sls" % BASE}} + "index": 0, + "type": "form", + "set": {} } } ], diff --git a/tests/sp.xml b/tests/sp.xml index 346188f4..fb3142e3 100644 --- a/tests/sp.xml +++ b/tests/sp.xml @@ -1,84 +1,34 @@ <?xml version='1.0' encoding='UTF-8'?> -<ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata" - xmlns:ns1="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" - xmlns:ns2="http://www.w3.org/2000/09/xmldsig#" - entityID="http://lingon.ladok.umu.se:8087/sp.xml"> - <ns0:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" - protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> - <ns0:Extensions> - <ns1:DiscoveryResponse - Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" - Location="http://lingon.ladok.umu.se:8087/disco" index="1"/> - </ns0:Extensions> - <ns0:KeyDescriptor use="signing"> - <ns2:KeyInfo> - <ns2:X509Data> - <ns2:X509Certificate> - 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= - </ns2:X509Certificate> - </ns2:X509Data> - </ns2:KeyInfo> - </ns0:KeyDescriptor> - <ns0:ArtifactResolutionService - Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" - Location="http://lingon.ladok.umu.se:8087/ars" index="1"/> - <ns0:SingleLogoutService - Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" - Location="http://lingon.ladok.umu.se:8087/sls"/> - <ns0:ManageNameIDService - Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" - Location="http://lingon.ladok.umu.se:8087/mni"/> - <ns0:ManageNameIDService - Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - Location="http://lingon.ladok.umu.se:8087/mni"/> - <ns0:ManageNameIDService - Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" - Location="http://lingon.ladok.umu.se:8087/mni"/> - <ns0:ManageNameIDService - Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" - Location="http://lingon.ladok.umu.se:8087/acs/artifact"/> - <ns0:AssertionConsumerService - Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" - Location="http://lingon.ladok.umu.se:8087/acs/post" index="1"/> - <ns0:AssertionConsumerService - Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - Location="http://lingon.ladok.umu.se:8087/acs/redirect" - index="2"/> - <ns0:AssertionConsumerService - Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" - Location="http://lingon.ladok.umu.se:8087/acs/artifact" - index="3"/> - <ns0:AssertionConsumerService - Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" - Location="http://lingon.ladok.umu.se:8087/ecp" index="4"/> - </ns0:SPSSODescriptor> - <ns0:Organization> - <ns0:OrganizationName xml:lang="se">AB Exempel</ns0:OrganizationName> - <ns0:OrganizationDisplayName xml:lang="se">AB Exempel - </ns0:OrganizationDisplayName> - <ns0:OrganizationURL xml:lang="en">http://www.example.org - </ns0:OrganizationURL> - </ns0:Organization> - <ns0:ContactPerson contactType="technical"> - <ns0:GivenName>Roland</ns0:GivenName> - <ns0:SurName>Hedberg</ns0:SurName> - <ns0:EmailAddress>tech@eample.com</ns0:EmailAddress> - <ns0:EmailAddress>tech@example.org</ns0:EmailAddress> - <ns0:TelephoneNumber>+46 70 100 0000</ns0:TelephoneNumber> - </ns0:ContactPerson> -</ns0:EntityDescriptor> +<ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ns1="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" xmlns:ns2="http://www.w3.org/2000/09/xmldsig#" entityID="https://lingon.ladok.umu.se:8087/sp.xml"><ns0:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><ns0:Extensions><ns1:DiscoveryResponse Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location="https://lingon.ladok.umu.se:8087/disco" index="1" /></ns0:Extensions><ns0:KeyDescriptor use="encryption"><ns2:KeyInfo><ns2:X509Data><ns2:X509Certificate>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= +</ns2:X509Certificate></ns2:X509Data></ns2:KeyInfo></ns0:KeyDescriptor><ns0:KeyDescriptor use="signing"><ns2:KeyInfo><ns2:X509Data><ns2:X509Certificate>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= +</ns2:X509Certificate></ns2:X509Data></ns2:KeyInfo></ns0:KeyDescriptor><ns0:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://lingon.ladok.umu.se:8087/ars" index="1" /><ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://lingon.ladok.umu.se:8087/sls" /><ns0:ManageNameIDService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://lingon.ladok.umu.se:8087/mni" /><ns0:ManageNameIDService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://lingon.ladok.umu.se:8087/mni" /><ns0:ManageNameIDService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://lingon.ladok.umu.se:8087/mni" /><ns0:ManageNameIDService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://lingon.ladok.umu.se:8087/acs/artifact" /><ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://lingon.ladok.umu.se:8087/acs/post" index="1" /><ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://lingon.ladok.umu.se:8087/acs/redirect" index="2" /><ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://lingon.ladok.umu.se:8087/acs/artifact" index="3" /><ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://lingon.ladok.umu.se:8087/ecp" index="4" /></ns0:SPSSODescriptor><ns0:Organization><ns0:OrganizationName xml:lang="se">AB Exempel</ns0:OrganizationName><ns0:OrganizationDisplayName xml:lang="se">AB Exempel</ns0:OrganizationDisplayName><ns0:OrganizationURL xml:lang="en">http://www.example.org</ns0:OrganizationURL></ns0:Organization><ns0:ContactPerson contactType="technical"><ns0:GivenName>Roland</ns0:GivenName><ns0:SurName>Hedberg</ns0:SurName><ns0:EmailAddress>tech@eample.com</ns0:EmailAddress><ns0:EmailAddress>tech@example.org</ns0:EmailAddress><ns0:TelephoneNumber>+46 70 100 0000</ns0:TelephoneNumber></ns0:ContactPerson></ns0:EntityDescriptor> |