diff --git a/example/plugins/backends/saml2_backend.yaml.example b/example/plugins/backends/saml2_backend.yaml.example index 8aca6b4e5..da52555f6 100644 --- a/example/plugins/backends/saml2_backend.yaml.example +++ b/example/plugins/backends/saml2_backend.yaml.example @@ -66,3 +66,11 @@ config: # include a Format attribute in the NameIDPolicy. # name_id_format: 'None' name_id_format_allow_create: true + + # disco_srv must be defined if there is more than one IdP in the metadata specified above + disco_srv: http://disco.example.com + + # Allow flows to start at the discovery response endpoint. + # Not recommend but allowed by default for backwards compatiblity. + # Default will change to false in a later release. + # allow_discovery_initiated: true diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index 2640fb9db..cceabf0c3 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -26,7 +26,7 @@ from satosa.context import Context from satosa.internal import AuthenticationInformation from satosa.internal import InternalData -from satosa.exception import SATOSAAuthenticationError +from satosa.exception import SATOSAAuthenticationError, SATOSAStateError from satosa.response import SeeOther, Response from satosa.saml_util import make_saml_response from satosa.metadata_creation.description import ( @@ -78,7 +78,9 @@ class SAMLBackend(BackendModule, SAMLBaseModule): """ A saml2 backend module (acting as a SP). """ + KEY_ALLOW_DISCO_INIT_CONFIG = 'allow_discovery_initiated' KEY_DISCO_SRV = 'disco_srv' + KEY_SAML_DISCOVERY_INITIATED = 'saml_discovery_initiated' KEY_SAML_DISCOVERY_SERVICE_URL = 'saml_discovery_service_url' KEY_SAML_DISCOVERY_SERVICE_POLICY = 'saml_discovery_service_policy' KEY_SP_CONFIG = 'sp_config' @@ -170,6 +172,7 @@ def start_auth(self, context, internal_req): :type internal_req: satosa.internal.InternalData :rtype: satosa.response.Response """ + context.state[self.name] = {} entity_id = self.get_idp_entity_id(context) if entity_id is None: @@ -212,6 +215,10 @@ def disco_query(self, context): loc = self.sp.create_discovery_service_request( disco_url, self.sp.config.entityid, **args ) + + state_dict = context.state[self.name] + state_dict[SAMLBackend.KEY_SAML_DISCOVERY_INITIATED] = True + return SeeOther(loc) def construct_requested_authn_context(self, entity_id): @@ -368,12 +375,24 @@ def disco_response(self, context): """ info = context.request state = context.state + session_id = lu.get_session_id(state) + state_dict = state.get(self.name, {}) + disco_initiated = state_dict.get( + SAMLBackend.KEY_SAML_DISCOVERY_INITIATED, + False) + + if (not self.config.get(SAMLBackend.KEY_ALLOW_DISCO_INIT_CONFIG, True) + and not disco_initiated): + msg = "IdP discovery initiated flow not allowed" + logline = lu.LOG_FMT.format(id=session_id, message=msg) + logger.debug(logline, exc_info=True) + raise SATOSAStateError(state, msg) try: entity_id = info["entityID"] except KeyError as err: msg = "No IDP chosen for state" - logline = lu.LOG_FMT.format(id=lu.get_session_id(state), message=msg) + logline = lu.LOG_FMT.format(id=session_id, message=msg) logger.debug(logline, exc_info=True) raise SATOSAAuthenticationError(state, "No IDP chosen") from err diff --git a/tests/satosa/backends/test_saml2.py b/tests/satosa/backends/test_saml2.py index eed74db6c..469e2205c 100644 --- a/tests/satosa/backends/test_saml2.py +++ b/tests/satosa/backends/test_saml2.py @@ -19,6 +19,7 @@ from satosa.backends.saml2 import SAMLBackend from satosa.context import Context +from satosa.exception import SATOSAStateError from satosa.internal import InternalData from tests.users import USERS from tests.util import FakeIdP, create_metadata_from_config_dict, FakeSP @@ -348,6 +349,60 @@ def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf): assert ui_info["description"] == expected_ui_info["description"] assert ui_info["logo"] == expected_ui_info["logo"] + def test_allow_discovery_initiated(self, sp_conf, context, idp_conf): + + # Test that with allow_discovery_initiated set to True can initiate + # flow at disco_response() and be directed to the correct IdP. + config = {"sp_config": sp_conf, + "disco_srv": DISCOSRV_URL, + "allow_discovery_initiated": True} + + samlbackend = SAMLBackend( + Mock(), + INTERNAL_ATTRIBUTES, + config, + "base_url", + "samlbackend", + ) + + context.request = {'entityID': idp_conf["entityid"]} + resp = samlbackend.disco_response(context) + assert_redirect_to_idp(resp, idp_conf) + + # Test that with allow_discovery_initiated set to False can not + # initiate flow at disco_response() and instead raises exception. + config = {"sp_config": sp_conf, + "disco_srv": DISCOSRV_URL, + SAMLBackend.KEY_ALLOW_DISCO_INIT_CONFIG: False} + + samlbackend = SAMLBackend( + Mock(), + INTERNAL_ATTRIBUTES, + config, + "base_url", + "samlbackend", + ) + + context.request = {'entityID': idp_conf["entityid"]} + + with pytest.raises(SATOSAStateError): + resp = samlbackend.disco_response(context) + + # Test that with allow_discovery_initiated set to False a flow + # that begins in the backend with start_auth() marks the state + # as having been initiated properly through the disco_query() + # method on the backend and that the flow succeeds in redirecting + # to the IdP selected during discovery. + samlbackend.start_auth(context, InternalData()) + name = samlbackend.name + key = SAMLBackend.KEY_SAML_DISCOVERY_INITIATED + initiated = context.state[name][key] + assert initiated is True + + context.request = {'entityID': idp_conf["entityid"]} + resp = samlbackend.disco_response(context) + assert_redirect_to_idp(resp, idp_conf) + class TestSAMLBackendRedirects: def test_default_redirect_to_discovery_service_if_using_mdq(