summaryrefslogtreecommitdiff
path: root/src/saml2
diff options
context:
space:
mode:
Diffstat (limited to 'src/saml2')
-rw-r--r--src/saml2/algsupport.py2
-rw-r--r--src/saml2/assertion.py16
-rw-r--r--src/saml2/attribute_converter.py2
-rw-r--r--src/saml2/attributemaps/saml_uri.py17
-rw-r--r--src/saml2/client_base.py136
-rw-r--r--src/saml2/config.py10
-rw-r--r--src/saml2/ecp.py50
-rw-r--r--src/saml2/ecp_client.py29
-rw-r--r--src/saml2/entity.py7
-rw-r--r--src/saml2/extension/requested_attributes.py131
-rw-r--r--src/saml2/extension/sp_type.py54
-rw-r--r--src/saml2/mdstore.py4
-rw-r--r--src/saml2/metadata.py12
-rw-r--r--src/saml2/pack.py49
-rw-r--r--src/saml2/profile/samlec.py14
-rw-r--r--src/saml2/response.py5
-rw-r--r--src/saml2/saml.py8
-rw-r--r--src/saml2/validate.py14
-rw-r--r--src/saml2/xmldsig/__init__.py2
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'