Skip to content

Commit 9f30f2f

Browse files
Merge pull request #807 from pandafy/issues/806-requested-authn-context
Adds option to configure RequestedAuthnContext
2 parents 5583f16 + d1a11db commit 9f30f2f

File tree

6 files changed

+116
-24
lines changed

6 files changed

+116
-24
lines changed

docs/howto/config.rst

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ ca_certs
342342
This is the path to a file containing root CA certificates for SSL server certificate validation.
343343

344344
Example::
345-
345+
346346
"ca_certs": full_path("cacerts.txt"),
347347

348348

@@ -1222,6 +1222,34 @@ Example::
12221222
"requested_attribute_name_format": NAME_FORMAT_BASIC
12231223

12241224

1225+
requested_authn_context
1226+
"""""""""""""""""""""""
1227+
1228+
This configuration option defines the ``<RequestedAuthnContext>`` for an AuthnRequest by
1229+
a client. The value is a dictionary with two fields
1230+
1231+
- ``authn_context_class_ref`` a list of string values representing
1232+
``<AuthnContextClassRef>`` elements.
1233+
1234+
- ``comparison`` a string representing the Comparison xml-attribute value of the
1235+
``<RequestedAuthnContext>`` element. Per the SAML core specificiation the value should
1236+
be one of "exact", "minimum", "maximum", or "better". The default is "exact".
1237+
1238+
Example::
1239+
1240+
"service": {
1241+
"sp": {
1242+
"requested_authn_context": {
1243+
"authn_context_class_ref": [
1244+
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
1245+
"urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient",
1246+
],
1247+
"comparison": "minimum",
1248+
}
1249+
}
1250+
}
1251+
1252+
12251253
idp/aa/sp
12261254
^^^^^^^^^
12271255

src/saml2/client_base.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
from saml2.profile import paos, ecp
1818
from saml2.saml import NAMEID_FORMAT_PERSISTENT
1919
from saml2.saml import NAMEID_FORMAT_TRANSIENT
20-
from saml2.samlp import AuthnQuery, RequestedAuthnContext
20+
from saml2.saml import AuthnContextClassRef
21+
from saml2.samlp import AuthnQuery
22+
from saml2.samlp import RequestedAuthnContext
2123
from saml2.samlp import NameIDMappingRequest
2224
from saml2.samlp import AttributeQuery
2325
from saml2.samlp import AuthzDecisionQuery
@@ -358,18 +360,36 @@ def create_authn_request(
358360
provider_name = self._my_name()
359361
args["provider_name"] = provider_name
360362

363+
requested_authn_context = (
364+
kwargs.pop("requested_authn_context", None)
365+
or self.config.getattr("requested_authn_context", "sp")
366+
or {}
367+
)
368+
requested_authn_context_accrs = requested_authn_context.get(
369+
"authn_context_class_ref", []
370+
)
371+
requested_authn_context_comparison = requested_authn_context.get(
372+
"comparison", "exact"
373+
)
374+
if requested_authn_context_accrs:
375+
args["requested_authn_context"] = RequestedAuthnContext(
376+
authn_context_class_ref=[
377+
AuthnContextClassRef(accr)
378+
for accr in requested_authn_context_accrs
379+
],
380+
comparison=requested_authn_context_comparison,
381+
)
382+
361383
# Allow argument values either as class instances or as dictionaries
362384
# all of these have cardinality 0..1
363385
_msg = AuthnRequest()
364-
for param in ["scoping", "requested_authn_context", "conditions", "subject"]:
386+
for param in ["scoping", "conditions", "subject"]:
365387
_item = kwargs.pop(param, None)
366388
if not _item:
367389
continue
368390

369391
if isinstance(_item, _msg.child_class(param)):
370392
args[param] = _item
371-
elif isinstance(_item, dict):
372-
args[param] = RequestedAuthnContext(**_item)
373393
else:
374394
raise ValueError("Wrong type for param {name}".format(name=param))
375395

src/saml2/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"sp_type",
105105
"sp_type_in_metadata",
106106
"requested_attributes",
107+
"requested_authn_context",
107108
]
108109

109110
AA_IDP_ARGS = [

tests/servera_conf.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from saml2 import BINDING_HTTP_POST
55
from saml2 import BINDING_HTTP_REDIRECT
66
from saml2 import BINDING_HTTP_ARTIFACT
7+
from saml2.authn_context import PASSWORDPROTECTEDTRANSPORT as AUTHN_PASSWORD_PROTECTED
8+
from saml2.authn_context import TIMESYNCTOKEN as AUTHN_TIME_SYNC_TOKEN
79
from saml2.saml import NAMEID_FORMAT_TRANSIENT
810
from saml2.saml import NAMEID_FORMAT_PERSISTENT
911

@@ -42,8 +44,17 @@
4244
"required_attributes": ["surName", "givenName", "mail"],
4345
"optional_attributes": ["title", "eduPersonAffiliation"],
4446
"idp": ["urn:mace:example.com:saml:roland:idp"],
45-
"name_id_format": [NAMEID_FORMAT_TRANSIENT,
46-
NAMEID_FORMAT_PERSISTENT]
47+
"name_id_format": [
48+
NAMEID_FORMAT_TRANSIENT,
49+
NAMEID_FORMAT_PERSISTENT,
50+
],
51+
"requested_authn_context": {
52+
"authn_context_class_ref": [
53+
AUTHN_PASSWORD_PROTECTED,
54+
AUTHN_TIME_SYNC_TOKEN,
55+
],
56+
"comparison": "exact",
57+
},
4758
}
4859
},
4960
"debug": 1,

tests/test_31_config.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@
55
import logging
66
from saml2.mdstore import MetadataStore, name
77

8-
from saml2 import BINDING_HTTP_REDIRECT, BINDING_SOAP, BINDING_HTTP_POST
9-
from saml2.config import SPConfig, IdPConfig, Config
10-
8+
from saml2 import BINDING_HTTP_REDIRECT
9+
from saml2 import BINDING_SOAP
10+
from saml2.config import Config
11+
from saml2.config import IdPConfig
12+
from saml2.config import SPConfig
13+
from saml2.authn_context import PASSWORDPROTECTEDTRANSPORT as AUTHN_PASSWORD_PROTECTED
14+
from saml2.authn_context import TIMESYNCTOKEN as AUTHN_TIME_SYNC_TOKEN
1115
from saml2 import logger
1216

1317
from pathutils import dotname, full_path
1418
from saml2.sigver import security_context, CryptoBackendXMLSecurity
1519

20+
1621
sp1 = {
1722
"entityid": "urn:mace:umu.se:saml:roland:sp",
1823
"service": {
@@ -26,8 +31,15 @@
2631
"urn:mace:example.com:saml:roland:idp": {
2732
'single_sign_on_service':
2833
{'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect':
29-
'http://localhost:8088/sso/'}},
30-
}
34+
'http://localhost:8088/sso/'}},
35+
},
36+
"requested_authn_context": {
37+
"authn_context_class_ref": [
38+
AUTHN_PASSWORD_PROTECTED,
39+
AUTHN_TIME_SYNC_TOKEN,
40+
],
41+
"comparison": "exact",
42+
},
3143
}
3244
},
3345
"key_file": full_path("test.key"),
@@ -211,12 +223,23 @@ def test_1():
211223

212224
assert len(c._sp_idp) == 1
213225
assert list(c._sp_idp.keys()) == ["urn:mace:example.com:saml:roland:idp"]
214-
assert list(c._sp_idp.values()) == [{'single_sign_on_service':
215-
{
216-
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect':
217-
'http://localhost:8088/sso/'}}]
226+
assert list(c._sp_idp.values()) == [
227+
{
228+
'single_sign_on_service': {
229+
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect': (
230+
'http://localhost:8088/sso/'
231+
)
232+
}
233+
}
234+
]
218235

219236
assert c.only_use_keys_in_metadata
237+
assert type(c.getattr("requested_authn_context")) is dict
238+
assert c.getattr("requested_authn_context").get("authn_context_class_ref") == [
239+
AUTHN_PASSWORD_PROTECTED,
240+
AUTHN_TIME_SYNC_TOKEN,
241+
]
242+
assert c.getattr("requested_authn_context").get("comparison") == "exact"
220243

221244

222245
def test_2():

tests/test_71_authn_request.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from contextlib import closing
22
from saml2.client import Saml2Client
33
from saml2.server import Server
4+
from saml2.saml import AuthnContextClassRef
45

56

67
def test_authn_request_with_acs_by_index():
@@ -15,22 +16,30 @@ def test_authn_request_with_acs_by_index():
1516
# instead of AssertionConsumerServiceURL. The index with label ACS_INDEX
1617
# exists in the SP metadata in servera.xml.
1718
request_id, authn_request = sp.create_authn_request(
18-
sp.config.entityid,
19-
assertion_consumer_service_index=ACS_INDEX)
19+
sp.config.entityid, assertion_consumer_service_index=ACS_INDEX
20+
)
2021

21-
# Make sure the authn_request contains AssertionConsumerServiceIndex.
22-
acs_index = getattr(authn_request,
23-
'assertion_consumer_service_index', None)
22+
assert authn_request.requested_authn_context.authn_context_class_ref == [
23+
AuthnContextClassRef(accr)
24+
for accr in sp.config.getattr("requested_authn_context").get("authn_context_class_ref")
25+
]
26+
assert authn_request.requested_authn_context.comparison == (
27+
sp.config.getattr("requested_authn_context").get("comparison")
28+
)
2429

30+
# Make sure the authn_request contains AssertionConsumerServiceIndex.
31+
acs_index = getattr(
32+
authn_request, 'assertion_consumer_service_index', None
33+
)
2534
assert acs_index == ACS_INDEX
2635

2736
# Create IdP.
2837
with closing(Server(config_file="idp_all_conf")) as idp:
29-
3038
# Ask the IdP to pick out the binding and destination from the
3139
# authn_request.
32-
binding, destination = idp.pick_binding("assertion_consumer_service",
33-
request=authn_request)
40+
binding, destination = idp.pick_binding(
41+
"assertion_consumer_service", request=authn_request
42+
)
3443

3544
# Make sure the IdP pick_binding method picks the correct location
3645
# or destination based on the ACS index in the authn request.

0 commit comments

Comments
 (0)