summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoland Hedberg <roland@catalogix.se>2017-10-11 08:28:11 +0200
committerGitHub <noreply@github.com>2017-10-11 08:28:11 +0200
commit53e527d7d0147f5d85f0ca456d8f609dd377d4ca (patch)
tree7735d7895ba7c2926ea41fbfc263ebaf00c1b1d1
parent232696285745f19f1d828519e34ec66427ad72a4 (diff)
parent20c961045cb1e2b7b56b2c7eb72595897d1a1477 (diff)
downloadpysaml2-53e527d7d0147f5d85f0ca456d8f609dd377d4ca.tar.gz
Merge pull request #437 from c00kiemon5ter/feature-eidas-support
Add support for eIDAS SAML profile
-rw-r--r--src/saml2/attributemaps/saml_uri.py17
-rw-r--r--src/saml2/client_base.py66
-rw-r--r--src/saml2/config.py3
-rw-r--r--src/saml2/extension/requested_attributes.py131
-rw-r--r--src/saml2/extension/sp_type.py54
-rw-r--r--src/saml2/metadata.py12
-rw-r--r--tests/server_conf.py13
-rw-r--r--tests/sp_mdext_conf.py2
-rw-r--r--tests/test_19_attribute_converter.py95
-rw-r--r--tests/test_51_client.py16
-rw-r--r--tests/test_83_md_extensions.py12
-rw-r--r--tools/data/requested_attributes.xsd28
-rw-r--r--tools/data/sp_type.xsd16
13 files changed, 439 insertions, 26 deletions
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 50b457d1..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
@@ -347,6 +352,67 @@ class Base(Entity):
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 e508a954..296f0e85 100644
--- a/src/saml2/config.py
+++ b/src/saml2/config.py
@@ -78,6 +78,9 @@ SP_ARGS = [
"requested_attribute_name_format",
"hide_assertion_consumer_service",
"force_authn",
+ "sp_type",
+ "sp_type_in_metadata",
+ "requested_attributes",
]
AA_IDP_ARGS = [
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/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/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_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_51_client.py b/tests/test_51_client.py
index bcc535af..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,20 @@ 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")
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/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>