Skip to content

Commit cc1c856

Browse files
Merge pull request #691 from IdentityPython/feat-requested-attributes-per-request
Set eIDAS RequestedAttributes per AuthnRequest
2 parents 3e5373d + 17b03f3 commit cc1c856

File tree

4 files changed

+92
-56
lines changed

4 files changed

+92
-56
lines changed

src/saml2/attribute_converter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def ac_factory(path=""):
6262
if path not in sys.path:
6363
sys.path.insert(0, path)
6464

65-
for fil in os.listdir(path):
65+
for fil in sorted(os.listdir(path)):
6666
if fil.endswith(".py"):
6767
mod = import_module(fil[:-3])
6868
for key, item in mod.__dict__.items():

src/saml2/client_base.py

Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212

1313
from saml2.entity import Entity
1414

15-
import saml2.attributemaps as attributemaps
16-
1715
from saml2.mdstore import destinations
1816
from saml2.profile import paos, ecp
1917
from saml2.saml import NAMEID_FORMAT_TRANSIENT
@@ -24,7 +22,8 @@
2422
from saml2.samlp import AuthnRequest
2523
from saml2.samlp import Extensions
2624
from saml2.extension import sp_type
27-
from saml2.extension import requested_attributes
25+
from saml2.extension.requested_attributes import RequestedAttribute
26+
from saml2.extension.requested_attributes import RequestedAttributes
2827

2928
import saml2
3029
from saml2.soap import make_soap_enveloped_saml_thingy
@@ -235,7 +234,7 @@ def create_authn_request(self, destination, vorg="", scoping=None,
235234
service_url_binding=None, message_id=0,
236235
consent=None, extensions=None, sign=None,
237236
allow_create=None, sign_prepare=False, sign_alg=None,
238-
digest_alg=None, **kwargs):
237+
digest_alg=None, requested_attributes=None, **kwargs):
239238
""" Creates an authentication request.
240239
241240
:param destination: Where the request should be sent.
@@ -253,6 +252,11 @@ def create_authn_request(self, destination, vorg="", scoping=None,
253252
:param allow_create: If the identity provider is allowed, in the course
254253
of fulfilling the request, to create a new identifier to represent
255254
the principal.
255+
:param requested_attributes: A list of dicts which define attributes to
256+
be used as eIDAS Requested Attributes for this request. If not
257+
defined the configuration option requested_attributes will be used,
258+
if defined. The format is the same as the requested_attributes
259+
configuration option.
256260
:param kwargs: Extra key word arguments
257261
:return: either a tuple of request ID and <samlp:AuthnRequest> instance
258262
or a tuple of request ID and str when sign is set to True
@@ -379,58 +383,62 @@ def create_authn_request(self, destination, vorg="", scoping=None,
379383
item = sp_type.SPType(text=conf_sp_type)
380384
extensions.add_extension_element(item)
381385

382-
requested_attrs = self.config.getattr('requested_attributes', 'sp')
383-
if requested_attrs:
384-
if not extensions:
385-
extensions = Extensions()
386+
requested_attrs = (
387+
requested_attributes
388+
or self.config.getattr('requested_attributes', 'sp')
389+
or []
390+
)
391+
392+
if not extensions:
393+
extensions = Extensions()
394+
395+
items = []
396+
for attr in requested_attrs:
397+
friendly_name = attr.get('friendly_name')
398+
name = attr.get('name')
399+
name_format = attr.get('name_format')
400+
is_required = str(attr.get('required', False)).lower()
401+
402+
if not name and not friendly_name:
403+
raise ValueError(
404+
"Missing required attribute: '{}' or '{}'".format(
405+
'name', 'friendly_name'
406+
)
407+
)
408+
409+
if not name:
410+
for converter in self.config.attribute_converters:
411+
try:
412+
name = converter._to[friendly_name.lower()]
413+
except KeyError:
414+
continue
415+
else:
416+
if not name_format:
417+
name_format = converter.name_format
418+
break
386419

387-
attributemapsmods = []
388-
for modname in attributemaps.__all__:
389-
attributemapsmods.append(getattr(attributemaps, modname))
390-
391-
items = []
392-
for attr in requested_attrs:
393-
friendly_name = attr.get('friendly_name')
394-
name = attr.get('name')
395-
name_format = attr.get('name_format')
396-
is_required = str(attr.get('required', False)).lower()
397-
398-
if not name and not friendly_name:
399-
raise ValueError(
400-
"Missing required attribute: '{}' or '{}'".format(
401-
'name', 'friendly_name'))
402-
403-
if not name:
404-
for mod in attributemapsmods:
405-
try:
406-
name = mod.MAP['to'][friendly_name]
407-
except KeyError:
408-
continue
409-
else:
410-
if not name_format:
411-
name_format = mod.MAP['identifier']
412-
break
413-
414-
if not friendly_name:
415-
for mod in attributemapsmods:
416-
try:
417-
friendly_name = mod.MAP['fro'][name]
418-
except KeyError:
419-
continue
420-
else:
421-
if not name_format:
422-
name_format = mod.MAP['identifier']
423-
break
420+
if not friendly_name:
421+
for converter in self.config.attribute_converters:
422+
try:
423+
friendly_name = converter._fro[name.lower()]
424+
except KeyError:
425+
continue
426+
else:
427+
if not name_format:
428+
name_format = converter.name_format
429+
break
424430

425-
items.append(requested_attributes.RequestedAttribute(
431+
items.append(
432+
RequestedAttribute(
426433
is_required=is_required,
427434
name_format=name_format,
428435
friendly_name=friendly_name,
429-
name=name))
436+
name=name,
437+
)
438+
)
430439

431-
item = requested_attributes.RequestedAttributes(
432-
extension_elements=items)
433-
extensions.add_extension_element(item)
440+
item = RequestedAttributes(extension_elements=items)
441+
extensions.add_extension_element(item)
434442

435443
force_authn = str(
436444
kwargs.pop("force_authn", None)
@@ -450,8 +458,7 @@ def create_authn_request(self, destination, vorg="", scoping=None,
450458
if sign is None:
451459
sign = self.authn_requests_signed
452460

453-
if (sign and self.sec.cert_handler.generate_cert()) or \
454-
client_crt is not None:
461+
if (sign and self.sec.cert_handler.generate_cert()) or client_crt is not None:
455462
with self.lock:
456463
self.sec.cert_handler.update_cert(True, client_crt)
457464
if client_crt is not None:

tests/server_conf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616
"idp": ["urn:mace:example.com:saml:roland:idp"],
1717
"requested_attributes": [
1818
{
19-
"name": "http://eidas.europa.eu/attributes/naturalperson/DateOfBirth",
19+
"name": "urn:oid:1.3.6.1.4.1.5923.1.1.1.2",
2020
"required": False,
2121
},
2222
{
23-
"friendly_name": "PersonIdentifier",
23+
"friendly_name": "eduPersonNickname",
2424
"required": True,
2525
},
2626
{
27-
"friendly_name": "PlaceOfBirth",
27+
"friendly_name": "eduPersonScopedAffiliation",
2828
},
2929
],
3030
}

tests/test_51_client.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,35 @@ def test_create_auth_request_0(self):
286286
assert c.attributes['FriendlyName']
287287
assert c.attributes['NameFormat']
288288

289+
def test_create_auth_request_requested_attributes(self):
290+
req_attr = [{"friendly_name": "eduPersonOrgUnitDN", "required": True}]
291+
ar_id, ar = self.client.create_authn_request(
292+
"http://www.example.com/sso",
293+
message_id="id1",
294+
requested_attributes=req_attr
295+
)
296+
297+
req_attrs_nodes = (
298+
e
299+
for e in ar.extensions.extension_elements
300+
if e.tag == RequestedAttributes.c_tag
301+
)
302+
req_attrs_node = next(req_attrs_nodes, None)
303+
assert req_attrs_node is not None
304+
305+
attrs = (
306+
child
307+
for child in req_attrs_node.children
308+
if child.friendly_name == "eduPersonOrgUnitDN"
309+
)
310+
attr = next(attrs, None)
311+
assert attr is not None
312+
assert attr.c_tag == RequestedAttribute.c_tag
313+
assert attr.is_required == 'true'
314+
assert attr.name == 'urn:mace:dir:attribute-def:eduPersonOrgUnitDN'
315+
assert attr.friendly_name == 'eduPersonOrgUnitDN'
316+
assert attr.name_format == 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic'
317+
289318
def test_create_auth_request_unset_force_authn_by_default(self):
290319
req_id, req = self.client.create_authn_request(
291320
"http://www.example.com/sso", sign=False, message_id="id1"

0 commit comments

Comments
 (0)