summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoland Hedberg <roland.hedberg@adm.umu.se>2013-03-29 10:37:42 +0100
committerRoland Hedberg <roland.hedberg@adm.umu.se>2013-03-29 10:37:42 +0100
commitb1121217f3d8afc4abe0c98554b72eca354d1b90 (patch)
treed6b1ccdcb785152f9fd8f664d08eac9c1d2c0645
parent1e161d0d1cc37ff90feb028d4365f48bb69e6ed6 (diff)
downloadpysaml2-b1121217f3d8afc4abe0c98554b72eca354d1b90.tar.gz
Worked on the SP test part.
-rwxr-xr-xscript/saml2i.py9
-rw-r--r--setup.py12
-rw-r--r--src/sp_test/__init__.py158
-rw-r--r--src/sp_test/base.py348
-rw-r--r--src/sp_test/check.py54
-rw-r--r--src/sp_test/tests.py136
-rw-r--r--src/srtest/__init__.py4
-rwxr-xr-xtests/localhost.py18
-rw-r--r--tests/sp.xml116
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
diff --git a/setup.py b/setup.py
index d1f26c0c..818b02d3 100644
--- a/setup.py
+++ b/setup.py
@@ -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>