Skip to content

Adds configuration directive for RequestedAuthnContext #806 #807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion docs/howto/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ ca_certs
This is the path to a file containing root CA certificates for SSL server certificate validation.

Example::

"ca_certs": full_path("cacerts.txt"),


Expand Down Expand Up @@ -1222,6 +1222,34 @@ Example::
"requested_attribute_name_format": NAME_FORMAT_BASIC


requested_authn_context
"""""""""""""""""""""""

This configuration option defines the ``<RequestedAuthnContext>`` for an AuthnRequest by
a client. The value is a dictionary with two fields

- ``authn_context_class_ref`` a list of string values representing
``<AuthnContextClassRef>`` elements.

- ``comparison`` a string representing the Comparison xml-attribute value of the
``<RequestedAuthnContext>`` element. Per the SAML core specificiation the value should
be one of "exact", "minimum", "maximum", or "better". The default is "exact".

Example::

"service": {
"sp": {
"requested_authn_context": {
"authn_context_class_ref": [
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
"urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient",
],
"comparison": "minimum",
}
}
}


idp/aa/sp
^^^^^^^^^

Expand Down
28 changes: 24 additions & 4 deletions src/saml2/client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
from saml2.profile import paos, ecp
from saml2.saml import NAMEID_FORMAT_PERSISTENT
from saml2.saml import NAMEID_FORMAT_TRANSIENT
from saml2.samlp import AuthnQuery, RequestedAuthnContext
from saml2.saml import AuthnContextClassRef
from saml2.samlp import AuthnQuery
from saml2.samlp import RequestedAuthnContext
from saml2.samlp import NameIDMappingRequest
from saml2.samlp import AttributeQuery
from saml2.samlp import AuthzDecisionQuery
Expand Down Expand Up @@ -358,18 +360,36 @@ def create_authn_request(
provider_name = self._my_name()
args["provider_name"] = provider_name

requested_authn_context = (
kwargs.pop("requested_authn_context", None)
or self.config.getattr("requested_authn_context", "sp")
or {}
)
requested_authn_context_accrs = requested_authn_context.get(
"authn_context_class_ref", []
)
requested_authn_context_comparison = requested_authn_context.get(
"comparison", "exact"
)
if requested_authn_context_accrs:
args["requested_authn_context"] = RequestedAuthnContext(
authn_context_class_ref=[
AuthnContextClassRef(accr)
for accr in requested_authn_context_accrs
],
comparison=requested_authn_context_comparison,
)

# Allow argument values either as class instances or as dictionaries
# all of these have cardinality 0..1
_msg = AuthnRequest()
for param in ["scoping", "requested_authn_context", "conditions", "subject"]:
for param in ["scoping", "conditions", "subject"]:
_item = kwargs.pop(param, None)
if not _item:
continue

if isinstance(_item, _msg.child_class(param)):
args[param] = _item
elif isinstance(_item, dict):
args[param] = RequestedAuthnContext(**_item)
else:
raise ValueError("Wrong type for param {name}".format(name=param))

Expand Down
1 change: 1 addition & 0 deletions src/saml2/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"sp_type",
"sp_type_in_metadata",
"requested_attributes",
"requested_authn_context",
]

AA_IDP_ARGS = [
Expand Down
15 changes: 13 additions & 2 deletions tests/servera_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from saml2 import BINDING_HTTP_POST
from saml2 import BINDING_HTTP_REDIRECT
from saml2 import BINDING_HTTP_ARTIFACT
from saml2.authn_context import PASSWORDPROTECTEDTRANSPORT as AUTHN_PASSWORD_PROTECTED
from saml2.authn_context import TIMESYNCTOKEN as AUTHN_TIME_SYNC_TOKEN
from saml2.saml import NAMEID_FORMAT_TRANSIENT
from saml2.saml import NAMEID_FORMAT_PERSISTENT

Expand Down Expand Up @@ -42,8 +44,17 @@
"required_attributes": ["surName", "givenName", "mail"],
"optional_attributes": ["title", "eduPersonAffiliation"],
"idp": ["urn:mace:example.com:saml:roland:idp"],
"name_id_format": [NAMEID_FORMAT_TRANSIENT,
NAMEID_FORMAT_PERSISTENT]
"name_id_format": [
NAMEID_FORMAT_TRANSIENT,
NAMEID_FORMAT_PERSISTENT,
],
"requested_authn_context": {
"authn_context_class_ref": [
AUTHN_PASSWORD_PROTECTED,
AUTHN_TIME_SYNC_TOKEN,
],
"comparison": "exact",
},
}
},
"debug": 1,
Expand Down
41 changes: 32 additions & 9 deletions tests/test_31_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
import logging
from saml2.mdstore import MetadataStore, name

from saml2 import BINDING_HTTP_REDIRECT, BINDING_SOAP, BINDING_HTTP_POST
from saml2.config import SPConfig, IdPConfig, Config

from saml2 import BINDING_HTTP_REDIRECT
from saml2 import BINDING_SOAP
from saml2.config import Config
from saml2.config import IdPConfig
from saml2.config import SPConfig
from saml2.authn_context import PASSWORDPROTECTEDTRANSPORT as AUTHN_PASSWORD_PROTECTED
from saml2.authn_context import TIMESYNCTOKEN as AUTHN_TIME_SYNC_TOKEN
from saml2 import logger

from pathutils import dotname, full_path
from saml2.sigver import security_context, CryptoBackendXMLSecurity


sp1 = {
"entityid": "urn:mace:umu.se:saml:roland:sp",
"service": {
Expand All @@ -26,8 +31,15 @@
"urn:mace:example.com:saml:roland:idp": {
'single_sign_on_service':
{'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect':
'http://localhost:8088/sso/'}},
}
'http://localhost:8088/sso/'}},
},
"requested_authn_context": {
"authn_context_class_ref": [
AUTHN_PASSWORD_PROTECTED,
AUTHN_TIME_SYNC_TOKEN,
],
"comparison": "exact",
},
}
},
"key_file": full_path("test.key"),
Expand Down Expand Up @@ -211,12 +223,23 @@ def test_1():

assert len(c._sp_idp) == 1
assert list(c._sp_idp.keys()) == ["urn:mace:example.com:saml:roland:idp"]
assert list(c._sp_idp.values()) == [{'single_sign_on_service':
{
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect':
'http://localhost:8088/sso/'}}]
assert list(c._sp_idp.values()) == [
{
'single_sign_on_service': {
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect': (
'http://localhost:8088/sso/'
)
}
}
]

assert c.only_use_keys_in_metadata
assert type(c.getattr("requested_authn_context")) is dict
assert c.getattr("requested_authn_context").get("authn_context_class_ref") == [
AUTHN_PASSWORD_PROTECTED,
AUTHN_TIME_SYNC_TOKEN,
]
assert c.getattr("requested_authn_context").get("comparison") == "exact"


def test_2():
Expand Down
25 changes: 17 additions & 8 deletions tests/test_71_authn_request.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from contextlib import closing
from saml2.client import Saml2Client
from saml2.server import Server
from saml2.saml import AuthnContextClassRef


def test_authn_request_with_acs_by_index():
Expand All @@ -15,22 +16,30 @@ def test_authn_request_with_acs_by_index():
# instead of AssertionConsumerServiceURL. The index with label ACS_INDEX
# exists in the SP metadata in servera.xml.
request_id, authn_request = sp.create_authn_request(
sp.config.entityid,
assertion_consumer_service_index=ACS_INDEX)
sp.config.entityid, assertion_consumer_service_index=ACS_INDEX
)

# Make sure the authn_request contains AssertionConsumerServiceIndex.
acs_index = getattr(authn_request,
'assertion_consumer_service_index', None)
assert authn_request.requested_authn_context.authn_context_class_ref == [
AuthnContextClassRef(accr)
for accr in sp.config.getattr("requested_authn_context").get("authn_context_class_ref")
]
assert authn_request.requested_authn_context.comparison == (
sp.config.getattr("requested_authn_context").get("comparison")
)

# Make sure the authn_request contains AssertionConsumerServiceIndex.
acs_index = getattr(
authn_request, 'assertion_consumer_service_index', None
)
assert acs_index == ACS_INDEX

# Create IdP.
with closing(Server(config_file="idp_all_conf")) as idp:

# Ask the IdP to pick out the binding and destination from the
# authn_request.
binding, destination = idp.pick_binding("assertion_consumer_service",
request=authn_request)
binding, destination = idp.pick_binding(
"assertion_consumer_service", request=authn_request
)

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