summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/howto/config.rst246
-rwxr-xr-xexample/sp-wsgi/sp.py16
-rwxr-xr-xsetup.py13
-rw-r--r--src/saml2/attribute_converter.py2
-rw-r--r--src/saml2/attributemaps/saml_uri.py17
-rw-r--r--src/saml2/authn.py5
-rw-r--r--src/saml2/client_base.py101
-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/httputil.py3
-rw-r--r--src/saml2/metadata.py12
-rw-r--r--src/saml2/pack.py49
-rw-r--r--src/saml2/response.py3
-rw-r--r--src/saml2/validate.py14
-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_40_sigver.py29
-rw-r--r--tests/test_51_client.py38
-rw-r--r--tests/test_60_sp.py11
-rw-r--r--tests/test_63_ecp.py3
-rw-r--r--tests/test_65_authn_query.py4
-rw-r--r--tests/test_68_assertion_id.py5
-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
27 files changed, 797 insertions, 128 deletions
diff --git a/doc/howto/config.rst b/doc/howto/config.rst
index 1819f6f4..c1691119 100644
--- a/doc/howto/config.rst
+++ b/doc/howto/config.rst
@@ -61,7 +61,7 @@ attribute_map_dir
Format::
"attribute_map_dir": "attribute-maps"
-
+
Points to a directory which has the attribute maps in Python modules.
A typical map file will looks like this::
@@ -91,9 +91,9 @@ The *to* and *fro* sub-dictionaries then contain the mapping between the names.
As you see the format is again a python dictionary where the key is the
name to convert from, and the value is the name to convert to.
-
-Since *to* in most cases is the inverse of the *fro* file, the
-software allowes you to only specify one of them and it will
+
+Since *to* in most cases is the inverse of the *fro* file, the
+software allowes you to only specify one of them and it will
automatically create the other.
cert_file
@@ -109,11 +109,11 @@ This is the public part of the service private/public key pair.
contact_person
^^^^^^^^^^^^^^
-This is only used by *make_metadata.py* when it constructs the metadata for
+This is only used by *make_metadata.py* when it constructs the metadata for
the service described by the configuration file.
This is where you describe who can be contacted if questions arise
about the service or if support is needed. The possible types are according to
-the standard **technical**, **support**, **administrative**, **billing**
+the standard **technical**, **support**, **administrative**, **billing**
and **other**.::
contact_person: [{
@@ -179,9 +179,9 @@ a file accessible on the server the service runs on, or somewhere on the net.::
}],
},
-The above configuration means that the service should read two local
+The above configuration means that the service should read two local
metadata files, and on top of that load one from the net. To verify the
-authenticity of the file downloaded from the net, the local copy of the
+authenticity of the file downloaded from the net, the local copy of the
public key should be used.
This public key must be acquired by some out-of-band method.
@@ -198,16 +198,49 @@ Where you describe the organization responsible for the service.::
}
.. note:: You can specify the language of the name, or the language used on
- the webpage, by entering a tuple, instead of a simple string,
+ the webpage, by entering a tuple, instead of a simple string,
where the second part is the language code. If you don't specify a
language the default is "en" (English).
+preferred_binding
+^^^^^^^^^^^^^^^^^
+
+Which binding should be prefered for a service.
+Example configuration::
+
+ "preferred_binding" = {
+ "single_sign_on_service": [
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact',
+ ],
+ "single_logout_service": [
+ 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP',
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact',
+ ],
+ }
+
+The available services are:
+
+* manage_name_id_service
+* assertion_consumer_service
+* name_id_mapping_service
+* authn_query_service
+* attribute_service
+* authz_service
+* assertion_id_request_service
+* artifact_resolution_service
+* attribute_consuming_service
+
+
service
^^^^^^^
-Which services the server will provide; those are combinations of "idp", "sp"
+Which services the server will provide; those are combinations of "idp", "sp"
and "aa".
-So if a server is a Service Provider (SP) then the configuration
+So if a server is a Service Provider (SP) then the configuration
could look something like this::
"service": {
@@ -225,7 +258,7 @@ could look something like this::
},
}
},
-
+
There are two options common to all services: 'name' and 'endpoints'.
The remaining options are specific to one or the other of the service types.
Which one is specified along side the name of the option.
@@ -312,17 +345,17 @@ An example might be::
}
}
}
-
-*lifetime*
- This is the maximum amount of time before the information should be
- regarded as stale. In an Assertion this is represented in the NotOnOrAfter
- attribute.
+
+*lifetime*
+ This is the maximum amount of time before the information should be
+ regarded as stale. In an Assertion this is represented in the NotOnOrAfter
+ attribute.
*attribute_restrictions*
By default there is no restrictions as to which attributes should be
- return. Instead all the attributes and values that are gathered by the
+ return. Instead all the attributes and values that are gathered by the
database backends will be returned if nothing else is stated.
In the example above the SP with the entity identifier
- "urn:mace:umu.se:saml:roland:sp"
+ "urn:mace:umu.se:saml:roland:sp"
has an attribute restriction: only the attributes
'givenName' and 'surName' are to be returned. There is no limitations as to
what values on these attributes that can be returned.
@@ -332,7 +365,7 @@ An example might be::
the friendly name, and the saml attribute name will be taken from the uri/oid
defined in the attribute map.
-If restrictions on values are deemed necessary those are represented by
+If restrictions on values are deemed necessary those are represented by
regular expressions.::
"service": {
@@ -375,12 +408,161 @@ Example::
}
+want_response_signed
+""""""""""""""""""""
+
+Indicates that Authentication Responses to this SP must be signed. If set to
+True, the SP will not consume any SAML Responses that are not signed.
+
+Example::
+
+ "service": {
+ "sp": {
+ "want_response_signed": True,
+ }
+ }
+
+
+force_authn
+"""""""""""
+
+Mandates that the identity provider MUST authenticate the presenter directly
+rather than rely on a previous security context.
+
+Example::
+
+ "service": {
+ "sp": {
+ "force_authn": True,
+ }
+ }
+
+
+allow_unsolicited
+"""""""""""""""""
+
+When set to true, the SP will consume unsolicited SAML Responses, i.e. SAML
+Responses for which it has not sent a respective SAML Authentication Request.
+
+Example::
+
+ "service": {
+ "sp": {
+ "allow_unsolicited": True,
+ }
+ }
+
+
+hide_assertion_consumer_service
+"""""""""""""""""""""""""""""""
+
+When set to true the AuthnRequest will not include the
+AssertionConsumerServiceURL and ProtocolBinding attributes.
+
+Example::
+
+ "service": {
+ "sp": {
+ "hide_assertion_consumer_service": True,
+ }
+ }
+
+This kind of functionality is required for the eIDAS SAML profile
+
+> eIDAS-Connectors SHOULD NOT provide AssertionConsumerServiceURL.
+
+.. note::
+ This is relevant only for the eIDAS SAML profile.
+
+
+sp_type
+"""""""
+
+Sets the value for the eIDAS SPType node. By the eIDAS specification the value
+can be one of *public* and *private*.
+
+Example::
+
+ "service": {
+ "sp": {
+ "sp_type": "private",
+ }
+ }
+
+.. note::
+ This is relevant only for the eIDAS SAML profile.
+
+
+sp_type_in_metadata
+"""""""""""""""""""
+
+Whether the SPType node should appear in the metadata document
+or as part of each AuthnRequest.
+
+Example::
+
+ "service": {
+ "sp": {
+ "sp_type_in_metadata": True,
+ }
+ }
+
+.. note::
+ This is relevant only for the eIDAS SAML profile.
+
+
+requested_attributes
+""""""""""""""""""""
+
+A list of attributes that the SP requires from an eIDAS-Service (IdP).
+Each attribute is an object with the following attributes:
+
+* friendly_name
+* name
+* required
+* name_format
+
+Where friendly_name is an attribute name such as *DateOfBirth*, name is the
+full attribute name such as
+*http://eidas.europa.eu/attributes/naturalperson/DateOfBirth*, required
+indicates whether this attributed is required for authentication, and
+name_format indicates the name format for that attribute, such as
+*urn:oasis:names:tc:SAML:2.0:attrname-format:uri*.
+
+It is mandatory that at least name or friendly_name is set.
+By default attributes are assumed to be required.
+Missing attributes are infered based on the attribute maps data.
+
+Example::
+
+ "service": {
+ "sp": {
+ "requested_attributes": [
+ {
+ "name": "http://eidas.europa.eu/attributes/naturalperson/PersonIdentifier",
+ },
+ {
+ "friendly_name": "DateOfBirth",
+ "required": False,
+ },
+ ],
+ }
+ }
+
+.. note::
+ This is relevant only for the eIDAS SAML profile.
+
+ This option is different from the required_attributes and
+ optional_attributes parameters that control the requested
+ attributes in the metadata of an SP.
+
+
idp
"""
Defines the set of IdPs that this SP is allowed to use; if unset, all listed
IdPs may be used. If set, then the value is expected to be a list with entity
-identifiers for the allowed IdPs.
+identifiers for the allowed IdPs.
A typical configuration, when the allowed set of IdPs are limited, would look
something like this::
@@ -404,7 +586,7 @@ Example::
"optional_attributes": ["title"],
}
}
-
+
Since the attribute names used here are the user friendly ones an attribute map
must exist, so that the server can use the full name when communicating
with other servers.
@@ -422,7 +604,7 @@ Example::
}
}
-Again as for *optional_attributes* the names given are expected to be
+Again as for *optional_attributes* the names given are expected to be
the user friendly names.
want_assertions_signed
@@ -444,7 +626,7 @@ Example::
idp/aa/sp
-^^^^^^^^^
+^^^^^^^^^
If the configuration is covering both two or three different service types
(like if one server is actually acting as both an IdP and a SP) then in some
@@ -516,7 +698,7 @@ Example::
subject_data
""""""""""""
-The name of a database where the map between a local identifier and
+The name of a database where the map between a local identifier and
a distributed identifier is kept. By default this is a shelve database.
So if you just specify name, then a shelve database with that name
is created. On the other hand if you specify a tuple then the first
@@ -548,8 +730,8 @@ Gives information about common identifiers for virtual_organizations::
},
Keys in this dictionary are the identifiers for the virtual organizations.
-The arguments per organization are 'nameid_format' and 'common_identifier'.
-Useful if all the IdPs and AAs that are involved in a virtual organization
+The arguments per organization are 'nameid_format' and 'common_identifier'.
+Useful if all the IdPs and AAs that are involved in a virtual organization
have common attribute values for users that are part of the VO.
Complete example
@@ -622,9 +804,9 @@ A slightly more complex configuration::
"key_file" : "./mykey.pem",
"cert_file" : "./mycert.pem",
"xmlsec_binary" : "/usr/local/bin/xmlsec1",
- "metadata" : {
+ "metadata" : {
"local": ["example.xml"],
- "remote": [{
+ "remote": [{
"url":"https://kalmar2.org/simplesaml/module.php/aggregator/?id=kalmarcentral2&set=saml2",
"cert":"kalmar2.pem"}]
},
@@ -640,9 +822,9 @@ A slightly more complex configuration::
"type": "technical",
}]
}
-
-Uses metadata files, both local and remote, and will talk to whatever
-IdP that appears in any of the metadata files.
+
+Uses metadata files, both local and remote, and will talk to whatever
+IdP that appears in any of the metadata files.
Other considerations
::::::::::::::::::::
diff --git a/example/sp-wsgi/sp.py b/example/sp-wsgi/sp.py
index 38a737ee..be1e8e68 100755
--- a/example/sp-wsgi/sp.py
+++ b/example/sp-wsgi/sp.py
@@ -106,21 +106,21 @@ def handle_static(environ, start_response, path):
:return: wsgi response for the static file.
"""
try:
- text = open(path).read()
+ data = open(path, 'rb').read()
if path.endswith(".ico"):
- resp = Response(text, headers=[('Content-Type', "image/x-icon")])
+ resp = Response(data, headers=[('Content-Type', "image/x-icon")])
elif path.endswith(".html"):
- resp = Response(text, headers=[('Content-Type', 'text/html')])
+ resp = Response(data, headers=[('Content-Type', 'text/html')])
elif path.endswith(".txt"):
- resp = Response(text, headers=[('Content-Type', 'text/plain')])
+ resp = Response(data, headers=[('Content-Type', 'text/plain')])
elif path.endswith(".css"):
- resp = Response(text, headers=[('Content-Type', 'text/css')])
+ resp = Response(data, headers=[('Content-Type', 'text/css')])
elif path.endswith(".js"):
- resp = Response(text, headers=[('Content-Type', 'text/javascript')])
+ resp = Response(data, headers=[('Content-Type', 'text/javascript')])
elif path.endswith(".png"):
- resp = Response(text, headers=[('Content-Type', 'image/png')])
+ resp = Response(data, headers=[('Content-Type', 'image/png')])
else:
- resp = Response(text)
+ resp = Response(data)
except IOError:
resp = NotFound()
return resp(environ, start_response)
diff --git a/setup.py b/setup.py
index b29c31ca..3116e9af 100755
--- a/setup.py
+++ b/setup.py
@@ -8,12 +8,8 @@ from setuptools.command.test import test as TestCommand
install_requires = [
# core dependencies
- 'decorator',
'requests >= 1.0.0',
'future',
- 'paste',
- 'zope.interface',
- 'repoze.who',
'cryptography',
'pytz',
'pyOpenSSL',
@@ -22,6 +18,14 @@ install_requires = [
'six'
]
+extras_require = {
+ 's2repoze': [
+ 'paste',
+ 'zope.interface',
+ 'repoze.who'
+ ]
+}
+
version = ''
with open('src/saml2/__init__.py', 'r') as fd:
version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
@@ -56,5 +60,6 @@ setup(
scripts=["tools/parse_xsd2.py", "tools/make_metadata.py",
"tools/mdexport.py", "tools/merge_metadata.py"],
install_requires=install_requires,
+ extras_require=extras_require,
zip_safe=False,
)
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/authn.py b/src/saml2/authn.py
index 1f2d02cf..1e1a220b 100644
--- a/src/saml2/authn.py
+++ b/src/saml2/authn.py
@@ -146,7 +146,8 @@ class UsernamePasswordMako(UserAuthnMethod):
return resp
def _verify(self, pwd, user):
- assert is_equal(pwd, self.passwd[user])
+ if not is_equal(pwd, self.passwd[user]):
+ raise ValueError("Wrong password")
def verify(self, request, **kwargs):
"""
@@ -176,7 +177,7 @@ class UsernamePasswordMako(UserAuthnMethod):
return_to = create_return_url(self.return_to, _dict["query"][0],
**{self.query_param: "true"})
resp = Redirect(return_to, headers=[cookie])
- except (AssertionError, KeyError):
+ except (ValueError, KeyError):
resp = Unauthorized("Unknown user or wrong password")
return resp
diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py
index 50b457d1..f8704c20 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
@@ -108,17 +113,30 @@ class Base(Entity):
else:
self.state = state_cache
- self.logout_requests_signed = False
- self.allow_unsolicited = False
- self.authn_requests_signed = False
- self.want_assertions_signed = False
- self.want_response_signed = False
- for foo in ["allow_unsolicited", "authn_requests_signed",
- "logout_requests_signed", "want_assertions_signed",
- "want_response_signed"]:
- v = self.config.getattr(foo, "sp")
- if v is True or v == 'true':
- setattr(self, foo, True)
+ attribute_defaults = {
+ "logout_requests_signed": False,
+ "allow_unsolicited": False,
+ "authn_requests_signed": False,
+ "want_assertions_signed": False,
+ "want_response_signed": True,
+ }
+
+ for attr, val_default in attribute_defaults.items():
+ val_config = self.config.getattr(attr, "sp")
+ if val_config is None:
+ val = val_default
+ else:
+ val = val_config
+
+ if val == 'true':
+ val = True
+
+ setattr(self, attr, val)
+
+ if self.entity_type == "sp" and not any([self.want_assertions_signed,
+ self.want_response_signed]):
+ logger.warning("The SAML service provider accepts unsigned SAML Responses " +
+ "and Assertions. This configuration is insecure.")
self.artifact2response = {}
@@ -347,6 +365,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/httputil.py b/src/saml2/httputil.py
index 0901f7b0..0e7f32a6 100644
--- a/src/saml2/httputil.py
+++ b/src/saml2/httputil.py
@@ -62,6 +62,9 @@ class Response(object):
return [mte.render(**argv)]
else:
if isinstance(message, six.string_types):
+ # Note(JP): A WSGI app should always respond
+ # with bytes, so at this point the message should
+ # become encoded instead of passing a text object.
return [message]
elif isinstance(message, six.binary_type):
return [message]
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/response.py b/src/saml2/response.py
index 5ca75bf1..6de8723b 100644
--- a/src/saml2/response.py
+++ b/src/saml2/response.py
@@ -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/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/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_40_sigver.py b/tests/test_40_sigver.py
index 48cf19f2..2511efc4 100644
--- a/tests/test_40_sigver.py
+++ b/tests/test_40_sigver.py
@@ -18,8 +18,7 @@ from saml2.saml import EncryptedAssertion
from saml2.samlp import response_from_string
from saml2.s_utils import factory, do_attribute_statement
-#from pyasn1.codec.der import decoder
-
+import pytest
from py.test import raises
from pathutils import full_path
@@ -69,6 +68,10 @@ Yj4cAafWaYfjBU2zi1ElwStIaJ5nyp/s/8B8SAPK2T79McMyccP3wSW13LHkmM1j
wKe3ACFXBvqGQN0IbcH49hu0FKhYFM/GPDJcIHFBsiyMBXChpye9vBaTNEBCtU3K
jjyG0hRT2mAQ9h+bkPmOvlEo/aH0xR68Z9hw4PF13w=="""
+try:
+ from pyasn1.codec.der import decoder
+except ImportError:
+ decoder = None
def test_cert_from_instance_1():
@@ -81,16 +84,18 @@ def test_cert_from_instance_1():
assert certs[0] == CERT1
-# def test_cert_from_instance_ssp():
-# xml_response = open(SIMPLE_SAML_PHP_RESPONSE).read()
-# response = samlp.response_from_string(xml_response)
-# assertion = response.assertion[0]
-# certs = sigver.cert_from_instance(assertion)
-# assert len(certs) == 1
-# assert certs[0] == CERT_SSP
-# der = base64.b64decode(certs[0])
-# print(str(decoder.decode(der)).replace('.', "\n."))
-# assert decoder.decode(der)
+@pytest.mark.skipif(not decoder,
+ reason="pyasn1 is not installed")
+def test_cert_from_instance_ssp():
+ xml_response = open(SIMPLE_SAML_PHP_RESPONSE).read()
+ response = samlp.response_from_string(xml_response)
+ assertion = response.assertion[0]
+ certs = sigver.cert_from_instance(assertion)
+ assert len(certs) == 1
+ assert certs[0] == CERT_SSP
+ der = base64.b64decode(certs[0])
+ print(str(decoder.decode(der)).replace('.', "\n."))
+ assert decoder.decode(der)
class FakeConfig():
diff --git a/tests/test_51_client.py b/tests/test_51_client.py
index 937e0e20..d72d8895 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")
@@ -389,6 +405,7 @@ class TestClient:
destination="http://lingon.catalogix.se:8087/",
sp_entity_id="urn:mace:example.com:saml:roland:sp",
name_id_policy=nameid_policy,
+ sign_response=True,
userid="foba0001@example.com",
authn=AUTHN)
@@ -433,6 +450,7 @@ class TestClient:
in_response_to="id2",
destination="http://lingon.catalogix.se:8087/",
sp_entity_id="urn:mace:example.com:saml:roland:sp",
+ sign_response=True,
name_id_policy=nameid_policy,
userid="also0001@example.com",
authn=AUTHN)
@@ -889,7 +907,6 @@ class TestClient:
node_id=assertion.id)
sigass = rm_xmltag(sigass)
-
response = sigver.response_factory(
in_response_to="_012345",
destination="http://lingon.catalogix.se:8087/",
@@ -912,6 +929,8 @@ class TestClient:
resp_str = base64.encodestring(enctext.encode('utf-8'))
# Now over to the client side
+ # Explicitely allow unsigned responses for this and the following 2 tests
+ self.client.want_response_signed = False
resp = self.client.parse_authn_request_response(
resp_str, BINDING_HTTP_POST,
{"_012345": "http://foo.example.com/service"})
@@ -1313,6 +1332,9 @@ class TestClient:
def test_signed_redirect(self):
+ # Revert configuration change to disallow unsinged responses
+ self.client.want_response_signed = True
+
msg_str = "%s" % self.client.create_authn_request(
"http://localhost:8088/sso", message_id="id1")[1]
@@ -1398,7 +1420,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
@@ -1428,7 +1450,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
@@ -1525,7 +1547,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])
@@ -1543,7 +1565,9 @@ class TestClientWithDummy():
response = self.client.send(**http_args)
print(response.text)
- _dic = unpack_form(response.text[3], "SAMLResponse")
+ _dic = unpack_form(response.text, "SAMLResponse")
+ # Explicitly allow unsigned responses for this test
+ self.client.want_response_signed = False
resp = self.client.parse_authn_request_response(_dic["SAMLResponse"],
BINDING_HTTP_POST,
{sid: "/"})
@@ -1558,7 +1582,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
@@ -1578,7 +1602,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_60_sp.py b/tests/test_60_sp.py
index 6448d6d8..78e88400 100644
--- a/tests/test_60_sp.py
+++ b/tests/test_60_sp.py
@@ -2,12 +2,17 @@
# -*- coding: utf-8 -*-
import base64
+import pytest
from saml2.authn_context import INTERNETPROTOCOLPASSWORD
from saml2.saml import NAMEID_FORMAT_TRANSIENT
from saml2.samlp import NameIDPolicy
-from saml2.s2repoze.plugins.sp import make_plugin
from saml2.server import Server
+try:
+ from saml2.s2repoze.plugins.sp import make_plugin
+except ImportError:
+ make_plugin = None
+
ENV1 = {'SERVER_SOFTWARE': 'CherryPy/3.1.2 WSGI Server',
'SCRIPT_NAME': '',
'ACTUAL_SERVER_PROTOCOL': 'HTTP/1.1',
@@ -43,9 +48,13 @@ AUTHN = {
}
+@pytest.mark.skipif(not make_plugin,
+ reason="s2repoze dependencies not installed")
class TestSP():
def setup_class(self):
self.sp = make_plugin("rem", saml_conf="server_conf")
+ # Explicitly allow unsigned responses for this test
+ self.sp.saml_client.want_response_signed = False
self.server = Server(config_file="idp_conf")
def teardown_class(self):
diff --git a/tests/test_63_ecp.py b/tests/test_63_ecp.py
index 32a1aaed..0db36003 100644
--- a/tests/test_63_ecp.py
+++ b/tests/test_63_ecp.py
@@ -136,7 +136,8 @@ def test_complete_flow():
assert inst.text == "XYZ"
# parse the response
-
+ # Explicitly allow unsigned responses for this test
+ sp.want_response_signed = False
resp = sp.parse_authn_request_response(respdict["body"], None, {sid: "/"})
print(resp.response)
diff --git a/tests/test_65_authn_query.py b/tests/test_65_authn_query.py
index 54d529f8..77ccaa9e 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)
@@ -92,6 +92,8 @@ def test_flow():
# ------- @SP ----------
xmlstr = get_msg(hinfo, binding)
+ # Explicitly allow unsigned responses for this test
+ sp.want_response_signed = False
aresp = sp.parse_authn_request_response(xmlstr, binding,
{resp.in_response_to: "/"})
diff --git a/tests/test_68_assertion_id.py b/tests/test_68_assertion_id.py
index 52959f3a..31b7e8e0 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)
@@ -78,7 +78,8 @@ def test_basic_flow():
# --------- @SP -------------
xmlstr = get_msg(hinfo, binding)
-
+ # Explicitly allow unsigned responses for this test
+ sp.want_response_signed = False
aresp = sp.parse_authn_request_response(xmlstr, binding,
{resp.in_response_to: "/"})
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>