Skip to content

Commit 53e527d

Browse files
authored
Merge pull request #437 from c00kiemon5ter/feature-eidas-support
Add support for eIDAS SAML profile
2 parents 2326962 + 20c9610 commit 53e527d

13 files changed

+439
-26
lines changed

src/saml2/attributemaps/saml_uri.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,19 @@
1313
SIS = 'urn:oid:1.2.752.194.10.2.'
1414
UMICH = 'urn:oid:1.3.6.1.4.1.250.1.57.'
1515
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
16+
EIDAS_NATURALPERSON = 'http://eidas.europa.eu/attributes/naturalperson/'
1617

1718
MAP = {
1819
'identifier': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
1920
'fro': {
21+
EIDAS_NATURALPERSON+'PersonIdentifier': 'PersonIdentifier',
22+
EIDAS_NATURALPERSON+'FamilyName': 'FamilyName',
23+
EIDAS_NATURALPERSON+'FirstName': 'FirstName',
24+
EIDAS_NATURALPERSON+'DateOfBirth': 'DateOfBirth',
25+
EIDAS_NATURALPERSON+'BirthName': 'BirthName',
26+
EIDAS_NATURALPERSON+'PlaceOfBirth': 'PlaceOfBirth',
27+
EIDAS_NATURALPERSON+'CurrentAddress': 'CurrentAddress',
28+
EIDAS_NATURALPERSON+'Gender': 'Gender',
2029
EDUCOURSE_OID+'1': 'eduCourseOffering',
2130
EDUCOURSE_OID+'2': 'eduCourseMember',
2231
EDUMEMBER1_OID+'1': 'isMemberOf',
@@ -161,6 +170,14 @@
161170
X500ATTR_OID+'65': 'pseudonym',
162171
},
163172
'to': {
173+
'PersonIdentifier': EIDAS_NATURALPERSON+'PersonIdentifier',
174+
'FamilyName': EIDAS_NATURALPERSON+'FamilyName',
175+
'FirstName': EIDAS_NATURALPERSON+'FirstName',
176+
'DateOfBirth': EIDAS_NATURALPERSON+'DateOfBirth',
177+
'BirthName': EIDAS_NATURALPERSON+'BirthName',
178+
'PlaceOfBirth': EIDAS_NATURALPERSON+'PlaceOfBirth',
179+
'CurrentAddress': EIDAS_NATURALPERSON+'CurrentAddress',
180+
'Gender': EIDAS_NATURALPERSON+'Gender',
164181
'associatedDomain': UCL_DIR_PILOT+'37',
165182
'authorityRevocationList': X500ATTR_OID+'38',
166183
'businessCategory': X500ATTR_OID+'15',

src/saml2/client_base.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
from saml2.entity import Entity
1212

13+
import saml2.attributemaps as attributemaps
14+
1315
from saml2.mdstore import destinations
1416
from saml2.profile import paos, ecp
1517
from saml2.saml import NAMEID_FORMAT_TRANSIENT
@@ -18,6 +20,9 @@
1820
from saml2.samlp import AttributeQuery
1921
from saml2.samlp import AuthzDecisionQuery
2022
from saml2.samlp import AuthnRequest
23+
from saml2.samlp import Extensions
24+
from saml2.extension import sp_type
25+
from saml2.extension import requested_attributes
2126

2227
import saml2
2328
import time
@@ -347,6 +352,67 @@ def create_authn_request(self, destination, vorg="", scoping=None,
347352
if force_authn:
348353
args['force_authn'] = 'true'
349354

355+
conf_sp_type = self.config.getattr('sp_type', 'sp')
356+
conf_sp_type_in_md = self.config.getattr('sp_type_in_metadata', 'sp')
357+
if conf_sp_type and conf_sp_type_in_md is False:
358+
if not extensions:
359+
extensions = Extensions()
360+
item = sp_type.SPType(text=conf_sp_type)
361+
extensions.add_extension_element(item)
362+
363+
requested_attrs = self.config.getattr('requested_attributes', 'sp')
364+
if requested_attrs:
365+
if not extensions:
366+
extensions = Extensions()
367+
368+
attributemapsmods = []
369+
for modname in attributemaps.__all__:
370+
attributemapsmods.append(getattr(attributemaps, modname))
371+
372+
items = []
373+
for attr in requested_attrs:
374+
friendly_name = attr.get('friendly_name')
375+
name = attr.get('name')
376+
name_format = attr.get('name_format')
377+
is_required = str(attr.get('required', False)).lower()
378+
379+
if not name and not friendly_name:
380+
raise ValueError(
381+
"Missing required attribute: '{}' or '{}'".format(
382+
'name', 'friendly_name'))
383+
384+
if not name:
385+
for mod in attributemapsmods:
386+
try:
387+
name = mod.MAP['to'][friendly_name]
388+
except KeyError:
389+
continue
390+
else:
391+
if not name_format:
392+
name_format = mod.MAP['identifier']
393+
break
394+
395+
if not friendly_name:
396+
for mod in attributemapsmods:
397+
try:
398+
friendly_name = mod.MAP['fro'][name]
399+
except KeyError:
400+
continue
401+
else:
402+
if not name_format:
403+
name_format = mod.MAP['identifier']
404+
break
405+
406+
items.append(requested_attributes.RequestedAttribute(
407+
is_required=is_required,
408+
name_format=name_format,
409+
friendly_name=friendly_name,
410+
name=name))
411+
412+
item = requested_attributes.RequestedAttributes(
413+
extension_elements=items)
414+
extensions.add_extension_element(item)
415+
350416
if kwargs:
351417
_args, extensions = self._filter_args(AuthnRequest(), extensions,
352418
**kwargs)

src/saml2/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@
7878
"requested_attribute_name_format",
7979
"hide_assertion_consumer_service",
8080
"force_authn",
81+
"sp_type",
82+
"sp_type_in_metadata",
83+
"requested_attributes",
8184
]
8285

8386
AA_IDP_ARGS = [
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python
2+
3+
#
4+
# Generated Tue Jul 18 14:58:29 2017 by parse_xsd.py version 0.5.
5+
#
6+
7+
import saml2
8+
from saml2 import SamlBase
9+
10+
from saml2 import saml
11+
12+
13+
NAMESPACE = 'http://eidas.europa.eu/saml-extensions'
14+
15+
class RequestedAttributeType_(SamlBase):
16+
"""The http://eidas.europa.eu/saml-extensions:RequestedAttributeType element """
17+
18+
c_tag = 'RequestedAttributeType'
19+
c_namespace = NAMESPACE
20+
c_children = SamlBase.c_children.copy()
21+
c_attributes = SamlBase.c_attributes.copy()
22+
c_child_order = SamlBase.c_child_order[:]
23+
c_cardinality = SamlBase.c_cardinality.copy()
24+
c_children['{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue'] = ('attribute_value', [saml.AttributeValue])
25+
c_cardinality['attribute_value'] = {"min":0}
26+
c_attributes['Name'] = ('name', 'None', True)
27+
c_attributes['NameFormat'] = ('name_format', 'None', True)
28+
c_attributes['FriendlyName'] = ('friendly_name', 'None', False)
29+
c_attributes['isRequired'] = ('is_required', 'None', False)
30+
c_child_order.extend(['attribute_value'])
31+
32+
def __init__(self,
33+
attribute_value=None,
34+
name=None,
35+
name_format=None,
36+
friendly_name=None,
37+
is_required=None,
38+
text=None,
39+
extension_elements=None,
40+
extension_attributes=None,
41+
):
42+
SamlBase.__init__(self,
43+
text=text,
44+
extension_elements=extension_elements,
45+
extension_attributes=extension_attributes,
46+
)
47+
self.attribute_value=attribute_value or []
48+
self.name=name
49+
self.name_format=name_format
50+
self.friendly_name=friendly_name
51+
self.is_required=is_required
52+
53+
def requested_attribute_type__from_string(xml_string):
54+
return saml2.create_class_from_xml_string(RequestedAttributeType_, xml_string)
55+
56+
57+
class RequestedAttribute(RequestedAttributeType_):
58+
"""The http://eidas.europa.eu/saml-extensions:RequestedAttribute element """
59+
60+
c_tag = 'RequestedAttribute'
61+
c_namespace = NAMESPACE
62+
c_children = RequestedAttributeType_.c_children.copy()
63+
c_attributes = RequestedAttributeType_.c_attributes.copy()
64+
c_child_order = RequestedAttributeType_.c_child_order[:]
65+
c_cardinality = RequestedAttributeType_.c_cardinality.copy()
66+
67+
def requested_attribute_from_string(xml_string):
68+
return saml2.create_class_from_xml_string(RequestedAttribute, xml_string)
69+
70+
71+
class RequestedAttributesType_(SamlBase):
72+
"""The http://eidas.europa.eu/saml-extensions:RequestedAttributesType element """
73+
74+
c_tag = 'RequestedAttributesType'
75+
c_namespace = NAMESPACE
76+
c_children = SamlBase.c_children.copy()
77+
c_attributes = SamlBase.c_attributes.copy()
78+
c_child_order = SamlBase.c_child_order[:]
79+
c_cardinality = SamlBase.c_cardinality.copy()
80+
c_children['{http://eidas.europa.eu/saml-extensions}RequestedAttribute'] = ('requested_attribute', [RequestedAttribute])
81+
c_cardinality['requested_attribute'] = {"min":0}
82+
c_child_order.extend(['requested_attribute'])
83+
84+
def __init__(self,
85+
requested_attribute=None,
86+
text=None,
87+
extension_elements=None,
88+
extension_attributes=None,
89+
):
90+
SamlBase.__init__(self,
91+
text=text,
92+
extension_elements=extension_elements,
93+
extension_attributes=extension_attributes,
94+
)
95+
self.requested_attribute=requested_attribute or []
96+
97+
def requested_attributes_type__from_string(xml_string):
98+
return saml2.create_class_from_xml_string(RequestedAttributesType_, xml_string)
99+
100+
101+
class RequestedAttributes(RequestedAttributesType_):
102+
"""The http://eidas.europa.eu/saml-extensions:RequestedAttributes element """
103+
104+
c_tag = 'RequestedAttributes'
105+
c_namespace = NAMESPACE
106+
c_children = RequestedAttributesType_.c_children.copy()
107+
c_attributes = RequestedAttributesType_.c_attributes.copy()
108+
c_child_order = RequestedAttributesType_.c_child_order[:]
109+
c_cardinality = RequestedAttributesType_.c_cardinality.copy()
110+
111+
def requested_attributes_from_string(xml_string):
112+
return saml2.create_class_from_xml_string(RequestedAttributes, xml_string)
113+
114+
115+
ELEMENT_FROM_STRING = {
116+
RequestedAttributes.c_tag: requested_attributes_from_string,
117+
RequestedAttributesType_.c_tag: requested_attributes_type__from_string,
118+
RequestedAttribute.c_tag: requested_attribute_from_string,
119+
RequestedAttributeType_.c_tag: requested_attribute_type__from_string,
120+
}
121+
122+
ELEMENT_BY_TAG = {
123+
'RequestedAttributes': RequestedAttributes,
124+
'RequestedAttributesType': RequestedAttributesType_,
125+
'RequestedAttribute': RequestedAttribute,
126+
'RequestedAttributeType': RequestedAttributeType_,
127+
}
128+
129+
130+
def factory(tag, **kwargs):
131+
return ELEMENT_BY_TAG[tag](**kwargs)

src/saml2/extension/sp_type.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env python
2+
3+
#
4+
# Generated Tue Jul 18 15:03:44 2017 by parse_xsd.py version 0.5.
5+
#
6+
7+
import saml2
8+
from saml2 import SamlBase
9+
10+
11+
NAMESPACE = 'http://eidas.europa.eu/saml-extensions'
12+
13+
class SPTypeType_(SamlBase):
14+
"""The http://eidas.europa.eu/saml-extensions:SPTypeType element """
15+
16+
c_tag = 'SPTypeType'
17+
c_namespace = NAMESPACE
18+
c_value_type = {'base': 'xsd:string', 'enumeration': ['public', 'private']}
19+
c_children = SamlBase.c_children.copy()
20+
c_attributes = SamlBase.c_attributes.copy()
21+
c_child_order = SamlBase.c_child_order[:]
22+
c_cardinality = SamlBase.c_cardinality.copy()
23+
24+
def sp_type_type__from_string(xml_string):
25+
return saml2.create_class_from_xml_string(SPTypeType_, xml_string)
26+
27+
28+
class SPType(SPTypeType_):
29+
"""The http://eidas.europa.eu/saml-extensions:SPType element """
30+
31+
c_tag = 'SPType'
32+
c_namespace = NAMESPACE
33+
c_children = SPTypeType_.c_children.copy()
34+
c_attributes = SPTypeType_.c_attributes.copy()
35+
c_child_order = SPTypeType_.c_child_order[:]
36+
c_cardinality = SPTypeType_.c_cardinality.copy()
37+
38+
def sp_type_from_string(xml_string):
39+
return saml2.create_class_from_xml_string(SPType, xml_string)
40+
41+
42+
ELEMENT_FROM_STRING = {
43+
SPType.c_tag: sp_type_from_string,
44+
SPTypeType_.c_tag: sp_type_type__from_string,
45+
}
46+
47+
ELEMENT_BY_TAG = {
48+
'SPType': SPType,
49+
'SPTypeType': SPTypeType_,
50+
}
51+
52+
53+
def factory(tag, **kwargs):
54+
return ELEMENT_BY_TAG[tag](**kwargs)

src/saml2/metadata.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from saml2.extension import idpdisc
1010
from saml2.extension import shibmd
1111
from saml2.extension import mdattr
12+
from saml2.extension import sp_type
1213
from saml2.saml import NAME_FORMAT_URI
1314
from saml2.saml import AttributeValue
1415
from saml2.saml import Attribute
@@ -722,7 +723,8 @@ def entity_descriptor(confd):
722723
entd.contact_person = do_contact_person_info(confd.contact_person)
723724

724725
if confd.entity_category:
725-
entd.extensions = md.Extensions()
726+
if not entd.extensions:
727+
entd.extensions = md.Extensions()
726728
ava = [AttributeValue(text=c) for c in confd.entity_category]
727729
attr = Attribute(attribute_value=ava,
728730
name="http://macedir.org/entity-category")
@@ -734,6 +736,14 @@ def entity_descriptor(confd):
734736
entd.extensions = md.Extensions()
735737
entd.extensions.add_extension_element(item)
736738

739+
conf_sp_type = confd.getattr('sp_type', 'sp')
740+
conf_sp_type_in_md = confd.getattr('sp_type_in_metadata', 'sp')
741+
if conf_sp_type and conf_sp_type_in_md is True:
742+
if not entd.extensions:
743+
entd.extensions = md.Extensions()
744+
item = sp_type.SPType(text=conf_sp_type)
745+
entd.extensions.add_extension_element(item)
746+
737747
serves = confd.serves
738748
if not serves:
739749
raise SAMLError(

tests/server_conf.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@
1414
"required_attributes": ["surName", "givenName", "mail"],
1515
"optional_attributes": ["title"],
1616
"idp": ["urn:mace:example.com:saml:roland:idp"],
17+
"requested_attributes": [
18+
{
19+
"name": "http://eidas.europa.eu/attributes/naturalperson/DateOfBirth",
20+
"required": False,
21+
},
22+
{
23+
"friendly_name": "PersonIdentifier",
24+
"required": True,
25+
},
26+
{
27+
"friendly_name": "PlaceOfBirth",
28+
},
29+
],
1730
}
1831
},
1932
"debug": 1,

tests/sp_mdext_conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"description": "My own SP",
77
"service": {
88
"sp": {
9+
"sp_type": "public",
10+
"sp_type_in_metadata": True,
911
"endpoints": {
1012
"assertion_consumer_service": [
1113
"http://lingon.catalogix.se:8087/"],

0 commit comments

Comments
 (0)