summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoland Hedberg <roland@catalogix.se>2017-10-11 08:32:10 +0200
committerGitHub <noreply@github.com>2017-10-11 08:32:10 +0200
commitb71f1443991c94099ff49226c92b8671d657fade (patch)
tree69cb8fb94681a3273d1fe995e0e79bb047d307cf
parent546f9d4ca82c8b4ae5cf7aa0fb80f0bedf385c89 (diff)
parent9e6ddc0030828a17fe44a3ecdc5c95b6b6c8d741 (diff)
downloadpysaml2-b71f1443991c94099ff49226c92b8671d657fade.tar.gz
Merge branch 'master' into master
-rw-r--r--README.rst12
-rw-r--r--src/saml2/algsupport.py2
-rw-r--r--src/saml2/assertion.py16
-rw-r--r--src/saml2/attribute_converter.py2
-rw-r--r--src/saml2/attributemaps/saml_uri.py17
-rw-r--r--src/saml2/client_base.py136
-rw-r--r--src/saml2/config.py10
-rw-r--r--src/saml2/ecp.py50
-rw-r--r--src/saml2/ecp_client.py29
-rw-r--r--src/saml2/entity.py7
-rw-r--r--src/saml2/extension/requested_attributes.py131
-rw-r--r--src/saml2/extension/sp_type.py54
-rw-r--r--src/saml2/mdstore.py4
-rw-r--r--src/saml2/metadata.py12
-rw-r--r--src/saml2/pack.py49
-rw-r--r--src/saml2/profile/samlec.py14
-rw-r--r--src/saml2/response.py5
-rw-r--r--src/saml2/saml.py8
-rw-r--r--src/saml2/validate.py14
-rw-r--r--src/saml2/xmldsig/__init__.py2
-rw-r--r--tests/SWITCHaaiRootCA.crt.pem22
-rw-r--r--tests/conftest.py11
-rw-r--r--tests/server_conf.py13
-rw-r--r--tests/sp_conf_nameidpolicy.py64
-rw-r--r--tests/sp_mdext_conf.py2
-rw-r--r--tests/test_19_attribute_converter.py95
-rw-r--r--tests/test_20_assertion.py37
-rw-r--r--tests/test_30_mdstore.py11
-rw-r--r--tests/test_31_config.py11
-rw-r--r--tests/test_50_server.py26
-rw-r--r--tests/test_51_client.py81
-rw-r--r--tests/test_65_authn_query.py2
-rw-r--r--tests/test_68_assertion_id.py2
-rw-r--r--tests/test_83_md_extensions.py12
-rw-r--r--tests/test_requirements.txt1
-rw-r--r--tools/data/requested_attributes.xsd28
-rw-r--r--tools/data/sp_type.xsd16
-rw-r--r--tox.ini2
38 files changed, 830 insertions, 180 deletions
diff --git a/README.rst b/README.rst
index 6aa3e23f..638b9133 100644
--- a/README.rst
+++ b/README.rst
@@ -38,3 +38,15 @@ testing. To run the tests on your system's version of python
To run tests in multiple python environments, you can use
`pyenv <https://github.com/yyuu/pyenv>`_ with `tox <https://tox.readthedocs.io/en/latest/>`_.
+
+
+Please contribute!
+==================
+
+To help out, you could:
+
+1. Test and report any bugs or other difficulties.
+2. Implement missing features.
+3. Write more unit tests.
+
+**If you have the time and inclination I'm looking for Collaborators**
diff --git a/src/saml2/algsupport.py b/src/saml2/algsupport.py
index f9bc06b8..72036b40 100644
--- a/src/saml2/algsupport.py
+++ b/src/saml2/algsupport.py
@@ -23,7 +23,7 @@ SIGNING_METHODS = {
"rsa-sha256": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
"rsa-sha384": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384',
"rsa-sha512": 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512',
- "dsa-sha1": 'http,//www.w3.org/2000/09/xmldsig#dsa-sha1',
+ "dsa-sha1": 'http://www.w3.org/2000/09/xmldsig#dsa-sha1',
'dsa-sha256': 'http://www.w3.org/2009/xmldsig11#dsa-sha256',
'ecdsa_sha1': 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha1',
'ecdsa_sha224': 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha224',
diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py
index 64944d11..0db4b723 100644
--- a/src/saml2/assertion.py
+++ b/src/saml2/assertion.py
@@ -78,19 +78,22 @@ def filter_on_attributes(ava, required=None, optional=None, acs=None,
"""
def _match_attr_name(attr, ava):
- try:
- friendly_name = attr["friendly_name"]
- except KeyError:
- friendly_name = get_local_name(acs, attr["name"],
- attr["name_format"])
+
+ local_name = get_local_name(acs, attr["name"], attr["name_format"])
+ if not local_name:
+ try:
+ local_name = attr["friendly_name"]
+ except KeyError:
+ pass
- _fn = _match(friendly_name, ava)
+ _fn = _match(local_name, ava)
if not _fn: # In the unlikely case that someone has provided us with
# URIs as attribute names
_fn = _match(attr["name"], ava)
return _fn
+
def _apply_attr_value_restrictions(attr, res, must=False):
try:
values = [av["text"] for av in attr["attribute_value"]]
@@ -105,7 +108,6 @@ def filter_on_attributes(ava, required=None, optional=None, acs=None,
return _filter_values(ava[_fn], values, must)
res = {}
-
if required is None:
required = []
diff --git a/src/saml2/attribute_converter.py b/src/saml2/attribute_converter.py
index 3d52a816..3d32d226 100644
--- a/src/saml2/attribute_converter.py
+++ b/src/saml2/attribute_converter.py
@@ -246,7 +246,7 @@ def get_local_name(acs, attr, name_format):
for aconv in acs:
#print(ac.format, name_format)
if aconv.name_format == name_format:
- return aconv._fro[attr]
+ return aconv._fro.get(attr)
def d_to_local_name(acs, attr):
diff --git a/src/saml2/attributemaps/saml_uri.py b/src/saml2/attributemaps/saml_uri.py
index ca6dfd84..e97090ff 100644
--- a/src/saml2/attributemaps/saml_uri.py
+++ b/src/saml2/attributemaps/saml_uri.py
@@ -13,10 +13,19 @@ SCHAC = 'urn:oid:1.3.6.1.4.1.25178.1.2.'
SIS = 'urn:oid:1.2.752.194.10.2.'
UMICH = 'urn:oid:1.3.6.1.4.1.250.1.57.'
OPENOSI_OID = 'urn:oid:1.3.6.1.4.1.27630.2.1.1.' #openosi-0.82.schema http://www.openosi.org/osi/display/ldap/Home
+EIDAS_NATURALPERSON = 'http://eidas.europa.eu/attributes/naturalperson/'
MAP = {
'identifier': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
'fro': {
+ EIDAS_NATURALPERSON+'PersonIdentifier': 'PersonIdentifier',
+ EIDAS_NATURALPERSON+'FamilyName': 'FamilyName',
+ EIDAS_NATURALPERSON+'FirstName': 'FirstName',
+ EIDAS_NATURALPERSON+'DateOfBirth': 'DateOfBirth',
+ EIDAS_NATURALPERSON+'BirthName': 'BirthName',
+ EIDAS_NATURALPERSON+'PlaceOfBirth': 'PlaceOfBirth',
+ EIDAS_NATURALPERSON+'CurrentAddress': 'CurrentAddress',
+ EIDAS_NATURALPERSON+'Gender': 'Gender',
EDUCOURSE_OID+'1': 'eduCourseOffering',
EDUCOURSE_OID+'2': 'eduCourseMember',
EDUMEMBER1_OID+'1': 'isMemberOf',
@@ -161,6 +170,14 @@ MAP = {
X500ATTR_OID+'65': 'pseudonym',
},
'to': {
+ 'PersonIdentifier': EIDAS_NATURALPERSON+'PersonIdentifier',
+ 'FamilyName': EIDAS_NATURALPERSON+'FamilyName',
+ 'FirstName': EIDAS_NATURALPERSON+'FirstName',
+ 'DateOfBirth': EIDAS_NATURALPERSON+'DateOfBirth',
+ 'BirthName': EIDAS_NATURALPERSON+'BirthName',
+ 'PlaceOfBirth': EIDAS_NATURALPERSON+'PlaceOfBirth',
+ 'CurrentAddress': EIDAS_NATURALPERSON+'CurrentAddress',
+ 'Gender': EIDAS_NATURALPERSON+'Gender',
'associatedDomain': UCL_DIR_PILOT+'37',
'authorityRevocationList': X500ATTR_OID+'38',
'businessCategory': X500ATTR_OID+'15',
diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py
index 55e5b1fc..531ddea5 100644
--- a/src/saml2/client_base.py
+++ b/src/saml2/client_base.py
@@ -10,6 +10,8 @@ import six
from saml2.entity import Entity
+import saml2.attributemaps as attributemaps
+
from saml2.mdstore import destinations
from saml2.profile import paos, ecp
from saml2.saml import NAMEID_FORMAT_TRANSIENT
@@ -18,6 +20,9 @@ from saml2.samlp import NameIDMappingRequest
from saml2.samlp import AttributeQuery
from saml2.samlp import AuthzDecisionQuery
from saml2.samlp import AuthnRequest
+from saml2.samlp import Extensions
+from saml2.extension import sp_type
+from saml2.extension import requested_attributes
import saml2
import time
@@ -207,7 +212,7 @@ class Base(Entity):
nameid_format=None,
service_url_binding=None, message_id=0,
consent=None, extensions=None, sign=None,
- allow_create=False, sign_prepare=False, sign_alg=None,
+ allow_create=None, sign_prepare=False, sign_alg=None,
digest_alg=None, **kwargs):
""" Creates an authentication request.
@@ -235,26 +240,30 @@ class Base(Entity):
args = {}
- try:
- args["assertion_consumer_service_url"] = kwargs[
- "assertion_consumer_service_urls"][0]
- del kwargs["assertion_consumer_service_urls"]
- except KeyError:
+ if self.config.getattr('hide_assertion_consumer_service', 'sp'):
+ args["assertion_consumer_service_url"] = None
+ binding = None
+ else:
try:
args["assertion_consumer_service_url"] = kwargs[
- "assertion_consumer_service_url"]
- del kwargs["assertion_consumer_service_url"]
+ "assertion_consumer_service_urls"][0]
+ del kwargs["assertion_consumer_service_urls"]
except KeyError:
try:
- args["assertion_consumer_service_index"] = str(kwargs[
- "assertion_consumer_service_index"])
- del kwargs["assertion_consumer_service_index"]
+ args["assertion_consumer_service_url"] = kwargs[
+ "assertion_consumer_service_url"]
+ del kwargs["assertion_consumer_service_url"]
except KeyError:
- if service_url_binding is None:
- service_urls = self.service_urls(binding)
- else:
- service_urls = self.service_urls(service_url_binding)
- args["assertion_consumer_service_url"] = service_urls[0]
+ try:
+ args["assertion_consumer_service_index"] = str(
+ kwargs["assertion_consumer_service_index"])
+ del kwargs["assertion_consumer_service_index"]
+ except KeyError:
+ if service_url_binding is None:
+ service_urls = self.service_urls(binding)
+ else:
+ service_urls = self.service_urls(service_url_binding)
+ args["assertion_consumer_service_url"] = service_urls[0]
try:
args["provider_name"] = kwargs["provider_name"]
@@ -268,7 +277,7 @@ class Base(Entity):
# all of these have cardinality 0..1
_msg = AuthnRequest()
for param in ["scoping", "requested_authn_context", "conditions",
- "subject", "scoping"]:
+ "subject"]:
try:
_item = kwargs[param]
except KeyError:
@@ -288,10 +297,15 @@ class Base(Entity):
args["name_id_policy"] = kwargs["name_id_policy"]
del kwargs["name_id_policy"]
except KeyError:
- if allow_create:
- allow_create = "true"
- else:
- allow_create = "false"
+ if allow_create is None:
+ allow_create = self.config.getattr("name_id_format_allow_create", "sp")
+ if allow_create is None:
+ allow_create = "false"
+ else:
+ if allow_create is True:
+ allow_create = "true"
+ else:
+ allow_create = "false"
if nameid_format == "":
name_id_policy = None
@@ -299,12 +313,21 @@ class Base(Entity):
if nameid_format is None:
nameid_format = self.config.getattr("name_id_format", "sp")
+ # If no nameid_format has been set in the configuration
+ # or passed in then transient is the default.
if nameid_format is None:
nameid_format = NAMEID_FORMAT_TRANSIENT
+
+ # If a list has been configured or passed in choose the
+ # first since NameIDPolicy can only have one format specified.
elif isinstance(nameid_format, list):
- # NameIDPolicy can only have one format specified
nameid_format = nameid_format[0]
+ # Allow a deployer to signal that no format should be specified
+ # in the NameIDPolicy by passing in or configuring the string 'None'.
+ elif nameid_format == 'None':
+ nameid_format = None
+
name_id_policy = samlp.NameIDPolicy(allow_create=allow_create,
format=nameid_format)
@@ -321,6 +344,75 @@ class Base(Entity):
except KeyError:
nsprefix = None
+ try:
+ force_authn = kwargs['force_authn']
+ except KeyError:
+ force_authn = self.config.getattr('force_authn', 'sp')
+ finally:
+ if force_authn:
+ args['force_authn'] = 'true'
+
+ conf_sp_type = self.config.getattr('sp_type', 'sp')
+ conf_sp_type_in_md = self.config.getattr('sp_type_in_metadata', 'sp')
+ if conf_sp_type and conf_sp_type_in_md is False:
+ if not extensions:
+ extensions = Extensions()
+ item = sp_type.SPType(text=conf_sp_type)
+ extensions.add_extension_element(item)
+
+ requested_attrs = self.config.getattr('requested_attributes', 'sp')
+ if requested_attrs:
+ if not extensions:
+ extensions = Extensions()
+
+ attributemapsmods = []
+ for modname in attributemaps.__all__:
+ attributemapsmods.append(getattr(attributemaps, modname))
+
+ items = []
+ for attr in requested_attrs:
+ friendly_name = attr.get('friendly_name')
+ name = attr.get('name')
+ name_format = attr.get('name_format')
+ is_required = str(attr.get('required', False)).lower()
+
+ if not name and not friendly_name:
+ raise ValueError(
+ "Missing required attribute: '{}' or '{}'".format(
+ 'name', 'friendly_name'))
+
+ if not name:
+ for mod in attributemapsmods:
+ try:
+ name = mod.MAP['to'][friendly_name]
+ except KeyError:
+ continue
+ else:
+ if not name_format:
+ name_format = mod.MAP['identifier']
+ break
+
+ if not friendly_name:
+ for mod in attributemapsmods:
+ try:
+ friendly_name = mod.MAP['fro'][name]
+ except KeyError:
+ continue
+ else:
+ if not name_format:
+ name_format = mod.MAP['identifier']
+ break
+
+ items.append(requested_attributes.RequestedAttribute(
+ is_required=is_required,
+ name_format=name_format,
+ friendly_name=friendly_name,
+ name=name))
+
+ item = requested_attributes.RequestedAttributes(
+ extension_elements=items)
+ extensions.add_extension_element(item)
+
if kwargs:
_args, extensions = self._filter_args(AuthnRequest(), extensions,
**kwargs)
diff --git a/src/saml2/config.py b/src/saml2/config.py
index 9fc3e708..296f0e85 100644
--- a/src/saml2/config.py
+++ b/src/saml2/config.py
@@ -73,8 +73,14 @@ SP_ARGS = [
"allow_unsolicited",
"ecp",
"name_id_format",
+ "name_id_format_allow_create",
"logout_requests_signed",
- "requested_attribute_name_format"
+ "requested_attribute_name_format",
+ "hide_assertion_consumer_service",
+ "force_authn",
+ "sp_type",
+ "sp_type_in_metadata",
+ "requested_attributes",
]
AA_IDP_ARGS = [
@@ -187,6 +193,7 @@ class Config(object):
self.contact_person = None
self.name_form = None
self.name_id_format = None
+ self.name_id_format_allow_create = None
self.virtual_organization = None
self.logger = None
self.only_use_keys_in_metadata = True
@@ -205,7 +212,6 @@ class Config(object):
self.crypto_backend = 'xmlsec1'
self.scope = ""
self.allow_unknown_attributes = False
- self.allow_unsolicited = False
self.extension_schema = {}
self.cert_handler_extra_class = None
self.verify_encrypt_cert_advice = None
diff --git a/src/saml2/ecp.py b/src/saml2/ecp.py
index f15a259c..5817cda4 100644
--- a/src/saml2/ecp.py
+++ b/src/saml2/ecp.py
@@ -24,6 +24,8 @@ from saml2.schema import soapenv
from saml2.response import authn_response
+from saml2 import saml
+
logger = logging.getLogger(__name__)
@@ -53,7 +55,7 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False):
# ----------------------------------------
# <paos:Request>
# ----------------------------------------
- my_url = cls.service_url(BINDING_PAOS)
+ my_url = cls.service_urls(BINDING_PAOS)[0]
# must_understand and actor according to the standard
#
@@ -64,6 +66,19 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False):
eelist.append(element_to_extension_element(paos_request))
# ----------------------------------------
+ # <samlp:AuthnRequest>
+ # ----------------------------------------
+
+ logger.info("entityid: %s, binding: %s" % (entityid, BINDING_SOAP))
+
+ location = cls._sso_location(entityid, binding=BINDING_SOAP)
+ req_id, authn_req = cls.create_authn_request(
+ location, binding=BINDING_PAOS, service_url_binding=BINDING_PAOS)
+
+ body = soapenv.Body()
+ body.extension_elements = [element_to_extension_element(authn_req)]
+
+ # ----------------------------------------
# <ecp:Request>
# ----------------------------------------
@@ -74,14 +89,16 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False):
# )
#
# idp_list = samlp.IDPList(idp_entry= [idp])
-#
-# ecp_request = ecp.Request(
-# actor = ACTOR, must_understand = "1",
-# provider_name = "Example Service Provider",
-# issuer=saml.Issuer(text="https://sp.example.org/entity"),
-# idp_list = idp_list)
-#
-# eelist.append(element_to_extension_element(ecp_request))
+
+ idp_list = None
+ ecp_request = ecp.Request(
+ actor=ACTOR,
+ must_understand="1",
+ provider_name=None,
+ issuer=saml.Issuer(text=authn_req.issuer.text),
+ idp_list=idp_list)
+
+ eelist.append(element_to_extension_element(ecp_request))
# ----------------------------------------
# <ecp:RelayState>
@@ -96,19 +113,6 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False):
header.extension_elements = eelist
# ----------------------------------------
- # <samlp:AuthnRequest>
- # ----------------------------------------
-
- logger.info("entityid: %s, binding: %s" % (entityid, BINDING_SOAP))
-
- location = cls._sso_location(entityid, binding=BINDING_SOAP)
- req_id, authn_req = cls.create_authn_request(
- location, binding=BINDING_PAOS, service_url_binding=BINDING_PAOS)
-
- body = soapenv.Body()
- body.extension_elements = [element_to_extension_element(authn_req)]
-
- # ----------------------------------------
# The SOAP envelope
# ----------------------------------------
@@ -126,7 +130,7 @@ def handle_ecp_authn_response(cls, soap_message, outstanding=None):
if item.c_tag == "RelayState" and item.c_namespace == ecp.NAMESPACE:
_relay_state = item
- response = authn_response(cls.config, cls.service_url(), outstanding,
+ response = authn_response(cls.config, cls.service_urls(), outstanding,
allow_unsolicited=True)
response.loads("%s" % rdict["body"], False, soap_message)
diff --git a/src/saml2/ecp_client.py b/src/saml2/ecp_client.py
index 90c3b44c..788d252d 100644
--- a/src/saml2/ecp_client.py
+++ b/src/saml2/ecp_client.py
@@ -119,7 +119,7 @@ class Client(Entity):
if response.status_code != 200:
raise SAMLError(
"Request to IdP failed (%s): %s" % (response.status_code,
- response.error))
+ response.text))
# SAMLP response in a SOAP envelope body, ecp response in headers
respdict = self.parse_soap_message(response.text)
@@ -200,22 +200,19 @@ class Client(Entity):
ht_args = self.use_soap(idp_response, args["rc_url"],
[args["relay_state"]])
-
+ ht_args["headers"][0] = ('Content-Type', MIME_PAOS)
logger.debug("[P3] Post to SP: %s", ht_args["data"])
- ht_args["headers"].append(('Content-Type', 'application/vnd.paos+xml'))
-
# POST the package from the IdP to the SP
- response = self.send(args["rc_url"], "POST", **ht_args)
+ response = self.send(**ht_args)
if response.status_code == 302:
# ignore where the SP is redirecting us to and go for the
# url I started off with.
pass
else:
- print(response.error)
raise SAMLError(
- "Error POSTing package to SP: %s" % response.error)
+ "Error POSTing package to SP: %s" % response.text)
logger.debug("[P3] SP response: %s", response.text)
@@ -255,8 +252,7 @@ class Client(Entity):
:param opargs: Arguments to the HTTP call
:return: The page
"""
- if url not in opargs:
- url = self._sp
+ sp_url = self._sp
# ********************************************
# Phase 1 - First conversation with the SP
@@ -264,13 +260,13 @@ class Client(Entity):
# headers needed to indicate to the SP that I'm ECP enabled
opargs["headers"] = self.add_paos_headers(opargs["headers"])
-
- response = self.send(url, op, **opargs)
- logger.debug("[Op] SP response: %s", response)
+ response = self.send(sp_url, op, **opargs)
+ logger.debug("[Op] SP response: %s" % response)
+ print(response.text)
if response.status_code != 200:
raise SAMLError(
- "Request to SP failed: %s" % response.error)
+ "Request to SP failed: %s" % response.text)
# The response might be a AuthnRequest instance in a SOAP envelope
# body. If so it's the start of the ECP conversation
@@ -282,7 +278,6 @@ class Client(Entity):
# header blocks may also be present
try:
respdict = self.parse_soap_message(response.text)
-
self.ecp_conversation(respdict, idp_entity_id)
# should by now be authenticated so this should go smoothly
@@ -290,11 +285,9 @@ class Client(Entity):
except (soap.XmlParseError, AssertionError, KeyError):
pass
- #print("RESP",response, self.http.response)
-
- if response.status_code != 404:
+ if response.status_code >= 400:
raise SAMLError("Error performing operation: %s" % (
- response.error,))
+ response.text,))
return response
diff --git a/src/saml2/entity.py b/src/saml2/entity.py
index b24c6210..27b30fe9 100644
--- a/src/saml2/entity.py
+++ b/src/saml2/entity.py
@@ -8,7 +8,7 @@ from binascii import hexlify
from hashlib import sha1
from saml2.metadata import ENDPOINTS
-from saml2.profile import paos, ecp
+from saml2.profile import paos, ecp, samlec
from saml2.soap import parse_soap_enveloped_saml_artifact_resolve
from saml2.soap import class_instances_from_soap_enveloped_saml_thingies
from saml2.soap import open_soap_envelope
@@ -224,7 +224,7 @@ class Entity(HTTPBase):
info["method"] = "POST"
elif binding == BINDING_HTTP_REDIRECT:
logger.info("HTTP REDIRECT")
- if 'sigalg' in kwargs:
+ if kwargs.get('sigalg', ''):
signer = self.sec.sec_backend.get_signer(kwargs['sigalg'])
else:
signer = None
@@ -407,7 +407,8 @@ class Entity(HTTPBase):
"""
return class_instances_from_soap_enveloped_saml_thingies(text, [paos,
ecp,
- samlp])
+ samlp,
+ samlec])
@staticmethod
def unpack_soap_message(text):
diff --git a/src/saml2/extension/requested_attributes.py b/src/saml2/extension/requested_attributes.py
new file mode 100644
index 00000000..3d574f15
--- /dev/null
+++ b/src/saml2/extension/requested_attributes.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python
+
+#
+# Generated Tue Jul 18 14:58:29 2017 by parse_xsd.py version 0.5.
+#
+
+import saml2
+from saml2 import SamlBase
+
+from saml2 import saml
+
+
+NAMESPACE = 'http://eidas.europa.eu/saml-extensions'
+
+class RequestedAttributeType_(SamlBase):
+ """The http://eidas.europa.eu/saml-extensions:RequestedAttributeType element """
+
+ c_tag = 'RequestedAttributeType'
+ c_namespace = NAMESPACE
+ c_children = SamlBase.c_children.copy()
+ c_attributes = SamlBase.c_attributes.copy()
+ c_child_order = SamlBase.c_child_order[:]
+ c_cardinality = SamlBase.c_cardinality.copy()
+ c_children['{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue'] = ('attribute_value', [saml.AttributeValue])
+ c_cardinality['attribute_value'] = {"min":0}
+ c_attributes['Name'] = ('name', 'None', True)
+ c_attributes['NameFormat'] = ('name_format', 'None', True)
+ c_attributes['FriendlyName'] = ('friendly_name', 'None', False)
+ c_attributes['isRequired'] = ('is_required', 'None', False)
+ c_child_order.extend(['attribute_value'])
+
+ def __init__(self,
+ attribute_value=None,
+ name=None,
+ name_format=None,
+ friendly_name=None,
+ is_required=None,
+ text=None,
+ extension_elements=None,
+ extension_attributes=None,
+ ):
+ SamlBase.__init__(self,
+ text=text,
+ extension_elements=extension_elements,
+ extension_attributes=extension_attributes,
+ )
+ self.attribute_value=attribute_value or []
+ self.name=name
+ self.name_format=name_format
+ self.friendly_name=friendly_name
+ self.is_required=is_required
+
+def requested_attribute_type__from_string(xml_string):
+ return saml2.create_class_from_xml_string(RequestedAttributeType_, xml_string)
+
+
+class RequestedAttribute(RequestedAttributeType_):
+ """The http://eidas.europa.eu/saml-extensions:RequestedAttribute element """
+
+ c_tag = 'RequestedAttribute'
+ c_namespace = NAMESPACE
+ c_children = RequestedAttributeType_.c_children.copy()
+ c_attributes = RequestedAttributeType_.c_attributes.copy()
+ c_child_order = RequestedAttributeType_.c_child_order[:]
+ c_cardinality = RequestedAttributeType_.c_cardinality.copy()
+
+def requested_attribute_from_string(xml_string):
+ return saml2.create_class_from_xml_string(RequestedAttribute, xml_string)
+
+
+class RequestedAttributesType_(SamlBase):
+ """The http://eidas.europa.eu/saml-extensions:RequestedAttributesType element """
+
+ c_tag = 'RequestedAttributesType'
+ c_namespace = NAMESPACE
+ c_children = SamlBase.c_children.copy()
+ c_attributes = SamlBase.c_attributes.copy()
+ c_child_order = SamlBase.c_child_order[:]
+ c_cardinality = SamlBase.c_cardinality.copy()
+ c_children['{http://eidas.europa.eu/saml-extensions}RequestedAttribute'] = ('requested_attribute', [RequestedAttribute])
+ c_cardinality['requested_attribute'] = {"min":0}
+ c_child_order.extend(['requested_attribute'])
+
+ def __init__(self,
+ requested_attribute=None,
+ text=None,
+ extension_elements=None,
+ extension_attributes=None,
+ ):
+ SamlBase.__init__(self,
+ text=text,
+ extension_elements=extension_elements,
+ extension_attributes=extension_attributes,
+ )
+ self.requested_attribute=requested_attribute or []
+
+def requested_attributes_type__from_string(xml_string):
+ return saml2.create_class_from_xml_string(RequestedAttributesType_, xml_string)
+
+
+class RequestedAttributes(RequestedAttributesType_):
+ """The http://eidas.europa.eu/saml-extensions:RequestedAttributes element """
+
+ c_tag = 'RequestedAttributes'
+ c_namespace = NAMESPACE
+ c_children = RequestedAttributesType_.c_children.copy()
+ c_attributes = RequestedAttributesType_.c_attributes.copy()
+ c_child_order = RequestedAttributesType_.c_child_order[:]
+ c_cardinality = RequestedAttributesType_.c_cardinality.copy()
+
+def requested_attributes_from_string(xml_string):
+ return saml2.create_class_from_xml_string(RequestedAttributes, xml_string)
+
+
+ELEMENT_FROM_STRING = {
+ RequestedAttributes.c_tag: requested_attributes_from_string,
+ RequestedAttributesType_.c_tag: requested_attributes_type__from_string,
+ RequestedAttribute.c_tag: requested_attribute_from_string,
+ RequestedAttributeType_.c_tag: requested_attribute_type__from_string,
+}
+
+ELEMENT_BY_TAG = {
+ 'RequestedAttributes': RequestedAttributes,
+ 'RequestedAttributesType': RequestedAttributesType_,
+ 'RequestedAttribute': RequestedAttribute,
+ 'RequestedAttributeType': RequestedAttributeType_,
+}
+
+
+def factory(tag, **kwargs):
+ return ELEMENT_BY_TAG[tag](**kwargs)
diff --git a/src/saml2/extension/sp_type.py b/src/saml2/extension/sp_type.py
new file mode 100644
index 00000000..8ffb2cea
--- /dev/null
+++ b/src/saml2/extension/sp_type.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+
+#
+# Generated Tue Jul 18 15:03:44 2017 by parse_xsd.py version 0.5.
+#
+
+import saml2
+from saml2 import SamlBase
+
+
+NAMESPACE = 'http://eidas.europa.eu/saml-extensions'
+
+class SPTypeType_(SamlBase):
+ """The http://eidas.europa.eu/saml-extensions:SPTypeType element """
+
+ c_tag = 'SPTypeType'
+ c_namespace = NAMESPACE
+ c_value_type = {'base': 'xsd:string', 'enumeration': ['public', 'private']}
+ c_children = SamlBase.c_children.copy()
+ c_attributes = SamlBase.c_attributes.copy()
+ c_child_order = SamlBase.c_child_order[:]
+ c_cardinality = SamlBase.c_cardinality.copy()
+
+def sp_type_type__from_string(xml_string):
+ return saml2.create_class_from_xml_string(SPTypeType_, xml_string)
+
+
+class SPType(SPTypeType_):
+ """The http://eidas.europa.eu/saml-extensions:SPType element """
+
+ c_tag = 'SPType'
+ c_namespace = NAMESPACE
+ c_children = SPTypeType_.c_children.copy()
+ c_attributes = SPTypeType_.c_attributes.copy()
+ c_child_order = SPTypeType_.c_child_order[:]
+ c_cardinality = SPTypeType_.c_cardinality.copy()
+
+def sp_type_from_string(xml_string):
+ return saml2.create_class_from_xml_string(SPType, xml_string)
+
+
+ELEMENT_FROM_STRING = {
+ SPType.c_tag: sp_type_from_string,
+ SPTypeType_.c_tag: sp_type_type__from_string,
+}
+
+ELEMENT_BY_TAG = {
+ 'SPType': SPType,
+ 'SPTypeType': SPTypeType_,
+}
+
+
+def factory(tag, **kwargs):
+ return ELEMENT_BY_TAG[tag](**kwargs)
diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py
index eff75c8b..72825ea8 100644
--- a/src/saml2/mdstore.py
+++ b/src/saml2/mdstore.py
@@ -750,7 +750,7 @@ class MetaDataExtern(InMemoryMetaData):
"""
response = self.http.send(self.url)
if response.status_code == 200:
- _txt = response.text.encode("utf-8")
+ _txt = response.content
return self.parse_and_check_signature(_txt)
else:
logger.info("Response status: %s", response.status_code)
@@ -814,7 +814,7 @@ class MetaDataMDX(InMemoryMetaData):
response = requests.get(mdx_url, headers={
'Accept': SAML_METADATA_CONTENT_TYPE})
if response.status_code == 200:
- _txt = response.text.encode("utf-8")
+ _txt = response.content
if self.parse_and_check_signature(_txt):
return self.entity[item]
diff --git a/src/saml2/metadata.py b/src/saml2/metadata.py
index 50ec0bae..de2e6e75 100644
--- a/src/saml2/metadata.py
+++ b/src/saml2/metadata.py
@@ -9,6 +9,7 @@ from saml2.extension import mdui
from saml2.extension import idpdisc
from saml2.extension import shibmd
from saml2.extension import mdattr
+from saml2.extension import sp_type
from saml2.saml import NAME_FORMAT_URI
from saml2.saml import AttributeValue
from saml2.saml import Attribute
@@ -722,7 +723,8 @@ def entity_descriptor(confd):
entd.contact_person = do_contact_person_info(confd.contact_person)
if confd.entity_category:
- entd.extensions = md.Extensions()
+ if not entd.extensions:
+ entd.extensions = md.Extensions()
ava = [AttributeValue(text=c) for c in confd.entity_category]
attr = Attribute(attribute_value=ava,
name="http://macedir.org/entity-category")
@@ -734,6 +736,14 @@ def entity_descriptor(confd):
entd.extensions = md.Extensions()
entd.extensions.add_extension_element(item)
+ conf_sp_type = confd.getattr('sp_type', 'sp')
+ conf_sp_type_in_md = confd.getattr('sp_type_in_metadata', 'sp')
+ if conf_sp_type and conf_sp_type_in_md is True:
+ if not entd.extensions:
+ entd.extensions = md.Extensions()
+ item = sp_type.SPType(text=conf_sp_type)
+ entd.extensions.add_extension_element(item)
+
serves = confd.serves
if not serves:
raise SAMLError(
diff --git a/src/saml2/pack.py b/src/saml2/pack.py
index 728a516f..3bf39fc8 100644
--- a/src/saml2/pack.py
+++ b/src/saml2/pack.py
@@ -40,12 +40,35 @@ except ImportError:
import defusedxml.ElementTree
NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/"
-FORM_SPEC = """<form method="post" action="%s">
- <input type="hidden" name="%s" value="%s" />
- <input type="hidden" name="RelayState" value="%s" />
- <input type="submit" value="Submit" />
-</form>"""
+FORM_SPEC = """\
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ </head>
+ <body onload="document.forms[0].submit()">
+ <noscript>
+ <p>
+ <strong>Note:</strong> Since your browser does not support JavaScript,
+ you must press the Continue button once to proceed.
+ </p>
+ </noscript>
+
+ <form action="{action}" method="post">
+ <div>
+ <input type="hidden" name="RelayState" value="{relay_state}"/>
+
+ <input type="hidden" name="{saml_type}" value="{saml_response}"/>
+ </div>
+ <noscript>
+ <div>
+ <input type="submit" value="Continue"/>
+ </div>
+ </noscript>
+ </form>
+ </body>
+</html>"""
def http_form_post_message(message, location, relay_state="",
typ="SAMLRequest", **kwargs):
@@ -58,8 +81,6 @@ def http_form_post_message(message, location, relay_state="",
:param relay_state: for preserving and conveying state information
:return: A tuple containing header information and a HTML message.
"""
- response = ["<head>", """<title>SAML 2.0 POST</title>""", "</head><body>"]
-
if not isinstance(message, six.string_types):
message = str(message)
if not isinstance(message, six.binary_type):
@@ -71,17 +92,17 @@ def http_form_post_message(message, location, relay_state="",
_msg = message
_msg = _msg.decode('ascii')
- response.append(FORM_SPEC % (location, typ, _msg, relay_state))
+ args = {
+ 'action' : location,
+ 'saml_type' : typ,
+ 'relay_state' : relay_state,
+ 'saml_response' : _msg
+ }
- response.append("""<script type="text/javascript">""")
- response.append(" window.onload = function ()")
- response.append(" { document.forms[0].submit(); }")
- response.append("""</script>""")
- response.append("</body>")
+ response = FORM_SPEC.format(**args)
return {"headers": [("Content-type", "text/html")], "data": response}
-
def http_post_message(message, relay_state="", typ="SAMLRequest", **kwargs):
"""
diff --git a/src/saml2/profile/samlec.py b/src/saml2/profile/samlec.py
new file mode 100644
index 00000000..b90f6d3d
--- /dev/null
+++ b/src/saml2/profile/samlec.py
@@ -0,0 +1,14 @@
+from saml2 import SamlBase
+
+
+NAMESPACE = 'urn:ietf:params:xml:ns:samlec'
+
+
+class GeneratedKey(SamlBase):
+ c_tag = 'GeneratedKey'
+ c_namespace = NAMESPACE
+
+
+ELEMENT_BY_TAG = {
+ 'GeneratedKey': GeneratedKey,
+}
diff --git a/src/saml2/response.py b/src/saml2/response.py
index 13323509..6de8723b 100644
--- a/src/saml2/response.py
+++ b/src/saml2/response.py
@@ -666,7 +666,7 @@ class AuthnResponse(StatusResponse):
_attr_statem = _assertion.attribute_statement[0]
ava.update(self.read_attribute_statement(_attr_statem))
if not ava:
- logger.error("Missing Attribute Statement")
+ logger.debug("Assertion contains no attribute statements")
return ava
def _bearer_confirmed(self, data):
@@ -910,7 +910,8 @@ class AuthnResponse(StatusResponse):
else: # This is a saml2int limitation
try:
assert len(self.response.assertion) == 1 or \
- len(self.response.encrypted_assertion) == 1
+ len(self.response.encrypted_assertion) == 1 or \
+ self.assertion is not None
except AssertionError:
raise Exception("No assertion part")
diff --git a/src/saml2/saml.py b/src/saml2/saml.py
index 35b7bd1a..c53aab95 100644
--- a/src/saml2/saml.py
+++ b/src/saml2/saml.py
@@ -139,10 +139,12 @@ class AttributeValueBase(SamlBase):
if self._extatt:
self.extension_attributes = self._extatt
- if not text:
- self.extension_attributes = {XSI_NIL: 'true'}
- else:
+ if text:
self.set_text(text)
+ elif not extension_elements:
+ self.extension_attributes = {XSI_NIL: 'true'}
+ elif XSI_TYPE in self.extension_attributes:
+ del self.extension_attributes[XSI_TYPE]
def __setattr__(self, key, value):
if key == "text":
diff --git a/src/saml2/validate.py b/src/saml2/validate.py
index de68fc00..9fe12c4d 100644
--- a/src/saml2/validate.py
+++ b/src/saml2/validate.py
@@ -3,6 +3,7 @@ from six.moves.urllib.parse import urlparse
import re
import struct
import base64
+import time
from saml2 import time_util
@@ -42,8 +43,8 @@ NCNAME = re.compile("(?P<NCName>[a-zA-Z_](\w|[_.-])*)")
def valid_ncname(name):
match = NCNAME.match(name)
- if not match:
- raise NotValid("NCName")
+ #if not match: # hack for invalid authnRequest/ID from meteor saml lib
+ # raise NotValid("NCName")
return True
@@ -90,8 +91,10 @@ def validate_on_or_after(not_on_or_after, slack):
now = time_util.utc_now()
nooa = calendar.timegm(time_util.str_to_time(not_on_or_after))
if now > nooa + slack:
+ now_str=time.strftime('%Y-%M-%dT%H:%M:%SZ', time.gmtime(now))
raise ResponseLifetimeExceed(
- "Can't use it, it's too old %d > %d" % (now - slack, nooa))
+ "Can't use repsonse, too old (now=%s + slack=%d > " \
+ "not_on_or_after=%s" % (now_str, slack, not_on_or_after))
return nooa
else:
return False
@@ -102,8 +105,9 @@ def validate_before(not_before, slack):
now = time_util.utc_now()
nbefore = calendar.timegm(time_util.str_to_time(not_before))
if nbefore > now + slack:
- raise ToEarly("Can't use it yet %d <= %d" % (now + slack, nbefore))
-
+ now_str = time.strftime('%Y-%M-%dT%H:%M:%SZ', time.gmtime(now))
+ raise ToEarly("Can't use response yet: (now=%s + slack=%d) "
+ "<= notbefore=%s" % (now_str, slack, not_before))
return True
diff --git a/src/saml2/xmldsig/__init__.py b/src/saml2/xmldsig/__init__.py
index e00f199d..144cdf54 100644
--- a/src/saml2/xmldsig/__init__.py
+++ b/src/saml2/xmldsig/__init__.py
@@ -29,7 +29,7 @@ DIGEST_ALLOWED_ALG = (('DIGEST_SHA1', DIGEST_SHA1),
('DIGEST_RIPEMD160', DIGEST_RIPEMD160))
DIGEST_AVAIL_ALG = DIGEST_ALLOWED_ALG + (('DIGEST_MD5', DIGEST_MD5),)
-SIG_DSA_SHA1 = 'http,//www.w3.org/2000/09/xmldsig#dsa-sha1'
+SIG_DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'
SIG_DSA_SHA256 = 'http://www.w3.org/2009/xmldsig11#dsa-sha256'
SIG_ECDSA_SHA1 = 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha1'
SIG_ECDSA_SHA224 = 'http://www.w3.org/2001/04/xmldsig-more#ECDSA_sha224'
diff --git a/tests/SWITCHaaiRootCA.crt.pem b/tests/SWITCHaaiRootCA.crt.pem
new file mode 100644
index 00000000..66c9e5d0
--- /dev/null
+++ b/tests/SWITCHaaiRootCA.crt.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDnzCCAoegAwIBAgINSWITCHaai+Root+CAzANBgkqhkiG9w0BAQUFADBrMQsw
+CQYDVQQGEwJDSDFAMD4GA1UEChM3U3dpdGNoIC0gVGVsZWluZm9ybWF0aWtkaWVu
+c3RlIGZ1ZXIgTGVocmUgdW5kIEZvcnNjaHVuZzEaMBgGA1UEAxMRU1dJVENIYWFp
+IFJvb3QgQ0EwHhcNMDgwNTE1MDYzMDAwWhcNMjgwNTE1MDYyOTU5WjBrMQswCQYD
+VQQGEwJDSDFAMD4GA1UEChM3U3dpdGNoIC0gVGVsZWluZm9ybWF0aWtkaWVuc3Rl
+IGZ1ZXIgTGVocmUgdW5kIEZvcnNjaHVuZzEaMBgGA1UEAxMRU1dJVENIYWFpIFJv
+b3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUSWbn/rhWew/s
+LJRyciyRKDGyFXSgiDO/EohYuZLw6EAKLLlhZorNtEHQbbn0Oo13S33MclHMvGWT
+KJM0u1hG+6gLy78EPmJbqAE1Uv23wVEH4SX0VJfl3JVqIebiAH/CjuLubgMUspDI
+jOdQHNLS7pthTbm7Tgh7zMsiLPyMTZJep5CGbqv8NoK6bMaF0Z+Bt7e1JRlhHFCV
+iJJaR/+hfpzLsJ8NWVivvrpRGaGJ1XR+9FGsTkjNdMCirNJJZ6XvUOe5w7pHSd9M
+cppFP0eyLs02AMzMXI4iz6PK/w3EdzXGXpK+gSgvLxWYct4xHpv1e2NXhNgdJOSN
+9ra/wJLVAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG
+MB0GA1UdDgQWBBTpmuIGWOsP14EDXVyXubG1k307hDANBgkqhkiG9w0BAQUFAAOC
+AQEAMV/eIW6pFB+mbk7rD7hUPTWDRaoca3kHqmFGFnHfuY8+c0/Mqjh8Y/jyX1yb
+f58crTSWrbyGbUZ3oxDGQ34tuZSkmeR32NqryiX3sP5qlNSozVguQKt8o4vhS1Qe
+WPsXALs3em2pdKuIGSOpbuDnopPcmU2g5Zi2R5P7qpKDKAKtNUEwV+LW7GBMEksO
+Nj7BFXk4AFBFBijaYJGgHmoKSImVgeNIvsV+BSv5HJ4q6vcxfnwuvvGHM0AGphYO
+6f5qtHMUgvAblI8M/2QsBgethaGrirtKJ3aCRLdaR2R1QfaGRpck/Ron5/MpMxiJ
+wLT8YlW/zjx2yNABhPSAjfzeMw==
+-----END CERTIFICATE-----
diff --git a/tests/conftest.py b/tests/conftest.py
index 3a895627..5048394c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,15 +1,18 @@
import os
+import pytest
#TODO: On my system this function seems to be returning an incorrect location
-def pytest_funcarg__xmlsec(request):
+@pytest.fixture
+def xmlsec(request):
for path in os.environ["PATH"].split(":"):
fil = os.path.join(path, "xmlsec1")
if os.access(fil,os.X_OK):
return fil
raise Exception("Can't find xmlsec1")
-
-def pytest_funcarg__AVA(request):
+
+@pytest.fixture
+def AVA(request):
return [
{
"surName": ["Jeter"],
@@ -27,4 +30,4 @@ def pytest_funcarg__AVA(request):
"surName": ["Hedberg"],
"givenName": ["Roland"],
},
- ]
+ ]
diff --git a/tests/server_conf.py b/tests/server_conf.py
index aa34d8f7..4b528119 100644
--- a/tests/server_conf.py
+++ b/tests/server_conf.py
@@ -14,6 +14,19 @@ CONFIG = {
"required_attributes": ["surName", "givenName", "mail"],
"optional_attributes": ["title"],
"idp": ["urn:mace:example.com:saml:roland:idp"],
+ "requested_attributes": [
+ {
+ "name": "http://eidas.europa.eu/attributes/naturalperson/DateOfBirth",
+ "required": False,
+ },
+ {
+ "friendly_name": "PersonIdentifier",
+ "required": True,
+ },
+ {
+ "friendly_name": "PlaceOfBirth",
+ },
+ ],
}
},
"debug": 1,
diff --git a/tests/sp_conf_nameidpolicy.py b/tests/sp_conf_nameidpolicy.py
new file mode 100644
index 00000000..d15989c2
--- /dev/null
+++ b/tests/sp_conf_nameidpolicy.py
@@ -0,0 +1,64 @@
+from pathutils import full_path
+from pathutils import xmlsec_path
+
+CONFIG = {
+ "entityid": "urn:mace:example.com:saml:roland:sp",
+ "name": "urn:mace:example.com:saml:roland:sp",
+ "description": "My own SP",
+ "service": {
+ "sp": {
+ "endpoints": {
+ "assertion_consumer_service": [
+ "http://lingon.catalogix.se:8087/"],
+ },
+ "required_attributes": ["surName", "givenName", "mail"],
+ "optional_attributes": ["title"],
+ "idp": ["urn:mace:example.com:saml:roland:idp"],
+ "name_id_format": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
+ "name_id_format_allow_create": "true"
+ }
+ },
+ "debug": 1,
+ "key_file": full_path("test.key"),
+ "cert_file": full_path("test.pem"),
+ "encryption_keypairs": [{"key_file": full_path("test_1.key"), "cert_file": full_path("test_1.crt")},
+ {"key_file": full_path("test_2.key"), "cert_file": full_path("test_2.crt")}],
+ "ca_certs": full_path("cacerts.txt"),
+ "xmlsec_binary": xmlsec_path,
+ "metadata": [{
+ "class": "saml2.mdstore.MetaDataFile",
+ "metadata": [(full_path("idp.xml"), ), (full_path("vo_metadata.xml"), )],
+ }],
+ "virtual_organization": {
+ "urn:mace:example.com:it:tek": {
+ "nameid_format": "urn:oid:1.3.6.1.4.1.1466.115.121.1.15-NameID",
+ "common_identifier": "umuselin",
+ }
+ },
+ "subject_data": "subject_data.db",
+ "accepted_time_diff": 60,
+ "attribute_map_dir": full_path("attributemaps"),
+ "valid_for": 6,
+ "organization": {
+ "name": ("AB Exempel", "se"),
+ "display_name": ("AB Exempel", "se"),
+ "url": "http://www.example.org",
+ },
+ "contact_person": [{
+ "given_name": "Roland",
+ "sur_name": "Hedberg",
+ "telephone_number": "+46 70 100 0000",
+ "email_address": ["tech@eample.com",
+ "tech@example.org"],
+ "contact_type": "technical"
+ },
+ ],
+ "logger": {
+ "rotating": {
+ "filename": full_path("sp.log"),
+ "maxBytes": 100000,
+ "backupCount": 5,
+ },
+ "loglevel": "info",
+ }
+}
diff --git a/tests/sp_mdext_conf.py b/tests/sp_mdext_conf.py
index 67e33414..b1f0cf42 100644
--- a/tests/sp_mdext_conf.py
+++ b/tests/sp_mdext_conf.py
@@ -6,6 +6,8 @@ CONFIG = {
"description": "My own SP",
"service": {
"sp": {
+ "sp_type": "public",
+ "sp_type_in_metadata": True,
"endpoints": {
"assertion_consumer_service": [
"http://lingon.catalogix.se:8087/"],
diff --git a/tests/test_19_attribute_converter.py b/tests/test_19_attribute_converter.py
index 0fa807b7..8662feee 100644
--- a/tests/test_19_attribute_converter.py
+++ b/tests/test_19_attribute_converter.py
@@ -10,6 +10,7 @@ from saml2.attribute_converter import AttributeConverter
from saml2.attribute_converter import to_local
from saml2.saml import attribute_from_string, name_id_from_string, NameID, NAMEID_FORMAT_PERSISTENT
from saml2.saml import attribute_statement_from_string
+import saml2.attributemaps.saml_uri as saml_map
def _eq(l1, l2):
@@ -139,12 +140,14 @@ class TestAC():
def test_to_local_name_from_unspecified(self):
_xml = """<?xml version='1.0' encoding='UTF-8'?>
<ns0:AttributeStatement xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion">
-<ns0:Attribute
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- Name="EmailAddress"
- NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
- <ns0:AttributeValue xsi:type="xs:string">foo@bar.com</ns0:AttributeValue>
-</ns0:Attribute></ns0:AttributeStatement>"""
+ <ns0:Attribute
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ Name="EmailAddress"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
+ <ns0:AttributeValue xsi:type="xs:string">foo@bar.com</ns0:AttributeValue>
+ </ns0:Attribute>
+ </ns0:AttributeStatement>
+ """
attr = attribute_statement_from_string(_xml)
ava = attribute_converter.to_local(self.acs, attr)
@@ -236,26 +239,70 @@ def test_noop_attribute_conversion():
assert attr.attribute_value[0].text == "Roland"
-ava = """<?xml version='1.0' encoding='UTF-8'?>
-<ns0:Attribute xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- FriendlyName="schacHomeOrganization" Name="urn:oid:1.3.6.1.4.1.25178.1.2.9"
- NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
- <ns0:AttributeValue xsi:nil="true" xsi:type="xs:string">
- uu.se
- </ns0:AttributeValue>
-</ns0:Attribute>"""
+class BuilderAVA():
+ def __init__(self, name, friendly_name, name_format):
+ template = """<?xml version='1.0' encoding='UTF-8'?>
+ <ns0:Attribute xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ Name="{attr_name}"
+ FriendlyName="{attr_friendly_name}"
+ NameFormat="{attr_name_format}">
+ <ns0:AttributeValue xsi:nil="true" xsi:type="xs:string">
+ uu.se
+ </ns0:AttributeValue>
+ </ns0:Attribute>
+ """
+
+ self.ava = template.format(
+ attr_name=name,
+ attr_friendly_name=friendly_name,
+ attr_name_format=name_format)
+
+
+class TestSchac():
+ def test(self):
+ failures = 0
+ friendly_name = "schacHomeOrganization"
+ ava_schac = BuilderAVA(
+ "urn:oid:1.3.6.1.4.1.25178.1.2.9",
+ friendly_name,
+ saml_map.MAP['identifier'])
+
+ attr = attribute_from_string(ava_schac.ava)
+ acs = attribute_converter.ac_factory()
+
+ for ac in acs:
+ try:
+ res = ac.ava_from(attr)
+ except KeyError:
+ failures += 1
+ else:
+ assert res[0] == "schacHomeOrganization"
+ assert failures != len(acs)
-def test_schac():
- attr = attribute_from_string(ava)
- acs = attribute_converter.ac_factory()
- for ac in acs:
- try:
- res = ac.ava_from(attr)
- assert res[0] == "schacHomeOrganization"
- except KeyError:
- pass
+
+class TestEIDAS():
+ def test(self):
+ failures = 0
+ friendly_name = 'PersonIdentifier'
+ ava_eidas = BuilderAVA(
+ saml_map.EIDAS_NATURALPERSON + friendly_name,
+ friendly_name,
+ saml_map.MAP['identifier'])
+
+ attr = attribute_from_string(ava_eidas.ava)
+ acs = attribute_converter.ac_factory()
+
+ for ac in acs:
+ try:
+ res = ac.ava_from(attr)
+ except KeyError:
+ failures += 1
+ else:
+ assert res[0] == friendly_name
+
+ assert failures != len(acs)
if __name__ == "__main__":
diff --git a/tests/test_20_assertion.py b/tests/test_20_assertion.py
index ae661d53..5fc36f6b 100644
--- a/tests/test_20_assertion.py
+++ b/tests/test_20_assertion.py
@@ -64,7 +64,7 @@ def test_filter_on_attributes_0():
required = [a]
ava = {"serialNumber": ["12345"]}
- ava = filter_on_attributes(ava, required)
+ ava = filter_on_attributes(ava, required, acs=ac_factory())
assert list(ava.keys()) == ["serialNumber"]
assert ava["serialNumber"] == ["12345"]
@@ -76,11 +76,23 @@ def test_filter_on_attributes_1():
required = [a]
ava = {"serialNumber": ["12345"], "givenName": ["Lars"]}
- ava = filter_on_attributes(ava, required)
+ ava = filter_on_attributes(ava, required, acs=ac_factory())
assert list(ava.keys()) == ["serialNumber"]
assert ava["serialNumber"] == ["12345"]
+def test_filter_on_attributes_2():
+
+ a = to_dict(Attribute(friendly_name="surName",name="urn:oid:2.5.4.4",
+ name_format=NAME_FORMAT_URI), ONTS)
+ required = [a]
+ ava = {"sn":["kakavas"]}
+
+ ava = filter_on_attributes(ava,required,acs=ac_factory())
+ assert list(ava.keys()) == ['sn']
+ assert ava["sn"] == ["kakavas"]
+
+
def test_filter_on_attributes_without_friendly_name():
ava = {"eduPersonTargetedID": "test@example.com",
"eduPersonAffiliation": "test",
@@ -106,7 +118,7 @@ def test_filter_on_attributes_with_missing_required_attribute():
name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10",
name_format=NAME_FORMAT_URI), ONTS)
with pytest.raises(MissingValue):
- filter_on_attributes(ava, required=[eptid])
+ filter_on_attributes(ava, required=[eptid], acs=ac_factory())
def test_filter_on_attributes_with_missing_optional_attribute():
@@ -115,7 +127,7 @@ def test_filter_on_attributes_with_missing_optional_attribute():
friendly_name="eduPersonTargetedID",
name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10",
name_format=NAME_FORMAT_URI), ONTS)
- assert filter_on_attributes(ava, optional=[eptid]) == {}
+ assert filter_on_attributes(ava, optional=[eptid], acs=ac_factory()) == {}
# ----------------------------------------------------------------------
@@ -420,7 +432,7 @@ def test_filter_values_req_2():
required = [a1, a2]
ava = {"serialNumber": ["12345"], "givenName": ["Lars"]}
- raises(MissingValue, filter_on_attributes, ava, required)
+ raises(MissingValue, filter_on_attributes, ava, required, acs=ac_factory())
def test_filter_values_req_3():
@@ -432,7 +444,7 @@ def test_filter_values_req_3():
required = [a]
ava = {"serialNumber": ["12345"]}
- ava = filter_on_attributes(ava, required)
+ ava = filter_on_attributes(ava, required, acs=ac_factory())
assert list(ava.keys()) == ["serialNumber"]
assert ava["serialNumber"] == ["12345"]
@@ -446,7 +458,7 @@ def test_filter_values_req_4():
required = [a]
ava = {"serialNumber": ["12345"]}
- raises(MissingValue, filter_on_attributes, ava, required)
+ raises(MissingValue, filter_on_attributes, ava, required, acs=ac_factory())
def test_filter_values_req_5():
@@ -458,7 +470,7 @@ def test_filter_values_req_5():
required = [a]
ava = {"serialNumber": ["12345", "54321"]}
- ava = filter_on_attributes(ava, required)
+ ava = filter_on_attributes(ava, required, acs=ac_factory())
assert list(ava.keys()) == ["serialNumber"]
assert ava["serialNumber"] == ["12345"]
@@ -472,7 +484,7 @@ def test_filter_values_req_6():
required = [a]
ava = {"serialNumber": ["12345", "54321"]}
- ava = filter_on_attributes(ava, required)
+ ava = filter_on_attributes(ava, required, acs=ac_factory())
assert list(ava.keys()) == ["serialNumber"]
assert ava["serialNumber"] == ["54321"]
@@ -489,7 +501,7 @@ def test_filter_values_req_opt_0():
ava = {"serialNumber": ["12345", "54321"]}
- ava = filter_on_attributes(ava, [r], [o])
+ ava = filter_on_attributes(ava, [r], [o], acs=ac_factory())
assert list(ava.keys()) == ["serialNumber"]
assert _eq(ava["serialNumber"], ["12345", "54321"])
@@ -507,7 +519,7 @@ def test_filter_values_req_opt_1():
ava = {"serialNumber": ["12345", "54321"]}
- ava = filter_on_attributes(ava, [r], [o])
+ ava = filter_on_attributes(ava, [r], [o], acs=ac_factory())
assert list(ava.keys()) == ["serialNumber"]
assert _eq(ava["serialNumber"], ["12345", "54321"])
@@ -543,7 +555,7 @@ def test_filter_values_req_opt_2():
ava = {"surname": ["Hedberg"], "givenName": ["Roland"],
"eduPersonAffiliation": ["staff"], "uid": ["rohe0002"]}
- raises(MissingValue, "filter_on_attributes(ava, r, o)")
+ raises(MissingValue, "filter_on_attributes(ava, r, o, acs=ac_factory())")
# ---------------------------------------------------------------------------
@@ -923,3 +935,4 @@ def test_assertion_with_authn_instant():
if __name__ == "__main__":
test_assertion_2()
+
diff --git a/tests/test_30_mdstore.py b/tests/test_30_mdstore.py
index aadd7726..2a79c86a 100644
--- a/tests/test_30_mdstore.py
+++ b/tests/test_30_mdstore.py
@@ -7,12 +7,13 @@ from collections import OrderedDict
from future.backports.urllib.parse import quote_plus
from saml2.config import Config
-from saml2.mdstore import MetadataStore
+from saml2.mdstore import MetadataStore, MetaDataExtern
from saml2.mdstore import MetaDataMDX
from saml2.mdstore import SAML_METADATA_CONTENT_TYPE
from saml2.mdstore import destinations
from saml2.mdstore import name
from saml2 import sigver
+from saml2.httpbase import HTTPBase
from saml2 import BINDING_SOAP
from saml2 import BINDING_HTTP_REDIRECT
from saml2 import BINDING_HTTP_POST
@@ -385,6 +386,14 @@ def test_load_local():
assert cfg
+def test_load_remote_encoding():
+ crypto = sigver._get_xmlsec_cryptobackend()
+ sc = sigver.SecurityContext(crypto, key_type="", cert_type="")
+ httpc = HTTPBase()
+ mds = MetaDataExtern(ATTRCONV, 'http://metadata.aai.switch.ch/metadata.aaitest.xml', sc, full_path('SWITCHaaiRootCA.crt.pem'), httpc)
+ mds.load()
+
+
def test_load_string():
sec_config.xmlsec_binary = sigver.get_xmlsec_binary(["/opt/local/bin"])
mds = MetadataStore(ATTRCONV, sec_config,
diff --git a/tests/test_31_config.py b/tests/test_31_config.py
index 623c944f..eb8480c6 100644
--- a/tests/test_31_config.py
+++ b/tests/test_31_config.py
@@ -68,6 +68,7 @@ sp2 = {
},
"authn_requests_signed": True,
"logout_requests_signed": True,
+ "force_authn": True,
}
},
#"xmlsec_binary" : "/opt/local/bin/xmlsec1",
@@ -408,5 +409,15 @@ def test_crypto_backend():
sec = security_context(idpc)
assert isinstance(sec.crypto, CryptoBackendXMLSecurity)
+def test_unset_force_authn():
+ cnf = SPConfig().load(sp1)
+ assert bool(cnf.getattr('force_authn', 'sp')) == False
+
+
+def test_set_force_authn():
+ cnf = SPConfig().load(sp2)
+ assert bool(cnf.getattr('force_authn', 'sp')) == True
+
+
if __name__ == "__main__":
test_crypto_backend()
diff --git a/tests/test_50_server.py b/tests/test_50_server.py
index 4aa834c5..f0dcae3c 100644
--- a/tests/test_50_server.py
+++ b/tests/test_50_server.py
@@ -96,7 +96,7 @@ class TestServer1():
self.client = client.Saml2Client(conf)
self.name_id = self.server.ident.transient_nameid(
"urn:mace:example.com:saml:roland:sp", "id12")
- self.ava = {"givenName": ["Derek"], "surName": ["Jeter"],
+ self.ava = {"givenName": ["Derek"], "sn": ["Jeter"],
"mail": ["derek@nyy.mlb.com"], "title": "The man"}
def teardown_class(self):
@@ -110,7 +110,7 @@ class TestServer1():
assert ava ==\
{'mail': ['derek@nyy.mlb.com'], 'givenName': ['Derek'],
- 'surName': ['Jeter'], 'title': ['The man']}
+ 'sn': ['Jeter'], 'title': ['The man']}
def verify_encrypted_assertion(self, assertion, decr_text):
@@ -145,7 +145,7 @@ class TestServer1():
format=saml.NAMEID_FORMAT_TRANSIENT)),
attribute_statement=do_attribute_statement(
{
- ("", "", "surName"): ("Jeter", ""),
+ ("", "", "sn"): ("Jeter", ""),
("", "", "givenName"): ("Derek", ""),
}
),
@@ -164,12 +164,12 @@ class TestServer1():
attr1 = attribute_statement.attribute[1]
if attr0.attribute_value[0].text == "Derek":
assert attr0.friendly_name == "givenName"
- assert attr1.friendly_name == "surName"
+ assert attr1.friendly_name == "sn"
assert attr1.attribute_value[0].text == "Jeter"
else:
assert attr1.friendly_name == "givenName"
assert attr1.attribute_value[0].text == "Derek"
- assert attr0.friendly_name == "surName"
+ assert attr0.friendly_name == "sn"
assert attr0.attribute_value[0].text == "Jeter"
#
subject = assertion.subject
@@ -187,7 +187,7 @@ class TestServer1():
name_id=saml.NAMEID_FORMAT_TRANSIENT),
attribute_statement=do_attribute_statement(
{
- ("", "", "surName"): ("Jeter", ""),
+ ("", "", "sn"): ("Jeter", ""),
("", "", "givenName"): ("Derek", ""),
}
),
@@ -277,7 +277,7 @@ class TestServer1():
resp = self.server.create_authn_response(
{
"eduPersonEntitlement": "Short stop",
- "surName": "Jeter",
+ "sn": "Jeter",
"givenName": "Derek",
"mail": "derek.jeter@nyy.mlb.com",
"title": "The man"
@@ -394,7 +394,7 @@ class TestServer1():
conf.load_file("server_conf")
self.client = client.Saml2Client(conf)
- ava = {"givenName": ["Derek"], "surName": ["Jeter"],
+ ava = {"givenName": ["Derek"], "sn": ["Jeter"],
"mail": ["derek@nyy.mlb.com"], "title": "The man"}
npolicy = samlp.NameIDPolicy(format=saml.NAMEID_FORMAT_TRANSIENT,
@@ -425,7 +425,7 @@ class TestServer1():
def test_signed_response(self):
name_id = self.server.ident.transient_nameid(
"urn:mace:example.com:saml:roland:sp", "id12")
- ava = {"givenName": ["Derek"], "surName": ["Jeter"],
+ ava = {"givenName": ["Derek"], "sn": ["Jeter"],
"mail": ["derek@nyy.mlb.com"], "title": "The man"}
signed_resp = self.server.create_authn_response(
@@ -1139,7 +1139,7 @@ class TestServer1():
"not_on_or_after": soon,
"user": {
"givenName": "Leo",
- "surName": "Laport",
+ "sn": "Laport",
}
}
self.client.users.add_information_about_person(sinfo)
@@ -1163,7 +1163,7 @@ class TestServer1():
"not_on_or_after": soon,
"user": {
"givenName": "Leo",
- "surName": "Laport",
+ "sn": "Laport",
}
}
@@ -1188,7 +1188,7 @@ class TestServer1():
#------------------------------------------------------------------------
IDENTITY = {"eduPersonAffiliation": ["staff", "member"],
- "surName": ["Jeter"], "givenName": ["Derek"],
+ "sn": ["Jeter"], "givenName": ["Derek"],
"mail": ["foo@gmail.com"], "title": "The man"}
@@ -1234,7 +1234,7 @@ def _logout_request(conf_file):
"not_on_or_after": soon,
"user": {
"givenName": "Leo",
- "surName": "Laport",
+ "sn": "Laport",
}
}
sp.users.add_information_about_person(sinfo)
diff --git a/tests/test_51_client.py b/tests/test_51_client.py
index 13cef7cc..2bd4d7cf 100644
--- a/tests/test_51_client.py
+++ b/tests/test_51_client.py
@@ -22,6 +22,8 @@ from saml2 import samlp
from saml2 import sigver
from saml2 import s_utils
from saml2.assertion import Assertion
+from saml2.extension.requested_attributes import RequestedAttributes
+from saml2.extension.requested_attributes import RequestedAttribute
from saml2.authn_context import INTERNETPROTOCOLPASSWORD
from saml2.client import Saml2Client
@@ -280,6 +282,51 @@ class TestClient:
assert nid_policy.allow_create == "false"
assert nid_policy.format == saml.NAMEID_FORMAT_TRANSIENT
+ node_requested_attributes = None
+ for e in ar.extensions.extension_elements:
+ if e.tag == RequestedAttributes.c_tag:
+ node_requested_attributes = e
+ break
+ assert node_requested_attributes is not None
+
+ for c in node_requested_attributes.children:
+ assert c.tag == RequestedAttribute.c_tag
+ assert c.attributes['isRequired'] in ['true', 'false']
+ assert c.attributes['Name']
+ assert c.attributes['FriendlyName']
+ assert c.attributes['NameFormat']
+
+ def test_create_auth_request_unset_force_authn(self):
+ req_id, req = self.client.create_authn_request(
+ "http://www.example.com/sso", sign=False, message_id="id1")
+ assert bool(req.force_authn) == False
+
+ def test_create_auth_request_set_force_authn(self):
+ req_id, req = self.client.create_authn_request(
+ "http://www.example.com/sso", sign=False, message_id="id1",
+ force_authn="true")
+ assert bool(req.force_authn) == True
+
+ def test_create_auth_request_nameid_policy_allow_create(self):
+ conf = config.SPConfig()
+ conf.load_file("sp_conf_nameidpolicy")
+ client = Saml2Client(conf)
+ ar_str = "%s" % client.create_authn_request(
+ "http://www.example.com/sso", message_id="id1")[1]
+
+ ar = samlp.authn_request_from_string(ar_str)
+ print(ar)
+ assert ar.assertion_consumer_service_url == ("http://lingon.catalogix"
+ ".se:8087/")
+ assert ar.destination == "http://www.example.com/sso"
+ assert ar.protocol_binding == BINDING_HTTP_POST
+ assert ar.version == "2.0"
+ assert ar.provider_name == "urn:mace:example.com:saml:roland:sp"
+ assert ar.issuer.text == "urn:mace:example.com:saml:roland:sp"
+ nid_policy = ar.name_id_policy
+ assert nid_policy.allow_create == "true"
+ assert nid_policy.format == saml.NAMEID_FORMAT_PERSISTENT
+
def test_create_auth_request_vo(self):
assert list(self.client.config.vorg.keys()) == [
"urn:mace:example.com:it:tek"]
@@ -346,7 +393,7 @@ class TestClient:
def test_response_1(self):
IDP = "urn:mace:example.com:saml:roland:idp"
- ava = {"givenName": ["Derek"], "surName": ["Jeter"],
+ ava = {"givenName": ["Derek"], "sn": ["Jeter"],
"mail": ["derek@nyy.mlb.com"], "title": ["The man"]}
nameid_policy = samlp.NameIDPolicy(allow_create="false",
@@ -394,7 +441,7 @@ class TestClient:
# --- authenticate another person
- ava = {"givenName": ["Alfonson"], "surName": ["Soriano"],
+ ava = {"givenName": ["Alfonson"], "sn": ["Soriano"],
"mail": ["alfonson@chc.mlb.com"], "title": ["outfielder"]}
resp_str = "%s" % self.server.create_authn_response(
@@ -712,7 +759,7 @@ class TestClient:
def setup_verify_authn_response(self):
idp = "urn:mace:example.com:saml:roland:idp"
- ava = {"givenName": ["Derek"], "surName": ["Jeter"],
+ ava = {"givenName": ["Derek"], "sn": ["Jeter"],
"mail": ["derek@nyy.mlb.com"], "title": ["The man"]}
ava_verify = {'mail': ['derek@nyy.mlb.com'], 'givenName': ['Derek'],
'sn': ['Jeter'], 'title': ["The man"]}
@@ -761,7 +808,7 @@ class TestClient:
format=saml.NAMEID_FORMAT_TRANSIENT)),
attribute_statement=do_attribute_statement(
{
- ("", "", "surName"): ("Jeter", ""),
+ ("", "", "sn"): ("Jeter", ""),
("", "", "givenName"): ("Derek", ""),
}
),
@@ -825,7 +872,7 @@ class TestClient:
nameid_policy = samlp.NameIDPolicy(allow_create="false",
format=saml.NAMEID_FORMAT_PERSISTENT)
- asser = Assertion({"givenName": "Derek", "surName": "Jeter"})
+ asser = Assertion({"givenName": "Derek", "sn": "Jeter"})
farg = add_path(
{},
['assertion', 'subject', 'subject_confirmation', 'method',
@@ -896,7 +943,7 @@ class TestClient:
nameid_policy = samlp.NameIDPolicy(allow_create="false",
format=saml.NAMEID_FORMAT_PERSISTENT)
- asser = Assertion({"givenName": "Derek", "surName": "Jeter"})
+ asser = Assertion({"givenName": "Derek", "sn": "Jeter"})
subject_confirmation_specs = {
'recipient': "http://lingon.catalogix.se:8087/",
@@ -1027,7 +1074,7 @@ class TestClient:
name_id=name_id,
farg=farg['assertion'])
- asser_2 = Assertion({"surName": "Jeter"})
+ asser_2 = Assertion({"sn": "Jeter"})
assertion_2 = asser_2.construct(
self.client.config.entityid,
@@ -1313,7 +1360,7 @@ class TestClient:
"not_on_or_after": in_a_while(minutes=15),
"ava": {
"givenName": "Anders",
- "surName": "Andersson",
+ "sn": "Andersson",
"mail": "anders.andersson@example.com"
}
}
@@ -1350,7 +1397,7 @@ class TestClient:
"not_on_or_after": in_a_while(minutes=15),
"ava": {
"givenName": "Anders",
- "surName": "Andersson",
+ "sn": "Andersson",
"mail": "anders.andersson@example.com"
},
"session_index": SessionIndex("_foo")
@@ -1367,7 +1414,7 @@ class TestClient:
binding, info = resp[entity_ids[0]]
assert binding == BINDING_HTTP_POST
- _dic = unpack_form(info["data"][3])
+ _dic = unpack_form(info["data"])
res = self.server.parse_logout_request(_dic["SAMLRequest"],
BINDING_HTTP_POST)
assert b'<ns0:SessionIndex>_foo</ns0:SessionIndex>' in res.xmlstr
@@ -1380,7 +1427,7 @@ class TestClient:
"not_on_or_after": a_while_ago(minutes=15),
"ava": {
"givenName": "Anders",
- "surName": "Andersson",
+ "sn": "Andersson",
"mail": "anders.andersson@example.com"
},
"session_index": SessionIndex("_foo")
@@ -1397,7 +1444,7 @@ class TestClient:
binding, info = resp[entity_ids[0]]
assert binding == BINDING_HTTP_POST
- _dic = unpack_form(info["data"][3])
+ _dic = unpack_form(info["data"])
res = self.server.parse_logout_request(_dic["SAMLRequest"],
BINDING_HTTP_POST)
assert b'<ns0:SessionIndex>_foo</ns0:SessionIndex>' in res.xmlstr
@@ -1473,7 +1520,7 @@ class TestClientWithDummy():
"not_on_or_after": in_a_while(minutes=15),
"ava": {
"givenName": "Anders",
- "surName": "Andersson",
+ "sn": "Andersson",
"mail": "anders.andersson@example.com"
}
}
@@ -1494,7 +1541,7 @@ class TestClientWithDummy():
sid, http_args = self.client.prepare_for_authenticate(
"urn:mace:example.com:saml:roland:idp", relay_state="really",
binding=binding, response_binding=response_binding)
- _dic = unpack_form(http_args["data"][3])
+ _dic = unpack_form(http_args["data"])
req = self.server.parse_authn_request(_dic["SAMLRequest"], binding)
resp_args = self.server.response_args(req.message, [response_binding])
@@ -1512,7 +1559,7 @@ class TestClientWithDummy():
response = self.client.send(**http_args)
print(response.text)
- _dic = unpack_form(response.text[3], "SAMLResponse")
+ _dic = unpack_form(response.text, "SAMLResponse")
resp = self.client.parse_authn_request_response(_dic["SAMLResponse"],
BINDING_HTTP_POST,
{sid: "/"})
@@ -1527,7 +1574,7 @@ class TestClientWithDummy():
sid, auth_binding, http_args = self.client.prepare_for_negotiated_authenticate(
"urn:mace:example.com:saml:roland:idp", relay_state="really",
binding=binding, response_binding=response_binding)
- _dic = unpack_form(http_args["data"][3])
+ _dic = unpack_form(http_args["data"])
assert binding == auth_binding
@@ -1547,7 +1594,7 @@ class TestClientWithDummy():
response = self.client.send(**http_args)
print(response.text)
- _dic = unpack_form(response.text[3], "SAMLResponse")
+ _dic = unpack_form(response.text, "SAMLResponse")
resp = self.client.parse_authn_request_response(_dic["SAMLResponse"],
BINDING_HTTP_POST,
{sid: "/"})
diff --git a/tests/test_65_authn_query.py b/tests/test_65_authn_query.py
index 54d529f8..68af10a1 100644
--- a/tests/test_65_authn_query.py
+++ b/tests/test_65_authn_query.py
@@ -28,7 +28,7 @@ def get_msg(hinfo, binding):
if binding == BINDING_SOAP:
xmlstr = hinfo["data"]
elif binding == BINDING_HTTP_POST:
- _inp = hinfo["data"][3]
+ _inp = hinfo["data"]
i = _inp.find(TAG1)
i += len(TAG1) + 1
j = _inp.find('"', i)
diff --git a/tests/test_68_assertion_id.py b/tests/test_68_assertion_id.py
index 52959f3a..283b4da6 100644
--- a/tests/test_68_assertion_id.py
+++ b/tests/test_68_assertion_id.py
@@ -27,7 +27,7 @@ def get_msg(hinfo, binding, response=False):
if binding == BINDING_SOAP:
msg = hinfo["data"]
elif binding == BINDING_HTTP_POST:
- _inp = hinfo["data"][3]
+ _inp = hinfo["data"]
i = _inp.find(TAG1)
i += len(TAG1) + 1
j = _inp.find('"', i)
diff --git a/tests/test_83_md_extensions.py b/tests/test_83_md_extensions.py
index 71f98868..dace10a5 100644
--- a/tests/test_83_md_extensions.py
+++ b/tests/test_83_md_extensions.py
@@ -1,5 +1,6 @@
from saml2.config import Config
from saml2.metadata import entity_descriptor
+from saml2.extension.sp_type import SPType
__author__ = 'roland'
@@ -14,4 +15,13 @@ assert ed.spsso_descriptor.extensions
assert len(ed.spsso_descriptor.extensions.extension_elements) == 3
assert ed.extensions
-assert len(ed.extensions.extension_elements) > 1 \ No newline at end of file
+assert len(ed.extensions.extension_elements) > 1
+
+assert any(e.tag is SPType.c_tag for e in ed.extensions.extension_elements)
+
+cnf.setattr('sp', 'sp_type_in_metadata', False)
+ed = entity_descriptor(cnf)
+
+print(ed)
+
+assert all(e.tag is not SPType.c_tag for e in ed.extensions.extension_elements)
diff --git a/tests/test_requirements.txt b/tests/test_requirements.txt
index 2c1ebbbc..33301079 100644
--- a/tests/test_requirements.txt
+++ b/tests/test_requirements.txt
@@ -2,3 +2,4 @@ mock==2.0.0
pymongo==3.0.1
pytest==3.0.3
responses==0.5.0
+pyasn1==0.2.3
diff --git a/tools/data/requested_attributes.xsd b/tools/data/requested_attributes.xsd
new file mode 100644
index 00000000..b796f3d3
--- /dev/null
+++ b/tools/data/requested_attributes.xsd
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema
+ xmlns="http://eidas.europa.eu/saml-extensions"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:eidas="http://eidas.europa.eu/saml-extensions"
+ targetNamespace="http://eidas.europa.eu/saml-extensions"
+ elementFormDefault="qualified"
+ attributeFormDefault="unqualified"
+ version="1">
+ <xsd:element name="RequestedAttributes" type="eidas:RequestedAttributesType"/>
+ <xsd:complexType name="RequestedAttributesType">
+ <xsd:sequence>
+ <xsd:element minOccurs="0" maxOccurs="unbounded" ref="eidas:RequestedAttribute"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ <xsd:element name="RequestedAttribute" type="eidas:RequestedAttributeType"/>
+ <xsd:complexType name="RequestedAttributeType">
+ <xsd:sequence>
+ <xsd:element minOccurs="0" maxOccurs="unbounded" ref="saml2:AttributeValue" type="anyType"/>
+ </xsd:sequence>
+ <xsd:attribute name="Name" type="string" use="required"/>
+ <xsd:attribute name="NameFormat" type="anyURI" use="required"/>
+ <xsd:attribute name="FriendlyName" type="string" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="lax"/>
+ <xsd:attribute name="isRequired" type="boolean" use="optional"/>
+ </xsd:complexType>
+</xsd:schema>
diff --git a/tools/data/sp_type.xsd b/tools/data/sp_type.xsd
new file mode 100644
index 00000000..dbb1418d
--- /dev/null
+++ b/tools/data/sp_type.xsd
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema
+ xmlns="http://eidas.europa.eu/saml-extensions"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ targetNamespace="http://eidas.europa.eu/saml-extensions"
+ elementFormDefault="qualified"
+ attributeFormDefault="unqualified"
+ version="1">
+ <xsd:element name="SPType" type="SPTypeType"/>
+ <xsd:simpleType name="SPTypeType">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="public"/>
+ <xsd:enumeration value="private"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+</xsd:schema>
diff --git a/tox.ini b/tox.ini
index d834512d..fcdbdde5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,4 +3,4 @@ envlist = py27,py34
[testenv]
deps = -rtests/test_requirements.txt
-commands = py.test tests/
+commands = py.test {posargs:tests/}