diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/saml2/algsupport.py | 2 | ||||
-rw-r--r-- | src/saml2/assertion.py | 16 | ||||
-rw-r--r-- | src/saml2/attribute_converter.py | 2 | ||||
-rw-r--r-- | src/saml2/attributemaps/saml_uri.py | 17 | ||||
-rw-r--r-- | src/saml2/client_base.py | 136 | ||||
-rw-r--r-- | src/saml2/config.py | 10 | ||||
-rw-r--r-- | src/saml2/ecp.py | 50 | ||||
-rw-r--r-- | src/saml2/ecp_client.py | 29 | ||||
-rw-r--r-- | src/saml2/entity.py | 7 | ||||
-rw-r--r-- | src/saml2/extension/requested_attributes.py | 131 | ||||
-rw-r--r-- | src/saml2/extension/sp_type.py | 54 | ||||
-rw-r--r-- | src/saml2/mdstore.py | 4 | ||||
-rw-r--r-- | src/saml2/metadata.py | 12 | ||||
-rw-r--r-- | src/saml2/pack.py | 49 | ||||
-rw-r--r-- | src/saml2/profile/samlec.py | 14 | ||||
-rw-r--r-- | src/saml2/response.py | 5 | ||||
-rw-r--r-- | src/saml2/saml.py | 8 | ||||
-rw-r--r-- | src/saml2/validate.py | 14 | ||||
-rw-r--r-- | src/saml2/xmldsig/__init__.py | 2 |
19 files changed, 457 insertions, 105 deletions
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' |