From 5714885cca0a045468349f7ac02d6029fe1548b2 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Thu, 10 Mar 2022 09:10:23 +0000 Subject: [PATCH 01/57] feat: register method to single_logout_service endpoints on saml frontend --- src/satosa/frontends/saml2.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index b481b5d25..b20ffcf2f 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -99,6 +99,19 @@ def handle_authn_request(self, context, binding_in): """ return self._handle_authn_request(context, binding_in, self.idp) + def handle_logout_request(self, context, binding_in): + """ + This method is bound to the starting endpoint of the logout. + + :type context: satosa.context.Context + :type binding_in: str + + :param contxt: The current context + :param binding_in: The binding type (http post, http redirect, ..) + :return: response + """ + return NotImplementedError() + def handle_backend_error(self, exception): """ See super class satosa.frontends.base.FrontendModule @@ -519,8 +532,14 @@ def _register_endpoints(self, providers): valid_providers = "{}|^{}".format(valid_providers, provider) valid_providers = valid_providers.lstrip("|") parsed_endp = urlparse(endp) - url_map.append(("(%s)/%s$" % (valid_providers, parsed_endp.path), - functools.partial(self.handle_authn_request, binding_in=binding))) + if endp_category == "single_sign_on_service": + url_map.append(("(%s)/%s$" % (valid_providers, parsed_endp.path), + functools.partial(self.handle_authn_request, binding_in=binding))) + elif endp_category == "single_logout_service": + url_map.append(("(%s)/%s$" % (valid_providers, parsed_endp.path), + functools.partial(self.handle_logout_request, binding_in=binding))) + else: + raise NotImplementedError() if self.expose_entityid_endpoint(): logger.debug("Exposing frontend entity endpoint = {}".format(self.idp.config.entityid)) From b260ba7864d9913e6e5fe8ee2e9d9a2e691944a8 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Thu, 10 Mar 2022 10:02:18 +0000 Subject: [PATCH 02/57] feat: register logout callback functions --- src/satosa/backends/base.py | 4 ++- src/satosa/backends/saml2.py | 6 ++-- src/satosa/base.py | 8 +++++ src/satosa/frontends/base.py | 3 +- src/satosa/frontends/saml2.py | 4 +-- src/satosa/metadata_creation/saml_metadata.py | 4 +-- src/satosa/plugin_loader.py | 29 ++++++++++++------- 7 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/satosa/backends/base.py b/src/satosa/backends/base.py index 8d0432da8..d7fbb2c54 100644 --- a/src/satosa/backends/base.py +++ b/src/satosa/backends/base.py @@ -10,10 +10,11 @@ class BackendModule(object): Base class for a backend module. """ - def __init__(self, auth_callback_func, internal_attributes, base_url, name): + def __init__(self, auth_callback_func, logout_callback_func, internal_attributes, base_url, name): """ :type auth_callback_func: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response + :type logout_callback_func: :type internal_attributes: dict[string, dict[str, str | list[str]]] :type base_url: str :type name: str @@ -27,6 +28,7 @@ def __init__(self, auth_callback_func, internal_attributes, base_url, name): :param name: name of the plugin """ self.auth_callback_func = auth_callback_func + self.logout_callback_func = logout_callback_func self.internal_attributes = internal_attributes self.converter = AttributeMapper(internal_attributes) self.base_url = base_url diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index d50a93fb7..91df5a274 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -89,10 +89,11 @@ class SAMLBackend(BackendModule, SAMLBaseModule): VALUE_ACR_COMPARISON_DEFAULT = 'exact' - def __init__(self, outgoing, internal_attributes, config, base_url, name): + def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): """ :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response + :type logout: :type internal_attributes: dict[str, dict[str, list[str] | str]] :type config: dict[str, Any] :type base_url: str @@ -100,12 +101,13 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): :param outgoing: Callback should be called by the module after the authorization in the backend is done. + :param logout: Logout callback :param internal_attributes: Internal attribute map :param config: The module config :param base_url: base url of the service :param name: name of the plugin """ - super().__init__(outgoing, internal_attributes, base_url, name) + super().__init__(outgoing, logout, internal_attributes, base_url, name) self.config = self.init_config(config) self.discosrv = config.get(SAMLBackend.KEY_DISCO_SRV) diff --git a/src/satosa/base.py b/src/satosa/base.py index 7468a4ca0..2fd2e01c5 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -41,9 +41,11 @@ def __init__(self, config): logger.info("Loading backend modules...") backends = load_backends(self.config, self._auth_resp_callback_func, + self._logout_resp_callback_func, self.config["INTERNAL_ATTRIBUTES"]) logger.info("Loading frontend modules...") frontends = load_frontends(self.config, self._auth_req_callback_func, + self._logout_req_callback_func, self.config["INTERNAL_ATTRIBUTES"]) self.response_micro_services = [] @@ -102,6 +104,9 @@ def _auth_req_callback_func(self, context, internal_request): return self._auth_req_finish(context, internal_request) + def _logout_req_callback_func(self, context, internal_request): + raise NotImplementedError() + def _auth_req_finish(self, context, internal_request): backend = self.module_router.backend_routing(context) context.request = None @@ -150,6 +155,9 @@ def _auth_resp_callback_func(self, context, internal_response): return self._auth_resp_finish(context, internal_response) + def _logout_resp_callback_func(self, context, internal_response): + raise NotImplementedError() + def _handle_satosa_authentication_error(self, error): """ Sends a response to the requester about the error diff --git a/src/satosa/frontends/base.py b/src/satosa/frontends/base.py index 52840a85c..5d39dd430 100644 --- a/src/satosa/frontends/base.py +++ b/src/satosa/frontends/base.py @@ -9,7 +9,7 @@ class FrontendModule(object): Base class for a frontend module. """ - def __init__(self, auth_req_callback_func, internal_attributes, base_url, name): + def __init__(self, auth_req_callback_func, logout_req_callback_func, internal_attributes, base_url, name): """ :type auth_req_callback_func: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response @@ -21,6 +21,7 @@ def __init__(self, auth_req_callback_func, internal_attributes, base_url, name): :param name: name of the plugin """ self.auth_req_callback_func = auth_req_callback_func + self.logout_req_callback_func = logout_req_callback_func self.internal_attributes = internal_attributes self.converter = AttributeMapper(internal_attributes) self.base_url = base_url diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index b20ffcf2f..42799b57e 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -65,10 +65,10 @@ class SAMLFrontend(FrontendModule, SAMLBaseModule): KEY_ENDPOINTS = 'endpoints' KEY_IDP_CONFIG = 'idp_config' - def __init__(self, auth_req_callback_func, internal_attributes, config, base_url, name): + def __init__(self, auth_req_callback_func, logout_req_callback_func, internal_attributes, config, base_url, name): self._validate_config(config) - super().__init__(auth_req_callback_func, internal_attributes, base_url, name) + super().__init__(auth_req_callback_func, logout_req_callback_func, internal_attributes, base_url, name) self.config = self.init_config(config) self.endpoints = config[self.KEY_ENDPOINTS] diff --git a/src/satosa/metadata_creation/saml_metadata.py b/src/satosa/metadata_creation/saml_metadata.py index 895de4b98..5653a6183 100644 --- a/src/satosa/metadata_creation/saml_metadata.py +++ b/src/satosa/metadata_creation/saml_metadata.py @@ -104,8 +104,8 @@ def create_entity_descriptors(satosa_config): :type satosa_config: satosa.satosa_config.SATOSAConfig :rtype: Tuple[str, str] """ - frontend_modules = load_frontends(satosa_config, None, satosa_config["INTERNAL_ATTRIBUTES"]) - backend_modules = load_backends(satosa_config, None, satosa_config["INTERNAL_ATTRIBUTES"]) + frontend_modules = load_frontends(satosa_config, None, None, satosa_config["INTERNAL_ATTRIBUTES"]) + backend_modules = load_backends(satosa_config, None, None, satosa_config["INTERNAL_ATTRIBUTES"]) logger.info("Loaded frontend plugins: {}".format([frontend.name for frontend in frontend_modules])) logger.info("Loaded backend plugins: {}".format([backend.name for backend in backend_modules])) diff --git a/src/satosa/plugin_loader.py b/src/satosa/plugin_loader.py index b7eb4cf46..9d95c83f2 100644 --- a/src/satosa/plugin_loader.py +++ b/src/satosa/plugin_loader.py @@ -27,46 +27,55 @@ def prepend_to_import_path(import_paths): del sys.path[0:len(import_paths)] # restore sys.path -def load_backends(config, callback, internal_attributes): +def load_backends(config, auth_callback, logout_callback, internal_attributes): """ Load all backend modules specified in the config :type config: satosa.satosa_config.SATOSAConfig - :type callback: + :type auth_callback: + (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response + :type logout_callback: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :rtype: Sequence[satosa.backends.base.BackendModule] :param config: The configuration of the satosa proxy - :param callback: Function that will be called by the backend after the authentication is done. + :param auth_callback: Function that will be called by the backend after the authentication is done. + :param logout_callback: Function that will be called by the backend after logout is done. :return: A list of backend modules """ backend_modules = _load_plugins( config.get("CUSTOM_PLUGIN_MODULE_PATHS"), config["BACKEND_MODULES"], backend_filter, config["BASE"], - internal_attributes, callback) + internal_attributes, auth_callback, + logout_callback + ) logger.info("Setup backends: {}".format([backend.name for backend in backend_modules])) return backend_modules -def load_frontends(config, callback, internal_attributes): +def load_frontends(config, auth_callback, logout_callback, internal_attributes): """ Load all frontend modules specified in the config :type config: satosa.satosa_config.SATOSAConfig - :type callback: + :type auth_callback: + (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response + :type logout_callback: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :rtype: Sequence[satosa.frontends.base.FrontendModule] :param config: The configuration of the satosa proxy - :param callback: Function that will be called by the frontend after the authentication request + :param auth_callback: Function that will be called by the frontend after the authentication request + :param logout_callback: Function that will be called by the frontend after the logout request has been processed. :return: A list of frontend modules """ frontend_modules = _load_plugins(config.get("CUSTOM_PLUGIN_MODULE_PATHS"), config["FRONTEND_MODULES"], - frontend_filter, config["BASE"], internal_attributes, callback) + frontend_filter, config["BASE"], internal_attributes, auth_callback, + logout_callback) logger.info("Setup frontends: {}".format([frontend.name for frontend in frontend_modules])) return frontend_modules @@ -151,7 +160,7 @@ def _load_plugin_config(config): raise SATOSAConfigurationError("The configuration is corrupt.") from exc -def _load_plugins(plugin_paths, plugins, plugin_filter, base_url, internal_attributes, callback): +def _load_plugins(plugin_paths, plugins, plugin_filter, base_url, internal_attributes, auth_callback, logout_callback): """ Loads endpoint plugins @@ -178,7 +187,7 @@ def _load_plugins(plugin_paths, plugins, plugin_filter, base_url, internal_attri if module_class: module_config = _replace_variables_in_plugin_module_config(plugin_config["config"], base_url, plugin_config["name"]) - instance = module_class(callback, internal_attributes, module_config, base_url, + instance = module_class(auth_callback, logout_callback, internal_attributes, module_config, base_url, plugin_config["name"]) loaded_plugin_modules.append(instance) return loaded_plugin_modules From c680cefa330f56e13538c533bebe4861084b2811 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 15 Mar 2022 11:52:03 +0000 Subject: [PATCH 03/57] feat: handle logout request on saml frontend --- src/satosa/base.py | 23 ++++++++++++++++++++++- src/satosa/frontends/saml2.py | 23 ++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/satosa/base.py b/src/satosa/base.py index 2fd2e01c5..01d7dd87a 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -105,13 +105,34 @@ def _auth_req_callback_func(self, context, internal_request): return self._auth_req_finish(context, internal_request) def _logout_req_callback_func(self, context, internal_request): - raise NotImplementedError() + """ + This function is called by a frontend module when a logout request has been processed. + + :type context: satosa.context.Context + :typr internal_request: + :rtype: + + :param context: The request context + :param internal_request: request processed by the frontend + :return Response + """ + state = context.state + state[STATE_KEY] = {"requester": internal_request.requester} + msg = "Requesting provider: {}".format(internal_request.requester) + logline = lu.LOG_FMT.format(id=lu.get_session_id(state), message=msg) + logger.info(logline) + return self._logout_req_finish(context, internal_request) def _auth_req_finish(self, context, internal_request): backend = self.module_router.backend_routing(context) context.request = None return backend.start_auth(context, internal_request) + def _logout_req_finish(self, context, internal_request): + backend = self.module_router.backend_routing(context) + context.request = None + return backend.start_logout(context, internal_request) + def _auth_resp_finish(self, context, internal_response): user_id_to_attr = self.config["INTERNAL_ATTRIBUTES"].get("user_id_to_attr", None) if user_id_to_attr: diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 42799b57e..f36b9d6b5 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -110,7 +110,7 @@ def handle_logout_request(self, context, binding_in): :param binding_in: The binding type (http post, http redirect, ..) :return: response """ - return NotImplementedError() + return self._handle_logout_request(context, binding_in, self.idp) def handle_backend_error(self, exception): """ @@ -274,6 +274,27 @@ def _handle_authn_request(self, context, binding_in, idp): context.decorate(Context.KEY_METADATA_STORE, self.idp.metadata) return self.auth_req_callback_func(context, internal_req) + def _handle_logout_request(self, context, binding_in, idp): + """ + :type context: satosa.context.Context + :type binding_in: str + :type idp: saml.server.Server + :rtype: satosa.response.Response + + :param context: The current context + :param binding_in: The pysaml binding type + :param idp: The saml frontend idp server + :return: response + """ + req_info = idp.parse_logout_request(context.request["SAMLRequest"], binding_in) + logout_req = req_info.message + msg = "{}".format(logout_req) + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logger.debug(logline) + internal_req = InternalData() + return self.logout_req_callback_func(context, internal_req) + + def _get_approved_attributes(self, idp, idp_policy, sp_entity_id, state): """ Returns a list of approved attributes From b766961955fe6c7edddfe51315643316430b67b4 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 15 Mar 2022 11:54:16 +0000 Subject: [PATCH 04/57] feat: create logout request on saml backend --- src/satosa/backends/base.py | 14 +++++++++ src/satosa/backends/saml2.py | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/satosa/backends/base.py b/src/satosa/backends/base.py index d7fbb2c54..9e9ce61c3 100644 --- a/src/satosa/backends/base.py +++ b/src/satosa/backends/base.py @@ -48,6 +48,20 @@ def start_auth(self, context, internal_request): """ raise NotImplementedError() + def start_logout(self, context, internal_request): + """ + This is the start up function of the backend logout. + + :type context: satosa.context.Context + :type internal_request: satosa.internal.InternalData + :rtype + + :param context: the request context + :param internal_request: Information about the logout request + :return: + """ + raise NotImplementedError() + def register_endpoints(self): """ Register backend functions to endpoint urls. diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index 91df5a274..3749cdbd7 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -17,6 +17,7 @@ from saml2.authn_context import requested_authn_context from saml2.samlp import RequesterID from saml2.samlp import Scoping +from saml2.saml import NameID import satosa.logging_util as lu import satosa.util as util @@ -27,6 +28,7 @@ from satosa.internal import AuthenticationInformation from satosa.internal import InternalData from satosa.exception import SATOSAAuthenticationError +from satosa.exception import SATOSAUnknownError from satosa.response import SeeOther, Response from satosa.saml_util import make_saml_response from satosa.metadata_creation.description import ( @@ -195,6 +197,18 @@ def start_auth(self, context, internal_req): return self.authn_request(context, entity_id) + def start_logout(self, context, internal_req): + """ + See super class method satosa.backends.base.BackendModule#start_logout + + :type context: satosa.context.Context + :type internal_req: satosa.internal.InternalData + :rtype: + """ + + entity_id = self.get_idp_entity_id(context) + return self.logout_request(context, entity_id) + def disco_query(self, context): """ Makes a request to the discovery server @@ -364,6 +378,48 @@ def authn_response(self, context, binding): context.state.pop(Context.KEY_FORCE_AUTHN, None) return self.auth_callback_func(context, self._translate_response(authn_response, context.state)) + def logout_request(self, context, entity_id): + """ + Perform Logout request on idp with given entity_id. + This is the start of single logout. + + :type context: satosa.context.Context + :type entity_id: str + :rtype: satosa.response.Response + + :param context: The current context + :param entity_id: Target IDP entity id + :return: response to the user agent + """ + try: + binding, destination = self.sp.pick_binding( + "single_logout_service", None, "idpsso", entity_id=entity_id + ) + msg = "binding: {}, destination: {}".format(binding, destination) + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logger.debug(logline) + + slo_endp, response_binding = self.sp.config.getattr("endpoints", "sp")["single_logout_service"][0] + name_id_format = self.sp.config.getattr("name_id_format", "sp") + name_id = NameID(format=name_id_format) + req_id, req = self.sp.create_logout_request( + destination, issuer_entity_id=entity_id, name_id=name_id + ) + msg = "req_id: {}, req: {}".format(req_id, req) + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logger.debug(logline) + relay_state = util.rndstr() + ht_args = self.sp.apply_binding(binding, "%s" % req, destination, relay_state=relay_state) + msg = "ht_args: {}".format(ht_args) + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logger.debug(logline) + except Exception as exc: + msg = "Failed to construct the LogoutRequest for state" + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logger.debug(logline, exc_info=True) + raise SATOSAUnknownError + return make_saml_response(binding, ht_args) + def disco_response(self, context): """ Endpoint for the discovery server response @@ -411,11 +467,13 @@ def _translate_response(self, response, state): if authenticating_authorities else None ) + session_index = response.session_info()['session_index'] auth_info = AuthenticationInformation( auth_class_ref=authn_context_ref, timestamp=authn_instant, authority=authenticating_authority, issuer=issuer, + session_index=session_index, ) # The SAML response may not include a NameID. From 5dec5251b3ab4841194b7819f8ac490cf67f1d70 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Wed, 30 Mar 2022 13:50:59 +0000 Subject: [PATCH 05/57] feat: build internal logout request --- src/satosa/frontends/saml2.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index f36b9d6b5..1f41569f1 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -291,7 +291,26 @@ def _handle_logout_request(self, context, binding_in, idp): msg = "{}".format(logout_req) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline) - internal_req = InternalData() + + resp_args = {} + resp_args['name_id'] = logout_req.name_id.text if logout_req.name_id.text else None + resp_args['session_indexes'] = [] + for session_index in logout_req.session_index: + resp_args['session_indexes'].append(session_index.text) + requester = logout_req.issuer.text + requester_name = self._get_sp_display_name(idp, requester) + + context.state[self.name] = self._create_state_data(context, resp_args, + context.request.get("RelayState")) + + name_id_value = logout_req.name_id.text + name_id_format = logout_req.name_id.format + + internal_req = InternalData( + subject_id=name_id_value, + subject_type=name_id_format, + requester=requester, + ) return self.logout_req_callback_func(context, internal_req) From 9800529669f52bedda1a3179ae20e93a3c9e6d71 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Wed, 30 Mar 2022 14:16:44 +0000 Subject: [PATCH 06/57] feat: add database for session storage --- src/satosa/base.py | 4 ++++ src/satosa/plugin_loader.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/satosa/base.py b/src/satosa/base.py index 01d7dd87a..7c74d2425 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -13,6 +13,7 @@ from .exception import SATOSAError, SATOSAAuthenticationError, SATOSAUnknownError from .plugin_loader import load_backends, load_frontends from .plugin_loader import load_request_microservices, load_response_microservices +from .plugin_loader import load_database from .routing import ModuleRouter, SATOSANoBoundEndpointError from .state import cookie_to_state, SATOSAStateError, State, state_to_cookie @@ -66,6 +67,9 @@ def __init__(self, config): self.config["BASE"])) self._link_micro_services(self.response_micro_services, self._auth_resp_finish) + logger.info("Loading database...") + self.db = load_database(self.config) + self.module_router = ModuleRouter(frontends, backends, self.request_micro_services + self.response_micro_services) diff --git a/src/satosa/plugin_loader.py b/src/satosa/plugin_loader.py index 9d95c83f2..770f8c5aa 100644 --- a/src/satosa/plugin_loader.py +++ b/src/satosa/plugin_loader.py @@ -289,3 +289,31 @@ def load_response_microservices(plugin_path, plugins, internal_attributes, base_ base_url) logger.info("Loaded response micro services:{}".format([type(k).__name__ for k in response_services])) return response_services + + +def load_database(config): + """ + Loads the storage database specifies in the config + + :type config: satosa.satosa_config.SATOSAConfig + + :param config: The configuration of the satosa proxy + """ + try: + db = config["DATABASE"]["name"] + except SATOSAConfigurationError as err: + logger.error(err) + if db == "memory": + from satosa.store import SessionStorage + return SessionStorage(config) + elif db == "mongodb": + from satosa.store import SessionStorageMDB + return SessionStorageMDB(config) + elif db == "postgresql": + from satosa.store import SessionStoragePDB + try: + return SessionStoragePDB(config) + except Exception as error: + return error + else: + raise NotImplementedError() From 48c049398d49ee0e39919a1624a9aa0b16467c76 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Wed, 30 Mar 2022 14:29:26 +0000 Subject: [PATCH 07/57] feat: build saml backend logout request --- src/satosa/backends/saml2.py | 18 +++++++++++------- src/satosa/base.py | 7 ++++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index 3749cdbd7..1d411da3f 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -197,7 +197,7 @@ def start_auth(self, context, internal_req): return self.authn_request(context, entity_id) - def start_logout(self, context, internal_req): + def start_logout(self, context, internal_req, internal_authn_resp): """ See super class method satosa.backends.base.BackendModule#start_logout @@ -207,7 +207,7 @@ def start_logout(self, context, internal_req): """ entity_id = self.get_idp_entity_id(context) - return self.logout_request(context, entity_id) + return self.logout_request(context, entity_id, internal_authn_resp) def disco_query(self, context): """ @@ -378,7 +378,7 @@ def authn_response(self, context, binding): context.state.pop(Context.KEY_FORCE_AUTHN, None) return self.auth_callback_func(context, self._translate_response(authn_response, context.state)) - def logout_request(self, context, entity_id): + def logout_request(self, context, entity_id, internal_authn_resp): """ Perform Logout request on idp with given entity_id. This is the start of single logout. @@ -401,9 +401,12 @@ def logout_request(self, context, entity_id): slo_endp, response_binding = self.sp.config.getattr("endpoints", "sp")["single_logout_service"][0] name_id_format = self.sp.config.getattr("name_id_format", "sp") - name_id = NameID(format=name_id_format) + name_id = internal_authn_resp["subject_id"] + name_id = NameID(format=name_id_format, text=name_id) + session_indexes = internal_authn_resp["auth_info"]["session_index"] req_id, req = self.sp.create_logout_request( - destination, issuer_entity_id=entity_id, name_id=name_id + destination, issuer_entity_id=entity_id, name_id=name_id, + session_indexes=session_indexes, sign=True ) msg = "req_id: {}, req: {}".format(req_id, req) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) @@ -467,13 +470,14 @@ def _translate_response(self, response, state): if authenticating_authorities else None ) - session_index = response.session_info()['session_index'] + session_indexes = [] + session_indexes.append(response.session_info()['session_index']) auth_info = AuthenticationInformation( auth_class_ref=authn_context_ref, timestamp=authn_instant, authority=authenticating_authority, issuer=issuer, - session_index=session_index, + session_index=session_indexes, ) # The SAML response may not include a NameID. diff --git a/src/satosa/base.py b/src/satosa/base.py index 7c74d2425..3a99fc9ad 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -130,12 +130,15 @@ def _logout_req_callback_func(self, context, internal_request): def _auth_req_finish(self, context, internal_request): backend = self.module_router.backend_routing(context) context.request = None + self.db.store_authn_req(context.state, internal_request) # not necessary return backend.start_auth(context, internal_request) def _logout_req_finish(self, context, internal_request): backend = self.module_router.backend_routing(context) context.request = None - return backend.start_logout(context, internal_request) + self.db.store_logout_req(context.state, internal_request) + internal_authn_resp = self.db.get_authn_resp(context.state) + return backend.start_logout(context, internal_request, internal_authn_resp) def _auth_resp_finish(self, context, internal_response): user_id_to_attr = self.config["INTERNAL_ATTRIBUTES"].get("user_id_to_attr", None) @@ -145,6 +148,8 @@ def _auth_resp_finish(self, context, internal_response): # remove all session state unless CONTEXT_STATE_DELETE is False context.state.delete = self.config.get("CONTEXT_STATE_DELETE", True) context.request = None + self.db.store_authn_resp(context.state, internal_response) + self.db.get_authn_resp(context.state) frontend = self.module_router.frontend_routing(context) return frontend.handle_authn_response(context, internal_response) From 1f8c8f01cafa7fc2666c740e837d6e6990cbc471 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Thu, 31 Mar 2022 09:50:39 +0000 Subject: [PATCH 08/57] feat: register single_logout_service endpoints on saml backend --- src/satosa/backends/saml2.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index 1d411da3f..558644042 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -423,6 +423,21 @@ def logout_request(self, context, entity_id, internal_authn_resp): raise SATOSAUnknownError return make_saml_response(binding, ht_args) + def logout_response(self, context, binding): + """ + Endpoint for the idp logout response + + :type context: satosa.context.Context + :type binding: str + :rtype: satosa.response.Response + + :param context: The current context + :param binding: SAML binding type + :return Response + """ + raise NotImplementedError() + + def disco_response(self, context): """ Endpoint for the discovery server response @@ -561,6 +576,11 @@ def register_endpoints(self): url_map.append( ("^%s/%s$" % (self.name, "reload-metadata"), self._reload_metadata)) + for endp, binding in sp_endpoints["single_logout_service"]: + parsed_endp = urlparse(endp) + url_map.append(("^%s$" % parsed_endp.path[1:], + functools.partial(self.logout_response, binding=binding))) + return url_map def _reload_metadata(self, context): From 29b73f804856569712d01602b411458c243986ec Mon Sep 17 00:00:00 2001 From: sebulibah Date: Mon, 11 Apr 2022 13:18:35 +0000 Subject: [PATCH 09/57] feat: bind handle_logout_message to single_logout_service endpoint on saml frontend --- src/satosa/frontends/saml2.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 1f41569f1..747a48657 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -99,6 +99,25 @@ def handle_authn_request(self, context, binding_in): """ return self._handle_authn_request(context, binding_in, self.idp) + def handle_logout_message(self, context, binding_in): + """ + This method is bound to the starting endpoint of the logout. + + :type context: satosa.context.Context + :type binding_in: str + :rtype: + + :param context: The current context + :param binding_in: The binding type + :return: + """ + if context.request["SAMLRequest"]: + return self.handle_logout_request(context, binding_in) + elif context.request["SAMLResponse"]: + return self.handle_logout_logout_response(context, binding_in) + else: + return NotImplementedError() + def handle_logout_request(self, context, binding_in): """ This method is bound to the starting endpoint of the logout. @@ -112,6 +131,9 @@ def handle_logout_request(self, context, binding_in): """ return self._handle_logout_request(context, binding_in, self.idp) + def handle_logout_logout_response(self, context, binding_in): + return NotImplementedError() + def handle_backend_error(self, exception): """ See super class satosa.frontends.base.FrontendModule @@ -577,7 +599,7 @@ def _register_endpoints(self, providers): functools.partial(self.handle_authn_request, binding_in=binding))) elif endp_category == "single_logout_service": url_map.append(("(%s)/%s$" % (valid_providers, parsed_endp.path), - functools.partial(self.handle_logout_request, binding_in=binding))) + functools.partial(self.handle_logout_message, binding_in=binding))) else: raise NotImplementedError() From 62483f369353f3b5189eaa96b5a5704fc1885773 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 19 Jul 2022 13:43:34 +0000 Subject: [PATCH 10/57] feat: handle logout response at saml backend --- src/satosa/backends/saml2.py | 38 +++++++++++++++++++++++++++++++++++- src/satosa/internal.py | 24 +++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index 558644042..e6e0f2520 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -26,6 +26,7 @@ from satosa.base import STATE_KEY as STATE_KEY_BASE from satosa.context import Context from satosa.internal import AuthenticationInformation +from satosa.internal import LogoutInformation from satosa.internal import InternalData from satosa.exception import SATOSAAuthenticationError from satosa.exception import SATOSAUnknownError @@ -435,7 +436,23 @@ def logout_response(self, context, binding): :param binding: SAML binding type :return Response """ - raise NotImplementedError() + if not context.request.get("SAMLResponse"): + msg = "Missing Response for state" + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logger.debug(logline) + raise SATOSAUnknownError(context.state, "Missing Response") + + try: + logout_response = self.sp.parse_logout_request_response( + context.request["SAMLResponse"], binding) + except Exception as err: + msg = "Failed to parse logout response for state" + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logger.debug(logline, exc_info=True) + raise SATOSAUnknownError(context.state, "Failed to parse logout response") from err + + return self.logout_callback_func(context, self._translate_logout_response( + logout_response, context.state)) def disco_response(self, context): @@ -518,6 +535,25 @@ def _translate_response(self, response, state): logger.debug(logline) return internal_resp + def _translate_logout_response(self, response, state): + timestamp = response.response.issue_instant + issuer = response.response.issuer.text + + status = { + "status_code": response.response.status.status_code.value, + } + + logout_info = LogoutInformation( + timestamp=timestamp, + issuer=issuer, + status=status + ) + + msg = "logout response content" + logline = lu.LOG_FMT.format(id=lu.get_session_id(state), message=msg) + logger.debug(logline) + return logout_info + def _metadata_endpoint(self, context): """ Endpoint for retrieving the backend metadata diff --git a/src/satosa/internal.py b/src/satosa/internal.py index 24de31890..b2fc403c4 100644 --- a/src/satosa/internal.py +++ b/src/satosa/internal.py @@ -111,6 +111,30 @@ def __init__( self.authority = authority +class LogoutInformation(_Datafy): + """ + Class that holds information about the logout + """ + + def __init__( + self, + timestamp=None, + issuer=None, + status=None, + *args, + **kwargs, + ): + """ + :param timestamp: time when the logout was done + :param issuer: where the logout was done + :param status: status of the logout + """ + super().__init__(self, *args, **kwargs) + self.timestamp = timestamp + self.issuer = issuer + self.status = status + + class InternalData(_Datafy): """ A base class for the data carriers between frontends/backends From d4d7120c0a46c358dd259a3bdab888d4b79da801 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Thu, 21 Jul 2022 09:18:46 +0000 Subject: [PATCH 11/57] feat: add logout response handlers in saml frontend --- src/satosa/base.py | 25 +++++++++++++++++++++---- src/satosa/frontends/saml2.py | 24 +++++++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/satosa/base.py b/src/satosa/base.py index 3a99fc9ad..97fd6b575 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -130,13 +130,11 @@ def _logout_req_callback_func(self, context, internal_request): def _auth_req_finish(self, context, internal_request): backend = self.module_router.backend_routing(context) context.request = None - self.db.store_authn_req(context.state, internal_request) # not necessary return backend.start_auth(context, internal_request) def _logout_req_finish(self, context, internal_request): backend = self.module_router.backend_routing(context) context.request = None - self.db.store_logout_req(context.state, internal_request) internal_authn_resp = self.db.get_authn_resp(context.state) return backend.start_logout(context, internal_request, internal_authn_resp) @@ -146,7 +144,7 @@ def _auth_resp_finish(self, context, internal_response): internal_response.attributes[user_id_to_attr] = [internal_response.subject_id] # remove all session state unless CONTEXT_STATE_DELETE is False - context.state.delete = self.config.get("CONTEXT_STATE_DELETE", True) + #context.state.delete = self.config.get("CONTEXT_STATE_DELETE", True) context.request = None self.db.store_authn_resp(context.state, internal_response) self.db.get_authn_resp(context.state) @@ -154,6 +152,13 @@ def _auth_resp_finish(self, context, internal_response): frontend = self.module_router.frontend_routing(context) return frontend.handle_authn_response(context, internal_response) + def _logout_resp_finish(self, context, internal_response): + self.db.delete_session(context.state) + context.request = None + + frontend = self.module_router.frontend_routing(context) + return frontend.handle_logout_response(context, internal_response) + def _auth_resp_callback_func(self, context, internal_response): """ This function is called by a backend module when the authorization is @@ -186,7 +191,19 @@ def _auth_resp_callback_func(self, context, internal_response): return self._auth_resp_finish(context, internal_response) def _logout_resp_callback_func(self, context, internal_response): - raise NotImplementedError() + """ + This function is called by a backend module when logout is complete + + :type context: satosa.context.Context + :type internal_response: satosa.internal.LogoutInformation + :rtype: satosa.response.Response + + :param context: The request context + :param internal_response: The logout response + """ + context.request = None + context.state["ROUTER"] = "idp" + return self._logout_resp_finish(context, internal_response) def _handle_satosa_authentication_error(self, error): """ diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 747a48657..98b5efa95 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -114,7 +114,7 @@ def handle_logout_message(self, context, binding_in): if context.request["SAMLRequest"]: return self.handle_logout_request(context, binding_in) elif context.request["SAMLResponse"]: - return self.handle_logout_logout_response(context, binding_in) + return self.handle_logout_response(context, binding_in) else: return NotImplementedError() @@ -131,8 +131,13 @@ def handle_logout_request(self, context, binding_in): """ return self._handle_logout_request(context, binding_in, self.idp) - def handle_logout_logout_response(self, context, binding_in): - return NotImplementedError() + def handle_logout_response(self, context, binding_in): + """ + See super class method satosa.frontends.base.FrontendModule#handle_logout_response + :type context: satosa.context.Context + :type binding_in: str + """ + return self._handle_logout_response(context, binding_in, self.idp) def handle_backend_error(self, exception): """ @@ -523,6 +528,19 @@ def _handle_authn_response(self, context, internal_response, idp): del context.state[self.name] return make_saml_response(resp_args["binding"], http_args) + def _handle_logout_response(self, context, internal_response, idp): + """ + See super class method satosa.frontends.base.FrontendModule#handle_logout_response + :type context: satosa.context.Context + :type internal_response: satosa.internal.InternalData + :rtype satosa.response.LogoutResponse + + :param context: the current context + :param internal_response: the internal logout response + :param idp: the saml frontend idp + """ + return NotImplementedError() + def _handle_backend_error(self, exception, idp): """ See super class satosa.frontends.base.FrontendModule From 97c0aa005101ee97b6f69fc6b358d807869e7b9a Mon Sep 17 00:00:00 2001 From: sebulibah Date: Wed, 3 Aug 2022 10:32:22 +0000 Subject: [PATCH 12/57] feat: create logout response for sp that initiates logout --- src/satosa/frontends/saml2.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 98b5efa95..cb39c1142 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -338,6 +338,24 @@ def _handle_logout_request(self, context, binding_in, idp): subject_type=name_id_format, requester=requester, ) + + # Return logout response to SP that initiated logout if logout request contains + # the element within the element + extensions = logout_req.extensions if logout_req.extensions else None + _extensions = [] + for ext in extensions.extension_elements: + _extensions.append(ext.namespace) + + if "urn:oasis:names:tc:SAML:2.0:protocol:ext:async-slo" not in _extensions: + binding, destination = self.idp.pick_binding( + "single_logout_service", None, "spsso", entity_id=logout_req.issuer.text + ) + logout_resp = self.idp.create_logout_response(logout_req, [binding]) + http_args = self.idp.apply_binding(binding, "%s" % logout_resp, destination) + msg = "http_args: {}".format(http_args) + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + make_saml_response(binding, http_args) + return self.logout_req_callback_func(context, internal_req) From 8e21fe3a0d5f4a2cb4d103e86d9242a91ecca723 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 12 Aug 2022 13:48:18 +0000 Subject: [PATCH 13/57] feat: create logout requests for sps with participating sessions --- src/satosa/frontends/saml2.py | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index cb39c1142..d847c98bb 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -164,6 +164,7 @@ def register_endpoints(self, backend_names): # Create the idp idp_config = IdPConfig().load(copy.deepcopy(self.idp_config)) self.idp = Server(config=idp_config) + self.sp_sessions = {} return self._register_endpoints(backend_names) + url_map def _create_state_data(self, context, resp_args, relay_state): @@ -339,6 +340,29 @@ def _handle_logout_request(self, context, binding_in, idp): requester=requester, ) + sp_sessions = self._sp_session_info(context) + + for sp_info in sp_sessions: + for authn_statement in sp_info[1]: + if authn_statement[0].session_index == resp_args["session_indexes"][0]: + continue + else: + binding, slo_destination = self.idp.pick_binding( + "single_logout_service", None, "spsso", entity_id=sp_info[0][0] + ) + + lreq_id, lreq = self.idp.create_logout_request( + destination=slo_destination, + issuer_entity_id=sp_info[0][0], + name_id=NameID(text=sp_info[0][1].text), + session_indexes=[authn_statement[0].session_index] + ) + + http_args = self.idp.apply_binding(binding, "%s" % lreq, slo_destination) + msg = "http_args: {}".format(http_args) + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + make_saml_response(binding, http_args) + # Return logout response to SP that initiated logout if logout request contains # the element within the element extensions = logout_req.extensions if logout_req.extensions else None @@ -358,6 +382,22 @@ def _handle_logout_request(self, context, binding_in, idp): return self.logout_req_callback_func(context, internal_req) + def _sp_session_info(self, context): + """ + :type context: satosa.context.Context + :rtype: list[((str, saml2.saml.NameID), [[saml2.saml.AuthnStatement]])] + + :param context: The current context + :return: list of service provider session information + """ + sp_sessions = [] + + session_id = context.state["SESSION_ID"] + if session_id in self.sp_sessions: + for sp in self.sp_sessions[session_id]: + sp_sessions.append( + (sp, self.idp.session_db.get_authn_statements(sp[1]))) + return sp_sessions def _get_approved_attributes(self, idp, idp_policy, sp_entity_id, state): """ @@ -462,6 +502,12 @@ def _handle_authn_response(self, context, internal_response, idp): name_qualifier=None, ) + session_id = context.state["SESSION_ID"] + if session_id not in self.sp_sessions.keys(): + self.sp_sessions[session_id] = [] + + self.sp_sessions[session_id].append((sp_entity_id, name_id)) + msg = "returning attributes {}".format(json.dumps(ava)) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline) From ed1a4618a07b410dbfe15bc863da34d159d30d73 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Wed, 17 Aug 2022 13:00:34 +0000 Subject: [PATCH 14/57] feat: return response object on saml backend after handling logout response --- src/satosa/backends/saml2.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index e6e0f2520..efcf17b94 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -450,10 +450,9 @@ def logout_response(self, context, binding): logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline, exc_info=True) raise SATOSAUnknownError(context.state, "Failed to parse logout response") from err - - return self.logout_callback_func(context, self._translate_logout_response( - logout_response, context.state)) - + message = "Logout {}".format("OK" if logout_response else "Failed") + status = "200 OK" if logout_response else "500 FAILED" + return Response(message=message, status=status) def disco_response(self, context): """ From 0ec992e94f3e0eb6e7a0db74ba35c1c0ca80ef0e Mon Sep 17 00:00:00 2001 From: sebulibah Date: Wed, 15 Feb 2023 11:56:03 +0000 Subject: [PATCH 15/57] feat: add postgres and dictionary state storage --- src/satosa/store.py | 83 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/satosa/store.py diff --git a/src/satosa/store.py b/src/satosa/store.py new file mode 100644 index 000000000..a1900b94d --- /dev/null +++ b/src/satosa/store.py @@ -0,0 +1,83 @@ +class Storage: + def __init__(self, config): + self.db_config = config["DATABASE"] + + +class SessionStorage: + """ + In-memory storage + """ + def __init__(self, config): + super().__init__(config) + self.authn_responses = {} + + def store_authn_resp(self, state, internal_resp): + self.authn_responses[state["SESSION_ID"]] = internal_resp.to_dict() + + def get_authn_resp(self, state): + return self.authn_responses.get(state["SESSION_ID"]) + + def delete_session(self, state, response_id): + if self.authn_responses.get(state["SESSION_ID"]): + del self.authn_responses[state["SESSION_ID"]] + + +from sqlalchemy.ext.declarative import declarative_base + + +Base = declarative_base() + +class AuthnResponse(Base): + from sqlalchemy.dialects.postgresql import JSON + from sqlalchemy import Column, Integer, String + + __tablename__ = 'authn_responses' + id = Column(Integer, primary_key=True, autoincrement=True) + session_id = Column(String) + authn_response = Column(JSON) + + +class SessionStoragePDB(Storage): + """ + PostgreSQL storage + """ + + def __init__(self, config): + super().__init__(config) + + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + HOST = self.db_config["host"] + PORT = self.db_config["port"] + DB_NAME = self.db_config["db_name"] + USER = self.db_config["user"] + PWD = self.db_config["password"] + + engine = create_engine(f"postgresql://{USER}:{PWD}@{HOST}:{PORT}/{DB_NAME}") + Base.metadata.create_all(engine) + self.Session = sessionmaker(bind=engine) + + def store_authn_resp(self, state, internal_resp): + session = self.Session() + auth_response = AuthnResponse( + session_id=state["SESSION_ID"], + authn_response=(internal_resp.to_dict()) + ) + session.add(auth_response) + session.commit() + session.close() + + def get_authn_resp(self, state): + session = self.Session() + authn_response = session.query(AuthnResponse).filter( + AuthnResponse.session_id == state["SESSION_ID"]).all() + session.close() + authn_response = vars(authn_response[-1])["authn_response"] + return authn_response + + def delete_session(self, state, response_id): + session = self.Session() + session.query(AuthnResponse).filter_by(id=response_id).delete() + session.commit() + session.close() From fa185368790a2a8f4e5ad85ba54660e230ee890e Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 24 Feb 2023 09:54:05 +0000 Subject: [PATCH 16/57] test: add logout arguments to satosa/frontends/test_saml2 to fix tests --- tests/satosa/frontends/test_saml2.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/satosa/frontends/test_saml2.py b/tests/satosa/frontends/test_saml2.py index 978489429..69f8fb4b2 100644 --- a/tests/satosa/frontends/test_saml2.py +++ b/tests/satosa/frontends/test_saml2.py @@ -69,6 +69,7 @@ def setup_for_authn_req(self, context, idp_conf, sp_conf, nameid_format=None, re base_url = self.construct_base_url_from_entity_id(idp_conf["entityid"]) samlfrontend = SAMLFrontend(lambda ctx, internal_req: (ctx, internal_req), + lambda ctx, internal_logout_req: (ctx, internal_logout_req), internal_attributes, config, base_url, "saml_frontend") samlfrontend.register_endpoints(["saml"]) @@ -119,7 +120,8 @@ def get_auth_response(self, samlfrontend, context, internal_response, sp_conf, i ]) def test_config_error_handling(self, conf): with pytest.raises(ValueError): - SAMLFrontend(lambda ctx, req: None, INTERNAL_ATTRIBUTES, conf, "base_url", "saml_frontend") + SAMLFrontend(lambda ctx, req: None, lambda ctx, req: None, + INTERNAL_ATTRIBUTES, conf, "base_url", "saml_frontend") def test_register_endpoints(self, idp_conf): """ @@ -133,6 +135,7 @@ def get_path_from_url(url): base_url = self.construct_base_url_from_entity_id(idp_conf["entityid"]) samlfrontend = SAMLFrontend(lambda context, internal_req: (context, internal_req), + lambda context, internal_logout_req: (context, internal_logout_req), INTERNAL_ATTRIBUTES, config, base_url, "saml_frontend") providers = ["foo", "bar"] @@ -247,7 +250,7 @@ def test_get_filter_attributes_with_sp_requested_attributes_without_friendlyname "eduPersonAffiliation", "mail", "displayName", "sn", "givenName"]}} # no op mapping for saml attribute names - samlfrontend = SAMLFrontend(None, internal_attributes, conf, base_url, "saml_frontend") + samlfrontend = SAMLFrontend(None, None, internal_attributes, conf, base_url, "saml_frontend") samlfrontend.register_endpoints(["testprovider"]) internal_req = InternalData( @@ -357,7 +360,8 @@ def test_sp_metadata_without_uiinfo(self, context, idp_conf, sp_conf): def test_metadata_endpoint(self, context, idp_conf): conf = {"idp_config": idp_conf, "endpoints": ENDPOINTS} - samlfrontend = SAMLFrontend(lambda ctx, req: None, INTERNAL_ATTRIBUTES, conf, "base_url", "saml_frontend") + samlfrontend = SAMLFrontend(lambda ctx, req: None, lambda ctx, req: None, + INTERNAL_ATTRIBUTES, conf, "base_url", "saml_frontend") samlfrontend.register_endpoints(["todo"]) resp = samlfrontend._metadata_endpoint(context) headers = dict(resp.headers) @@ -399,7 +403,8 @@ class TestSAMLMirrorFrontend: @pytest.fixture(autouse=True) def create_frontend(self, idp_conf): conf = {"idp_config": idp_conf, "endpoints": ENDPOINTS} - self.frontend = SAMLMirrorFrontend(lambda ctx, req: None, INTERNAL_ATTRIBUTES, conf, BASE_URL, + self.frontend = SAMLMirrorFrontend(lambda ctx, req: None, lambda ctx, req: None, + INTERNAL_ATTRIBUTES, conf, BASE_URL, "saml_mirror_frontend") self.frontend.register_endpoints([self.BACKEND]) @@ -490,6 +495,7 @@ def frontend(self, idp_conf, sp_conf): # Create, register the endpoints, and then return the frontend # instance. frontend = SAMLVirtualCoFrontend(lambda ctx, req: None, + lambda ctx, logout_req: None, internal_attributes, conf, BASE_URL, From 415b409f15eb89ebc7e2c23face814fad82f59bc Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 24 Feb 2023 09:54:39 +0000 Subject: [PATCH 17/57] test: add logout arguments to satosa/frontends/test_openid_connect to fix tests --- tests/satosa/frontends/test_openid_connect.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/satosa/frontends/test_openid_connect.py b/tests/satosa/frontends/test_openid_connect.py index cb322e680..8ee680200 100644 --- a/tests/satosa/frontends/test_openid_connect.py +++ b/tests/satosa/frontends/test_openid_connect.py @@ -88,7 +88,8 @@ def frontend_config_with_extra_id_token_claims(self, signing_key_path): def create_frontend(self, frontend_config): # will use in-memory storage - instance = OpenIDConnectFrontend(lambda ctx, req: None, INTERNAL_ATTRIBUTES, + instance = OpenIDConnectFrontend(lambda ctx, req: None, lambda ctx, req: None, + INTERNAL_ATTRIBUTES, frontend_config, BASE_URL, "oidc_frontend") instance.register_endpoints(["foo_backend"]) return instance @@ -98,6 +99,7 @@ def create_frontend_with_extra_scopes(self, frontend_config_with_extra_scopes): internal_attributes_with_extra_scopes = copy.deepcopy(INTERNAL_ATTRIBUTES) internal_attributes_with_extra_scopes["attributes"].update(EXTRA_CLAIMS) instance = OpenIDConnectFrontend( + lambda ctx, req: None, lambda ctx, req: None, internal_attributes_with_extra_scopes, frontend_config_with_extra_scopes, @@ -447,7 +449,7 @@ def test_token_endpoint_with_extra_claims(self, context, frontend_config_with_ex def test_token_endpoint_issues_refresh_tokens_if_configured(self, context, frontend_config, authn_req): frontend_config["provider"]["refresh_token_lifetime"] = 60 * 60 * 24 * 365 - frontend = OpenIDConnectFrontend(lambda ctx, req: None, INTERNAL_ATTRIBUTES, + frontend = OpenIDConnectFrontend(lambda ctx, req: None, lambda ctx, req: None, INTERNAL_ATTRIBUTES, frontend_config, BASE_URL, "oidc_frontend") frontend.register_endpoints(["test_backend"]) From 1a9cf0dd6facf6378364aa72170bd741f332a7bc Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 24 Feb 2023 10:26:58 +0000 Subject: [PATCH 18/57] fix: add logout callback function to samlvirtualcofrontend class --- src/satosa/frontends/saml2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index d847c98bb..e6a481df7 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -951,9 +951,9 @@ class SAMLVirtualCoFrontend(SAMLFrontend): KEY_ORGANIZATION = 'organization' KEY_ORGANIZATION_KEYS = ['display_name', 'name', 'url'] - def __init__(self, auth_req_callback_func, internal_attributes, config, base_url, name): + def __init__(self, auth_req_callback_func, logout_req_callback, internal_attributes, config, base_url, name): self.has_multiple_backends = False - super().__init__(auth_req_callback_func, internal_attributes, config, base_url, name) + super().__init__(auth_req_callback_func, logout_req_callback, internal_attributes, config, base_url, name) def handle_authn_request(self, context, binding_in): """ From 2aaaf4c243524dcacb71e8f7c7261f5dcd879e40 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 24 Feb 2023 10:31:31 +0000 Subject: [PATCH 19/57] fix: add logout callback function to openid connect frontend module --- src/satosa/frontends/openid_connect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/satosa/frontends/openid_connect.py b/src/satosa/frontends/openid_connect.py index 8bd1319f7..ad2a7055b 100644 --- a/src/satosa/frontends/openid_connect.py +++ b/src/satosa/frontends/openid_connect.py @@ -39,9 +39,9 @@ class OpenIDConnectFrontend(FrontendModule): A OpenID Connect frontend module """ - def __init__(self, auth_req_callback_func, internal_attributes, conf, base_url, name): + def __init__(self, auth_req_callback_func, logout_req_callback_func, internal_attributes, conf, base_url, name): self._validate_config(conf) - super().__init__(auth_req_callback_func, internal_attributes, base_url, name) + super().__init__(auth_req_callback_func, logout_req_callback_func, internal_attributes, base_url, name) self.config = conf self.signing_key = RSAKey(key=rsa_load(conf["signing_key_path"]), use="sig", alg="RS256", From cead5431e44c6787943e7ad0020372521103cf62 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 24 Feb 2023 12:00:57 +0000 Subject: [PATCH 20/57] test: add logout arguments to satosa/backends/test_saml2 to fix tests --- tests/satosa/backends/test_saml2.py | 31 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/satosa/backends/test_saml2.py b/tests/satosa/backends/test_saml2.py index eed74db6c..c1b73e63b 100644 --- a/tests/satosa/backends/test_saml2.py +++ b/tests/satosa/backends/test_saml2.py @@ -84,7 +84,7 @@ class TestSAMLBackend: @pytest.fixture(autouse=True) def create_backend(self, sp_conf, idp_conf): setup_test_config(sp_conf, idp_conf) - self.samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, + self.samlbackend = SAMLBackend(Mock(), Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL}, "base_url", "samlbackend") @@ -168,7 +168,7 @@ def test_start_auth_redirects_directly_to_mirrored_idp( def test_redirect_to_idp_if_only_one_idp_in_metadata(self, context, sp_conf, idp_conf): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] # instantiate new backend, without any discovery service configured - samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") + samlbackend = SAMLBackend(None, None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_idp(resp, idp_conf) @@ -241,6 +241,7 @@ def test_authn_response_with_encrypted_assertion(self, sp_conf, context): sp_conf["entityid"] = "https://federation-dev-1.scienceforum.sc/Saml2/proxy_saml2_backend.xml" samlbackend = SAMLBackend( + Mock(), Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL}, @@ -279,7 +280,7 @@ def test_authn_response_with_encrypted_assertion(self, sp_conf, context): def test_backend_reads_encryption_key_from_key_file(self, sp_conf): sp_conf["key_file"] = os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem") - samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, + samlbackend = SAMLBackend(Mock(), Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL}, "base_url", "samlbackend") assert samlbackend.encryption_keys @@ -287,7 +288,7 @@ def test_backend_reads_encryption_key_from_key_file(self, sp_conf): def test_backend_reads_encryption_key_from_encryption_keypair(self, sp_conf): del sp_conf["key_file"] sp_conf["encryption_keypairs"] = [{"key_file": os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")}] - samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, + samlbackend = SAMLBackend(Mock(), Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL}, "base_url", "samlbackend") assert samlbackend.encryption_keys @@ -301,7 +302,7 @@ def test_metadata_endpoint(self, context, sp_conf): def test_get_metadata_desc(self, sp_conf, idp_conf): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] # instantiate new backend, with a single backing IdP - samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") + samlbackend = SAMLBackend(None, None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") entity_descriptions = samlbackend.get_metadata_desc() assert len(entity_descriptions) == 1 @@ -328,7 +329,7 @@ def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] # instantiate new backend, with a single backing IdP - samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") + samlbackend = SAMLBackend(None, None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") entity_descriptions = samlbackend.get_metadata_desc() assert len(entity_descriptions) == 1 @@ -356,7 +357,7 @@ def test_default_redirect_to_discovery_service_if_using_mdq( # one IdP in the metadata, but MDQ also configured so should always redirect to the discovery service sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"] - samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL,}, + samlbackend = SAMLBackend(None, None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL,}, "base_url", "saml_backend") resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) @@ -373,21 +374,21 @@ def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_not_se SAMLBackend.KEY_MEMORIZE_IDP: True, } samlbackend = SAMLBackend( - None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"] samlbackend = SAMLBackend( - None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_idp(resp, idp_conf) backend_conf[SAMLBackend.KEY_MEMORIZE_IDP] = False samlbackend = SAMLBackend( - None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) @@ -396,7 +397,7 @@ def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_not_se context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"] backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True samlbackend = SAMLBackend( - None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) @@ -417,14 +418,14 @@ def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set_tr SAMLBackend.KEY_MIRROR_FORCE_AUTHN: True, } samlbackend = SAMLBackend( - None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True samlbackend = SAMLBackend( - None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_idp(resp, idp_conf) @@ -445,14 +446,14 @@ def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set_1( SAMLBackend.KEY_MIRROR_FORCE_AUTHN: True, } samlbackend = SAMLBackend( - None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True samlbackend = SAMLBackend( - None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_idp(resp, idp_conf) From f676b132868f84d028b2f48332043de5fc05c3e6 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 28 Feb 2023 08:35:49 +0000 Subject: [PATCH 21/57] test: add logout arguments to fix backend tests --- tests/satosa/backends/test_bitbucket.py | 2 +- tests/satosa/backends/test_oauth.py | 2 +- tests/satosa/backends/test_openid_connect.py | 2 +- tests/satosa/backends/test_orcid.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/satosa/backends/test_bitbucket.py b/tests/satosa/backends/test_bitbucket.py index 192c55a84..d8c96ff8b 100644 --- a/tests/satosa/backends/test_bitbucket.py +++ b/tests/satosa/backends/test_bitbucket.py @@ -74,7 +74,7 @@ class TestBitBucketBackend(object): @pytest.fixture(autouse=True) def create_backend(self): - self.bb_backend = BitBucketBackend(Mock(), INTERNAL_ATTRIBUTES, + self.bb_backend = BitBucketBackend(Mock(), Mock(), INTERNAL_ATTRIBUTES, BB_CONFIG, "base_url", "bitbucket") @pytest.fixture diff --git a/tests/satosa/backends/test_oauth.py b/tests/satosa/backends/test_oauth.py index 0100cfaa9..63746cf6c 100644 --- a/tests/satosa/backends/test_oauth.py +++ b/tests/satosa/backends/test_oauth.py @@ -65,7 +65,7 @@ class TestFacebookBackend(object): @pytest.fixture(autouse=True) def create_backend(self): - self.fb_backend = FacebookBackend(Mock(), INTERNAL_ATTRIBUTES, FB_CONFIG, "base_url", "facebook") + self.fb_backend = FacebookBackend(Mock(), Mock(), INTERNAL_ATTRIBUTES, FB_CONFIG, "base_url", "facebook") @pytest.fixture def incoming_authn_response(self, context): diff --git a/tests/satosa/backends/test_openid_connect.py b/tests/satosa/backends/test_openid_connect.py index b282e7725..eb48cc553 100644 --- a/tests/satosa/backends/test_openid_connect.py +++ b/tests/satosa/backends/test_openid_connect.py @@ -25,7 +25,7 @@ class TestOpenIDConnectBackend(object): @pytest.fixture(autouse=True) def create_backend(self, internal_attributes, backend_config): - self.oidc_backend = OpenIDConnectBackend(Mock(), internal_attributes, backend_config, "base_url", "oidc") + self.oidc_backend = OpenIDConnectBackend(Mock(), Mock(), internal_attributes, backend_config, "base_url", "oidc") @pytest.fixture def backend_config(self): diff --git a/tests/satosa/backends/test_orcid.py b/tests/satosa/backends/test_orcid.py index 5120d4e89..bad2a2c8a 100644 --- a/tests/satosa/backends/test_orcid.py +++ b/tests/satosa/backends/test_orcid.py @@ -24,6 +24,7 @@ class TestOrcidBackend(object): @pytest.fixture(autouse=True) def create_backend(self, internal_attributes, backend_config): self.orcid_backend = OrcidBackend( + Mock(), Mock(), internal_attributes, backend_config, From 321be655ca5f6b0aa76f52222271b66951f754dc Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 28 Feb 2023 09:38:14 +0000 Subject: [PATCH 22/57] fix: add logout callback arguments to backends --- src/satosa/backends/apple.py | 9 +++++++-- src/satosa/backends/bitbucket.py | 9 +++++++-- src/satosa/backends/github.py | 6 ++++-- src/satosa/backends/linkedin.py | 9 +++++++-- src/satosa/backends/oauth.py | 12 ++++++++---- src/satosa/backends/openid_connect.py | 5 +++-- src/satosa/backends/orcid.py | 9 +++++++-- src/satosa/backends/reflector.py | 8 ++++++-- 8 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/satosa/backends/apple.py b/src/satosa/backends/apple.py index 633e22c19..cce0240ac 100644 --- a/src/satosa/backends/apple.py +++ b/src/satosa/backends/apple.py @@ -36,11 +36,13 @@ class AppleBackend(BackendModule): """Sign in with Apple backend""" - def __init__(self, auth_callback_func, internal_attributes, config, base_url, name): + def __init__(self, auth_callback_func, logout_callback_func, internal_attributes, config, base_url, name): """ Sign in with Apple backend module. :param auth_callback_func: Callback should be called by the module after the authorization in the backend is done. + :param logout_callback_func: Callback should be called by the module after logout in the + backend is done. :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -50,13 +52,16 @@ def __init__(self, auth_callback_func, internal_attributes, config, base_url, na :type auth_callback_func: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response + :type logout_callback_func: + (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :type config: dict[str, dict[str, str] | list[str]] :type base_url: str :type name: str """ - super().__init__(auth_callback_func, internal_attributes, base_url, name) + super().__init__(auth_callback_func, logout_callback_func, internal_attributes, base_url, name) self.auth_callback_func = auth_callback_func + self.logout_callback_func = logout_callback_func self.config = config self.client = _create_client( config["provider_metadata"], diff --git a/src/satosa/backends/bitbucket.py b/src/satosa/backends/bitbucket.py index 6932ce901..33b1012e8 100644 --- a/src/satosa/backends/bitbucket.py +++ b/src/satosa/backends/bitbucket.py @@ -19,10 +19,12 @@ class BitBucketBackend(_OAuthBackend): logprefix = "BitBucket Backend:" - def __init__(self, outgoing, internal_attributes, config, base_url, name): + def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): """BitBucket backend constructor :param outgoing: Callback should be called by the module after the authorization in the backend is done. + :param logout: Callback should be called by the module after logout in + the backend is done. :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -32,6 +34,9 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response + :type logout: + (satosa.context.Context, satosa.internal.InternalData) -> + satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :type config: dict[str, dict[str, str] | list[str] | str] :type base_url: str @@ -39,7 +44,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): """ config.setdefault('response_type', 'code') config['verify_accesstoken_state'] = False - super().__init__(outgoing, internal_attributes, config, base_url, + super().__init__(outgoing, logout, internal_attributes, config, base_url, name, 'bitbucket', 'account_id') def get_request_args(self, get_state=stateID): diff --git a/src/satosa/backends/github.py b/src/satosa/backends/github.py index b04906f56..e5066aaf7 100644 --- a/src/satosa/backends/github.py +++ b/src/satosa/backends/github.py @@ -21,10 +21,12 @@ class GitHubBackend(_OAuthBackend): """GitHub OAuth 2.0 backend""" - def __init__(self, outgoing, internal_attributes, config, base_url, name): + def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): """GitHub backend constructor :param outgoing: Callback should be called by the module after the authorization in the backend is done. + :param logout: Callback should be called by the module after logout + in the backend is done. :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -42,7 +44,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): config.setdefault('response_type', 'code') config['verify_accesstoken_state'] = False super().__init__( - outgoing, internal_attributes, config, base_url, name, 'github', + outgoing, logout, internal_attributes, config, base_url, name, 'github', 'id') def start_auth(self, context, internal_request, get_state=stateID): diff --git a/src/satosa/backends/linkedin.py b/src/satosa/backends/linkedin.py index 06a5cbac8..660aef999 100644 --- a/src/satosa/backends/linkedin.py +++ b/src/satosa/backends/linkedin.py @@ -22,10 +22,12 @@ class LinkedInBackend(_OAuthBackend): """LinkedIn OAuth 2.0 backend""" - def __init__(self, outgoing, internal_attributes, config, base_url, name): + def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): """LinkedIn backend constructor :param outgoing: Callback should be called by the module after the authorization in the backend is done. + :param logout: Callback should be called by the module after logout + in the backend is done :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -35,6 +37,9 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response + :type logout: + (satosa.context.Context, satosa.internal.InternalData) -> + satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :type config: dict[str, dict[str, str] | list[str] | str] :type base_url: str @@ -43,7 +48,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): config.setdefault('response_type', 'code') config['verify_accesstoken_state'] = False super().__init__( - outgoing, internal_attributes, config, base_url, name, 'linkedin', + outgoing, logout, internal_attributes, config, base_url, name, 'linkedin', 'id') def start_auth(self, context, internal_request, get_state=stateID): diff --git a/src/satosa/backends/oauth.py b/src/satosa/backends/oauth.py index 2308f1eee..3d2a7bd37 100644 --- a/src/satosa/backends/oauth.py +++ b/src/satosa/backends/oauth.py @@ -32,7 +32,7 @@ class _OAuthBackend(BackendModule): See satosa.backends.oauth.FacebookBackend. """ - def __init__(self, outgoing, internal_attributes, config, base_url, name, external_type, user_id_attr): + def __init__(self, outgoing, logout, internal_attributes, config, base_url, name, external_type, user_id_attr): """ :param outgoing: Callback should be called by the module after the authorization in the backend is done. @@ -52,7 +52,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name, extern :type name: str :type external_type: str """ - super().__init__(outgoing, internal_attributes, base_url, name) + super().__init__(outgoing, logout, internal_attributes, base_url, name) self.config = config self.redirect_url = "%s/%s" % (self.config["base_url"], self.config["authz_page"]) self.external_type = external_type @@ -190,11 +190,13 @@ class FacebookBackend(_OAuthBackend): """ DEFAULT_GRAPH_ENDPOINT = "https://graph.facebook.com/v2.5/me" - def __init__(self, outgoing, internal_attributes, config, base_url, name): + def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): """ Constructor. :param outgoing: Callback should be called by the module after the authorization in the backend is done. + :param logout: Callback should be called by the module after the logout in the backend is + done. :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -204,6 +206,8 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response + :type logout: + (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :type config: dict[str, dict[str, str] | list[str] | str] :type base_url: str @@ -211,7 +215,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): """ config.setdefault("response_type", "code") config["verify_accesstoken_state"] = False - super().__init__(outgoing, internal_attributes, config, base_url, name, "facebook", "id") + super().__init__(outgoing, logout, internal_attributes, config, base_url, name, "facebook", "id") def get_request_args(self, get_state=stateID): request_args = super().get_request_args(get_state=get_state) diff --git a/src/satosa/backends/openid_connect.py b/src/satosa/backends/openid_connect.py index 87772f565..c82230b87 100644 --- a/src/satosa/backends/openid_connect.py +++ b/src/satosa/backends/openid_connect.py @@ -33,7 +33,7 @@ class OpenIDConnectBackend(BackendModule): OIDC module """ - def __init__(self, auth_callback_func, internal_attributes, config, base_url, name): + def __init__(self, auth_callback_func, logout_callback_func, internal_attributes, config, base_url, name): """ OIDC backend module. :param auth_callback_func: Callback should be called by the module after the authorization @@ -52,8 +52,9 @@ def __init__(self, auth_callback_func, internal_attributes, config, base_url, na :type base_url: str :type name: str """ - super().__init__(auth_callback_func, internal_attributes, base_url, name) + super().__init__(auth_callback_func, logout_callback_func, internal_attributes, base_url, name) self.auth_callback_func = auth_callback_func + self.logout_callback_func = logout_callback_func self.config = config self.client = _create_client( config["provider_metadata"], diff --git a/src/satosa/backends/orcid.py b/src/satosa/backends/orcid.py index aaa18b7e5..d64e66331 100644 --- a/src/satosa/backends/orcid.py +++ b/src/satosa/backends/orcid.py @@ -21,10 +21,12 @@ class OrcidBackend(_OAuthBackend): """Orcid OAuth 2.0 backend""" - def __init__(self, outgoing, internal_attributes, config, base_url, name): + def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): """Orcid backend constructor :param outgoing: Callback should be called by the module after the authorization in the backend is done. + :param logout: Callback should be called by the module after + logout in the backend is done. :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -34,6 +36,9 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response + :type logout: + (satosa.context.Context, satosa.internal.InternalData) -> + satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :type config: dict[str, dict[str, str] | list[str] | str] :type base_url: str @@ -42,7 +47,7 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): config.setdefault('response_type', 'code') config['verify_accesstoken_state'] = False super().__init__( - outgoing, internal_attributes, config, base_url, name, 'orcid', + outgoing, logout, internal_attributes, config, base_url, name, 'orcid', 'orcid') def get_request_args(self, get_state=stateID): diff --git a/src/satosa/backends/reflector.py b/src/satosa/backends/reflector.py index 6702dc733..81b81ebf9 100644 --- a/src/satosa/backends/reflector.py +++ b/src/satosa/backends/reflector.py @@ -16,10 +16,12 @@ class ReflectorBackend(BackendModule): ENTITY_ID = ORG_NAME = AUTH_CLASS_REF = SUBJECT_ID = "reflector" - def __init__(self, outgoing, internal_attributes, config, base_url, name): + def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): """ :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response + :type logout: + :type internal_attributes: dict[str, dict[str, list[str] | str]] :type config: dict[str, Any] :type base_url: str @@ -27,12 +29,14 @@ def __init__(self, outgoing, internal_attributes, config, base_url, name): :param outgoing: Callback should be called by the module after the authorization in the backend is done. + :param logout: Callback should be called by the module after logout + in the backend is done. :param internal_attributes: Internal attribute map :param config: The module config :param base_url: base url of the service :param name: name of the plugin """ - super().__init__(outgoing, internal_attributes, base_url, name) + super().__init__(outgoing, logout, internal_attributes, base_url, name) def start_auth(self, context, internal_req): """ From f908453e0f8b08436b3c86b38a71c0112ef99a9b Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 28 Feb 2023 13:08:19 +0000 Subject: [PATCH 23/57] fix: add logout argument to ping frontend --- src/satosa/frontends/ping.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/satosa/frontends/ping.py b/src/satosa/frontends/ping.py index 8eda3948c..e73890f98 100644 --- a/src/satosa/frontends/ping.py +++ b/src/satosa/frontends/ping.py @@ -15,8 +15,8 @@ class PingFrontend(satosa.frontends.base.FrontendModule): 200 OK, intended to be used as a simple heartbeat monitor. """ - def __init__(self, auth_req_callback_func, internal_attributes, config, base_url, name): - super().__init__(auth_req_callback_func, internal_attributes, base_url, name) + def __init__(self, auth_req_callback_func, logout_callback_func, internal_attributes, config, base_url, name): + super().__init__(auth_req_callback_func, logout_callback_func, internal_attributes, base_url, name) self.config = config From f4c87601562d530eda6a6040acfb05a17518c5e9 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Thu, 2 Mar 2023 13:38:26 +0000 Subject: [PATCH 24/57] test: add logout arguments to fix failing test --- tests/satosa/test_routing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/satosa/test_routing.py b/tests/satosa/test_routing.py index be23456ad..76f7f330f 100644 --- a/tests/satosa/test_routing.py +++ b/tests/satosa/test_routing.py @@ -13,11 +13,11 @@ class TestModuleRouter: def create_router(self): backends = [] for provider in BACKEND_NAMES: - backends.append(TestBackend(None, {"attributes": {}}, None, None, provider)) + backends.append(TestBackend(None, None, {"attributes": {}}, None, None, provider)) frontends = [] for receiver in FRONTEND_NAMES: - frontends.append(TestFrontend(None, {"attributes": {}}, None, None, receiver)) + frontends.append(TestFrontend(None, None, {"attributes": {}}, None, None, receiver)) request_micro_service_name = "RequestService" response_micro_service_name = "ResponseService" From 6358adeb3b7f83f7109f802a22fca027725e1d71 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 14 Mar 2023 08:36:14 +0000 Subject: [PATCH 25/57] fix: handle case where entity_id is None in start_logout --- src/satosa/backends/saml2.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index efcf17b94..33b1a225c 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -204,10 +204,14 @@ def start_logout(self, context, internal_req, internal_authn_resp): :type context: satosa.context.Context :type internal_req: satosa.internal.InternalData - :rtype: + :rtype: satosa.response.Response """ - entity_id = self.get_idp_entity_id(context) + entity_id = internal_authn_resp["auth_info"]["issuer"] + if entity_id is None: + message = "Logout Failed" + status = "500 FAILED" + return Response(message=message, status=status) return self.logout_request(context, entity_id, internal_authn_resp) def disco_query(self, context): From 22d9a9b27c53f621c74676c63ec97d2b8888f96c Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 4 Apr 2023 10:32:10 +0000 Subject: [PATCH 26/57] test: add single_logout_service endpoints to test configuration --- tests/conftest.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9e7a5e18f..dc50c0ca9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,7 +47,10 @@ def sp_conf(cert_and_key): "assertion_consumer_service": [ ("%s/acs/redirect" % sp_base, BINDING_HTTP_REDIRECT) ], - "discovery_response": [("%s/disco" % sp_base, BINDING_DISCO)] + "discovery_response": [("%s/disco" % sp_base, BINDING_DISCO)], + "single_logout_service": [ + ("%s/sp/slo/redirect" % sp_base, BINDING_HTTP_REDIRECT), + ("%s/sp/slo/post" % sp_base, BINDING_HTTP_POST)] }, "want_response_signed": False, "allow_unsolicited": True, @@ -76,6 +79,10 @@ def idp_conf(cert_and_key): "single_sign_on_service": [ ("%s/sso/redirect" % idp_base, BINDING_HTTP_REDIRECT), ], + "single_logout_service": [ + ("%s/slo/redirect" % idp_base, BINDING_HTTP_REDIRECT), + ("%s/slo/post" % idp_base, BINDING_HTTP_POST) + ] }, "policy": { "default": { @@ -95,6 +102,7 @@ def idp_conf(cert_and_key): "logo": [{"text": "https://idp.example.com/static/logo.png", "width": "120", "height": "60", "lang": "en"}], }, + "session_storage": "memory" }, }, "cert_file": cert_and_key[0], @@ -137,6 +145,7 @@ def satosa_config_dict(backend_plugin_config, frontend_plugin_config, request_mi "CUSTOM_PLUGIN_MODULE_PATHS": [os.path.dirname(__file__)], "BACKEND_MODULES": [backend_plugin_config], "FRONTEND_MODULES": [frontend_plugin_config], + "DATABASE": {"name": "memory"}, "MICRO_SERVICES": [request_microservice_config, response_microservice_config], "LOGGING": {"version": 1} } @@ -199,7 +208,8 @@ def saml_frontend_config(cert_and_key, sp_conf): "service": { "idp": { "endpoints": { - "single_sign_on_service": [] + "single_sign_on_service": [], + "single_logout_service": [] }, "name": "Frontend IdP", "name_id_format": NAMEID_FORMAT_TRANSIENT, @@ -233,7 +243,9 @@ def saml_frontend_config(cert_and_key, sp_conf): "endpoints": { "single_sign_on_service": {BINDING_HTTP_POST: "sso/post", - BINDING_HTTP_REDIRECT: "sso/redirect"} + BINDING_HTTP_REDIRECT: "sso/redirect"}, + "single_logout_service": {BINDING_HTTP_REDIRECT: "slo/redirect", + BINDING_HTTP_POST: "slo/post"} } } } @@ -264,8 +276,11 @@ def saml_backend_config(idp_conf): "endpoints": { "assertion_consumer_service": [ ("{}/{}/acs/redirect".format(BASE_URL, name), BINDING_HTTP_REDIRECT)], - "discovery_response": [("{}/disco", BINDING_DISCO)] - + "discovery_response": [("{}/disco", BINDING_DISCO)], + "single_logout_service": [ + ("{}/{}/sp/slo/redirect".format(BASE_URL, name), BINDING_HTTP_REDIRECT), + ("{}/{}/sp/slo/post".format(BASE_URL, name), BINDING_HTTP_POST) + ] } } }, From b307d26cd789279666755ebf054609c0bd4482d0 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 4 Apr 2023 11:48:32 +0000 Subject: [PATCH 27/57] test: add logout callback function arguments to test utils --- tests/util.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/util.py b/tests/util.py index c26c796fe..800a2f8da 100644 --- a/tests/util.py +++ b/tests/util.py @@ -410,13 +410,17 @@ class FakeFrontend(FrontendModule): TODO comment """ - def __init__(self, handle_authn_request_func=None, internal_attributes=None, + def __init__(self, handle_authn_request_func=None, handle_logout_request_func=None, + internal_attributes=None, base_url="", name="FakeFrontend", handle_authn_response_func=None, + handle_logout_response_func=None, register_endpoints_func=None): super().__init__(None, internal_attributes, base_url, name) self.handle_authn_request_func = handle_authn_request_func self.handle_authn_response_func = handle_authn_response_func + self.handle_logout_request_func = handle_logout_request_func + self.handle_logout_response_func = handle_logout_response_func self.register_endpoints_func = register_endpoints_func def handle_authn_request(self, context, binding_in): @@ -458,8 +462,8 @@ def register_endpoints(self, backend_names): class TestBackend(BackendModule): __test__ = False - def __init__(self, auth_callback_func, internal_attributes, config, base_url, name): - super().__init__(auth_callback_func, internal_attributes, base_url, name) + def __init__(self, auth_callback_func, logout_callback_func, internal_attributes, config, base_url, name): + super().__init__(auth_callback_func, logout_callback_func, internal_attributes, base_url, name) def register_endpoints(self): return [("^{}/response$".format(self.name), self.handle_response)] @@ -478,8 +482,8 @@ def handle_response(self, context): class TestFrontend(FrontendModule): __test__ = False - def __init__(self, auth_req_callback_func, internal_attributes, config, base_url, name): - super().__init__(auth_req_callback_func, internal_attributes, base_url, name) + def __init__(self, auth_req_callback_func, logout_callback_func, internal_attributes, config, base_url, name): + super().__init__(auth_req_callback_func, logout_callback_func, internal_attributes, base_url, name) def register_endpoints(self, backend_names): url_map = [("^{}/{}/request$".format(p, self.name), self.handle_request) for p in backend_names] From 5f5dbf18f47a6a030051fe8607222fbd3516adb1 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 4 Apr 2023 12:23:19 +0000 Subject: [PATCH 28/57] test: add assertion for single logout endpoints for saml frontend in saml metadata creation --- .../metadata_creation/test_saml_metadata.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/satosa/metadata_creation/test_saml_metadata.py b/tests/satosa/metadata_creation/test_saml_metadata.py index 77e8ac1d7..b8b60bd90 100644 --- a/tests/satosa/metadata_creation/test_saml_metadata.py +++ b/tests/satosa/metadata_creation/test_saml_metadata.py @@ -43,6 +43,18 @@ def assert_single_sign_on_endpoints_for_saml_mirror_frontend(self, entity_descri expected_url = "{}/{}/{}/{}".format(BASE_URL, backend_name, encoded_target_entity_id, path) assert expected_url in sso_urls_for_binding + def assert_single_logout_endpoints_for_saml_frontend(self, entity_descriptor, saml_frontend_config, backend_names): + metadata = InMemoryMetaData(None, str(entity_descriptor)) + metadata.load() + slo = metadata.service(saml_frontend_config["config"]["idp_config"]["entityid"], "idpsso_descriptor", + "single_logout_service") + + for backend_name in backend_names: + for binding, path in saml_frontend_config["config"]["endpoints"]["single_logout_service"].items(): + slo_urls_for_binding = [endpoint["location"] for endpoint in slo[binding]] + expected_url = "{}/{}/{}".format(BASE_URL, backend_name, path) + assert expected_url in slo_urls_for_binding + def assert_assertion_consumer_service_endpoints_for_saml_backend(self, entity_descriptor, saml_backend_config): metadata = InMemoryMetaData(None, str(entity_descriptor)) metadata.load() @@ -63,6 +75,8 @@ def test_saml_frontend_with_saml_backend(self, satosa_config_dict, saml_frontend entity_descriptor = frontend_metadata[saml_frontend_config["name"]][0] self.assert_single_sign_on_endpoints_for_saml_frontend(entity_descriptor, saml_frontend_config, [saml_backend_config["name"]]) + self.assert_single_logout_endpoints_for_saml_frontend(entity_descriptor, saml_frontend_config, + [saml_backend_config["name"]]) assert len(backend_metadata) == 1 self.assert_assertion_consumer_service_endpoints_for_saml_backend( backend_metadata[saml_backend_config["name"]][0], @@ -79,6 +93,8 @@ def test_saml_frontend_with_oidc_backend(self, satosa_config_dict, saml_frontend entity_descriptor = frontend_metadata[saml_frontend_config["name"]][0] self.assert_single_sign_on_endpoints_for_saml_frontend(entity_descriptor, saml_frontend_config, [oidc_backend_config["name"]]) + self.assert_single_logout_endpoints_for_saml_frontend(entity_descriptor, saml_frontend_config, + [oidc_backend_config["name"]]) # OIDC backend does not produce any SAML metadata assert not backend_metadata @@ -95,6 +111,8 @@ def test_saml_frontend_with_multiple_backends(self, satosa_config_dict, saml_fro self.assert_single_sign_on_endpoints_for_saml_frontend(entity_descriptor, saml_frontend_config, [saml_backend_config["name"], oidc_backend_config["name"]]) + self.assert_single_logout_endpoints_for_saml_frontend(entity_descriptor, saml_frontend_config, + [oidc_backend_config["name"]]) # only the SAML backend produces SAML metadata assert len(backend_metadata) == 1 self.assert_assertion_consumer_service_endpoints_for_saml_backend( @@ -189,6 +207,8 @@ def test_two_saml_frontends(self, satosa_config_dict, saml_frontend_config, saml entity_descriptor = saml_entities[0] self.assert_single_sign_on_endpoints_for_saml_frontend(entity_descriptor, saml_frontend_config, [oidc_backend_config["name"]]) + self.assert_single_logout_endpoints_for_saml_frontend(entity_descriptor, saml_frontend_config, + [oidc_backend_config["name"]]) mirrored_saml_entities = frontend_metadata[saml_mirror_frontend_config["name"]] assert len(mirrored_saml_entities) == 1 From dd0d7d4022c9468184cb2c9a7d86d16dec84c330 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Wed, 5 Apr 2023 10:36:05 +0000 Subject: [PATCH 29/57] test: add assertion for single logout service endpoints for saml backend --- .../metadata_creation/test_saml_metadata.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/satosa/metadata_creation/test_saml_metadata.py b/tests/satosa/metadata_creation/test_saml_metadata.py index b8b60bd90..68f999a7e 100644 --- a/tests/satosa/metadata_creation/test_saml_metadata.py +++ b/tests/satosa/metadata_creation/test_saml_metadata.py @@ -64,6 +64,15 @@ def assert_assertion_consumer_service_endpoints_for_saml_backend(self, entity_de "assertion_consumer_service"]: assert acs[binding][0]["location"] == url + def assert_single_logout_endpoints_for_saml_backend(self, entity_descriptor, saml_backend_config): + metadata = InMemoryMetaData(None, str(entity_descriptor)) + metadata.load() + slo = metadata.service(saml_backend_config["config"]["sp_config"]["entityid"], "spsso_descriptor", + "single_logout_service") + for url, binding in saml_backend_config["config"]["sp_config"]["service"]["sp"]["endpoints"][ + "single_logout_service"]: + assert slo[binding][0]["location"] == url + def test_saml_frontend_with_saml_backend(self, satosa_config_dict, saml_frontend_config, saml_backend_config): satosa_config_dict["FRONTEND_MODULES"] = [saml_frontend_config] satosa_config_dict["BACKEND_MODULES"] = [saml_backend_config] @@ -81,6 +90,9 @@ def test_saml_frontend_with_saml_backend(self, satosa_config_dict, saml_frontend self.assert_assertion_consumer_service_endpoints_for_saml_backend( backend_metadata[saml_backend_config["name"]][0], saml_backend_config) + self.assert_single_logout_endpoints_for_saml_backend( + backend_metadata[saml_backend_config["name"]][0], + saml_backend_config) def test_saml_frontend_with_oidc_backend(self, satosa_config_dict, saml_frontend_config, oidc_backend_config): satosa_config_dict["FRONTEND_MODULES"] = [saml_frontend_config] @@ -118,6 +130,9 @@ def test_saml_frontend_with_multiple_backends(self, satosa_config_dict, saml_fro self.assert_assertion_consumer_service_endpoints_for_saml_backend( backend_metadata[saml_backend_config["name"]][0], saml_backend_config) + self.assert_single_logout_endpoints_for_saml_backend( + backend_metadata[saml_backend_config["name"]][0], + saml_backend_config) def test_saml_mirror_frontend_with_saml_backend_with_multiple_target_providers(self, satosa_config_dict, idp_conf, saml_mirror_frontend_config, @@ -145,6 +160,9 @@ def test_saml_mirror_frontend_with_saml_backend_with_multiple_target_providers(s self.assert_assertion_consumer_service_endpoints_for_saml_backend( backend_metadata[saml_backend_config["name"]][0], saml_backend_config) + self.assert_single_logout_endpoints_for_saml_backend( + backend_metadata[saml_backend_config["name"]][0], + saml_backend_config) def test_saml_mirror_frontend_with_oidc_backend(self, satosa_config_dict, saml_mirror_frontend_config, oidc_backend_config): @@ -191,6 +209,9 @@ def test_saml_mirror_frontend_with_multiple_backends(self, satosa_config_dict, i self.assert_assertion_consumer_service_endpoints_for_saml_backend( backend_metadata[saml_backend_config["name"]][0], saml_backend_config) + self.assert_single_logout_endpoints_for_saml_backend( + backend_metadata[saml_backend_config["name"]][0], + saml_backend_config) def test_two_saml_frontends(self, satosa_config_dict, saml_frontend_config, saml_mirror_frontend_config, oidc_backend_config): From 6a171e537bbb256861bb5c551c39340d97fe37e6 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Thu, 6 Apr 2023 08:06:45 +0000 Subject: [PATCH 30/57] test: update rontends/test_saml2 with single_logout_service endpoints --- tests/satosa/frontends/test_saml2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/satosa/frontends/test_saml2.py b/tests/satosa/frontends/test_saml2.py index 69f8fb4b2..af34f8b14 100644 --- a/tests/satosa/frontends/test_saml2.py +++ b/tests/satosa/frontends/test_saml2.py @@ -44,7 +44,9 @@ } ENDPOINTS = {"single_sign_on_service": {BINDING_HTTP_REDIRECT: "sso/redirect", - BINDING_HTTP_POST: "sso/post"}} + BINDING_HTTP_POST: "sso/post"}, + "single_logout_service": {BINDING_HTTP_REDIRECT: "slo/redirect", + BINDING_HTTP_POST: "slo/post"}} BASE_URL = "https://satosa-idp.example.com" From 3b67c6aa2f12ecba695f6a9c732dda88f291e25d Mon Sep 17 00:00:00 2001 From: sebulibah Date: Thu, 6 Apr 2023 13:59:54 +0000 Subject: [PATCH 31/57] refactor: improve error handling for logout request construction on saml backend --- src/satosa/backends/saml2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index 33b1a225c..3115562b0 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -425,7 +425,8 @@ def logout_request(self, context, entity_id, internal_authn_resp): msg = "Failed to construct the LogoutRequest for state" logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline, exc_info=True) - raise SATOSAUnknownError + status = "500 FAILED" + return Response(message=msg, status=status) return make_saml_response(binding, ht_args) def logout_response(self, context, binding): From 1d470ea7cbfdfe1b03d0704a858424e6b81f0e3e Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 25 Apr 2023 12:25:04 +0000 Subject: [PATCH 32/57] feat: improve logout response handling --- src/satosa/backends/saml2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index aba6e518f..a2d1c0923 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -510,10 +510,10 @@ def logout_response(self, context, binding): msg = "Failed to parse logout response for state" logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline, exc_info=True) - raise SATOSAUnknownError(context.state, "Failed to parse logout response") from err - message = "Logout {}".format("OK" if logout_response else "Failed") - status = "200 OK" if logout_response else "500 FAILED" - return Response(message=message, status=status) + message = "Logout Failed" + status = "500 FAILED" + return Response(message=message, status=status) + return self.logout_callback_func(context) def disco_response(self, context): """ From 8f2a3c13079e9d4fb4001625a7687b5a9a0af800 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 25 Apr 2023 12:54:11 +0000 Subject: [PATCH 33/57] feat: delete session before proceeding to the saml backend to handle logout --- src/satosa/base.py | 10 +++++----- src/satosa/frontends/saml2.py | 12 ++++++++---- src/satosa/store.py | 14 ++++++++++---- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/satosa/base.py b/src/satosa/base.py index 05aa49b94..fc4a21542 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -135,6 +135,7 @@ def _logout_req_finish(self, context, internal_request): backend = self.module_router.backend_routing(context) context.request = None internal_authn_resp = self.db.get_authn_resp(context.state) + self.db.delete_session(context.state) return backend.start_logout(context, internal_request, internal_authn_resp) def _auth_resp_finish(self, context, internal_response): @@ -151,12 +152,11 @@ def _auth_resp_finish(self, context, internal_response): frontend = self.module_router.frontend_routing(context) return frontend.handle_authn_response(context, internal_response) - def _logout_resp_finish(self, context, internal_response): - self.db.delete_session(context.state) + def _logout_resp_finish(self, context): context.request = None frontend = self.module_router.frontend_routing(context) - return frontend.handle_logout_response(context, internal_response) + return frontend.handle_logout_response(context) def _auth_resp_callback_func(self, context, internal_response): """ @@ -189,7 +189,7 @@ def _auth_resp_callback_func(self, context, internal_response): return self._auth_resp_finish(context, internal_response) - def _logout_resp_callback_func(self, context, internal_response): + def _logout_resp_callback_func(self, context): """ This function is called by a backend module when logout is complete @@ -202,7 +202,7 @@ def _logout_resp_callback_func(self, context, internal_response): """ context.request = None context.state["ROUTER"] = "idp" - return self._logout_resp_finish(context, internal_response) + return self._logout_resp_finish(context) def _handle_satosa_authentication_error(self, error): """ diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 80286e7ee..2251aa03b 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -131,13 +131,13 @@ def handle_logout_request(self, context, binding_in): """ return self._handle_logout_request(context, binding_in, self.idp) - def handle_logout_response(self, context, binding_in): + def handle_logout_response(self, context): """ See super class method satosa.frontends.base.FrontendModule#handle_logout_response :type context: satosa.context.Context :type binding_in: str """ - return self._handle_logout_response(context, binding_in, self.idp) + return self._handle_logout_response(context) def handle_backend_error(self, exception): """ @@ -590,7 +590,7 @@ def _handle_authn_response(self, context, internal_response, idp): del context.state[self.name] return make_saml_response(resp_args["binding"], http_args) - def _handle_logout_response(self, context, internal_response, idp): + def _handle_logout_response(self, context): """ See super class method satosa.frontends.base.FrontendModule#handle_logout_response :type context: satosa.context.Context @@ -601,7 +601,11 @@ def _handle_logout_response(self, context, internal_response, idp): :param internal_response: the internal logout response :param idp: the saml frontend idp """ - return NotImplementedError() + msg = "Logout Complete" + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logger.debug(logline, exc_info=True) + status = "200 OK" + return Response(message=msg, status=status) def _handle_backend_error(self, exception, idp): """ diff --git a/src/satosa/store.py b/src/satosa/store.py index a1900b94d..0b926a34e 100644 --- a/src/satosa/store.py +++ b/src/satosa/store.py @@ -3,7 +3,7 @@ def __init__(self, config): self.db_config = config["DATABASE"] -class SessionStorage: +class SessionStorage(Storage): """ In-memory storage """ @@ -54,7 +54,13 @@ def __init__(self, config): USER = self.db_config["user"] PWD = self.db_config["password"] - engine = create_engine(f"postgresql://{USER}:{PWD}@{HOST}:{PORT}/{DB_NAME}") + engine = create_engine("postgresql://{USER}:{PWD}@{HOST}:{PORT}/{DB_NAME}".format( + USER=USER, + PWD=PWD, + HOST=HOST, + PORT=PORT, + DB_NAME=DB_NAME + )) Base.metadata.create_all(engine) self.Session = sessionmaker(bind=engine) @@ -76,8 +82,8 @@ def get_authn_resp(self, state): authn_response = vars(authn_response[-1])["authn_response"] return authn_response - def delete_session(self, state, response_id): + def delete_session(self, state): session = self.Session() - session.query(AuthnResponse).filter_by(id=response_id).delete() + session.query(AuthnResponse).filter(AuthnResponse.session_id == state["SESSION_ID"]).delete() session.commit() session.close() From fcc57ee90258e672e7c34eb7d9cdc92c4367f5c0 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Wed, 5 Jul 2023 12:08:53 +0000 Subject: [PATCH 34/57] fix(store): remove invalid parameter in delete_session method --- src/satosa/store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/satosa/store.py b/src/satosa/store.py index 0b926a34e..f62315025 100644 --- a/src/satosa/store.py +++ b/src/satosa/store.py @@ -17,7 +17,7 @@ def store_authn_resp(self, state, internal_resp): def get_authn_resp(self, state): return self.authn_responses.get(state["SESSION_ID"]) - def delete_session(self, state, response_id): + def delete_session(self, state): if self.authn_responses.get(state["SESSION_ID"]): del self.authn_responses[state["SESSION_ID"]] From 4301299a042a00f8b865f10a215061de99301606 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 11 Jul 2023 08:43:09 +0000 Subject: [PATCH 35/57] refactor(frontends/saml2): check for sp sessions in the store --- src/satosa/frontends/saml2.py | 46 ++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 2251aa03b..52af9f498 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -339,27 +339,27 @@ def _handle_logout_request(self, context, binding_in, idp): ) sp_sessions = self._sp_session_info(context) - - for sp_info in sp_sessions: - for authn_statement in sp_info[1]: - if authn_statement[0].session_index == resp_args["session_indexes"][0]: - continue - else: - binding, slo_destination = self.idp.pick_binding( - "single_logout_service", None, "spsso", entity_id=sp_info[0][0] - ) - - lreq_id, lreq = self.idp.create_logout_request( - destination=slo_destination, - issuer_entity_id=sp_info[0][0], - name_id=NameID(text=sp_info[0][1].text), - session_indexes=[authn_statement[0].session_index] - ) - - http_args = self.idp.apply_binding(binding, "%s" % lreq, slo_destination) - msg = "http_args: {}".format(http_args) - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) - make_saml_response(binding, http_args) + if sp_sessions: + for sp_info in sp_sessions: + for authn_statement in sp_info[1]: + if authn_statement[0].session_index == resp_args["session_indexes"][0]: + continue + else: + binding, slo_destination = self.idp.pick_binding( + "single_logout_service", None, "spsso", entity_id=sp_info[0][0] + ) + + lreq_id, lreq = self.idp.create_logout_request( + destination=slo_destination, + issuer_entity_id=sp_info[0][0], + name_id=NameID(text=sp_info[0][1].text), + session_indexes=[authn_statement[0].session_index] + ) + + http_args = self.idp.apply_binding(binding, "%s" % lreq, slo_destination) + msg = "http_args: {}".format(http_args) + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + make_saml_response(binding, http_args) # Return logout response to SP that initiated logout if logout request contains # the element within the element @@ -395,7 +395,9 @@ def _sp_session_info(self, context): for sp in self.sp_sessions[session_id]: sp_sessions.append( (sp, self.idp.session_db.get_authn_statements(sp[1]))) - return sp_sessions + else: + pass + return sp_sessions def _get_approved_attributes(self, idp, idp_policy, sp_entity_id, state): """ From 2f56941e8fd5677f3bc6f833f223580c52860e27 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 11 Jul 2023 08:46:49 +0000 Subject: [PATCH 36/57] refactor(frontends/saml2): check for extensions in the logoutrequest --- src/satosa/frontends/saml2.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 52af9f498..332416ae2 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -364,19 +364,20 @@ def _handle_logout_request(self, context, binding_in, idp): # Return logout response to SP that initiated logout if logout request contains # the element within the element extensions = logout_req.extensions if logout_req.extensions else None - _extensions = [] - for ext in extensions.extension_elements: - _extensions.append(ext.namespace) - - if "urn:oasis:names:tc:SAML:2.0:protocol:ext:async-slo" not in _extensions: - binding, destination = self.idp.pick_binding( - "single_logout_service", None, "spsso", entity_id=logout_req.issuer.text - ) - logout_resp = self.idp.create_logout_response(logout_req, [binding]) - http_args = self.idp.apply_binding(binding, "%s" % logout_resp, destination) - msg = "http_args: {}".format(http_args) - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) - make_saml_response(binding, http_args) + if extensions is not None: + _extensions = [] + for ext in extensions.extension_elements: + _extensions.append(ext.namespace) + + if "urn:oasis:names:tc:SAML:2.0:protocol:ext:async-slo" not in _extensions: + binding, destination = self.idp.pick_binding( + "single_logout_service", None, "spsso", entity_id=logout_req.issuer.text + ) + logout_resp = self.idp.create_logout_response(logout_req, [binding]) + http_args = self.idp.apply_binding(binding, "%s" % logout_resp, destination) + msg = "http_args: {}".format(http_args) + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + make_saml_response(binding, http_args) return self.logout_req_callback_func(context, internal_req) From b3d4374ac2185580c5a86fbba46f4b7de6718b23 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Thu, 13 Jul 2023 08:56:02 +0000 Subject: [PATCH 37/57] feat(saml_util): add content-type for soap binding responses --- src/satosa/saml_util.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/satosa/saml_util.py b/src/satosa/saml_util.py index fced07568..d79b06638 100644 --- a/src/satosa/saml_util.py +++ b/src/satosa/saml_util.py @@ -1,4 +1,4 @@ -from saml2 import BINDING_HTTP_REDIRECT +from saml2 import BINDING_HTTP_REDIRECT, BINDING_SOAP from .response import SeeOther, Response @@ -13,5 +13,11 @@ def make_saml_response(binding, http_args): if binding == BINDING_HTTP_REDIRECT: headers = dict(http_args["headers"]) return SeeOther(str(headers["Location"])) + elif binding == BINDING_SOAP: + return Response( + http_args["data"], + headers=http_args["headers"], + content="application/soap+xml" + ) return Response(http_args["data"], headers=http_args["headers"]) From f222b129730e9d09f351d1ba08d4bfe8fb61cbaf Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 18 Aug 2023 13:55:15 +0000 Subject: [PATCH 38/57] fix(frontends/saml2): handle key error on receiving SAMLResponse --- src/satosa/frontends/saml2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 332416ae2..a7b89153d 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -111,9 +111,9 @@ def handle_logout_message(self, context, binding_in): :param binding_in: The binding type :return: """ - if context.request["SAMLRequest"]: + if "SAMLRequest" in context.request: return self.handle_logout_request(context, binding_in) - elif context.request["SAMLResponse"]: + elif "SAMLResponse" in context.request: return self.handle_logout_response(context, binding_in) else: return NotImplementedError() From 3eb672d020b4b4e5105f70970fb6425d0ddc830e Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 18 Aug 2023 13:59:24 +0000 Subject: [PATCH 39/57] feat(frontends/saml2): sign outbound logout requests --- src/satosa/frontends/saml2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index a7b89153d..19b0de411 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -353,7 +353,8 @@ def _handle_logout_request(self, context, binding_in, idp): destination=slo_destination, issuer_entity_id=sp_info[0][0], name_id=NameID(text=sp_info[0][1].text), - session_indexes=[authn_statement[0].session_index] + session_indexes=[authn_statement[0].session_index], + sign=True ) http_args = self.idp.apply_binding(binding, "%s" % lreq, slo_destination) From a8e127b63636467b59321cccae6b5aa4a335ff05 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Thu, 14 Sep 2023 09:46:03 +0000 Subject: [PATCH 40/57] feat: prevent redundant logout for deleted sessions --- src/satosa/backends/saml2.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index a2d1c0923..fce5c93d2 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -208,6 +208,10 @@ def start_logout(self, context, internal_req, internal_authn_resp): :rtype: satosa.response.Response """ + if internal_authn_resp is None: + message = "Session Information Deleted" + status = "500 FAILED" + return Response(message=message, status=status) entity_id = internal_authn_resp["auth_info"]["issuer"] if entity_id is None: message = "Logout Failed" From 7d4b4eb3204fcdd691557d59aceb403848c93f57 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Thu, 14 Sep 2023 10:22:40 +0000 Subject: [PATCH 41/57] fix: handle empty authn_response to prevent IndexError --- src/satosa/store.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/satosa/store.py b/src/satosa/store.py index f62315025..301abcb6a 100644 --- a/src/satosa/store.py +++ b/src/satosa/store.py @@ -79,6 +79,8 @@ def get_authn_resp(self, state): authn_response = session.query(AuthnResponse).filter( AuthnResponse.session_id == state["SESSION_ID"]).all() session.close() + if not authn_response: + return None authn_response = vars(authn_response[-1])["authn_response"] return authn_response From 8b7754294796606f273517f6552fd050490dc975 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 15 Sep 2023 14:56:31 +0000 Subject: [PATCH 42/57] feat: add function to send requests from satosa --- src/satosa/frontends/saml2.py | 7 ++++--- src/satosa/saml_util.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 19b0de411..2d5cfc352 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -33,6 +33,7 @@ from ..response import Response from ..response import ServiceError from ..saml_util import make_saml_response +from ..saml_util import propagate_logout from satosa.exception import SATOSAError import satosa.util as util @@ -114,7 +115,7 @@ def handle_logout_message(self, context, binding_in): if "SAMLRequest" in context.request: return self.handle_logout_request(context, binding_in) elif "SAMLResponse" in context.request: - return self.handle_logout_response(context, binding_in) + return self.handle_logout_response(context) else: return NotImplementedError() @@ -360,7 +361,7 @@ def _handle_logout_request(self, context, binding_in, idp): http_args = self.idp.apply_binding(binding, "%s" % lreq, slo_destination) msg = "http_args: {}".format(http_args) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) - make_saml_response(binding, http_args) + propagate_logout(binding, http_args) # Return logout response to SP that initiated logout if logout request contains # the element within the element @@ -378,7 +379,7 @@ def _handle_logout_request(self, context, binding_in, idp): http_args = self.idp.apply_binding(binding, "%s" % logout_resp, destination) msg = "http_args: {}".format(http_args) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) - make_saml_response(binding, http_args) + propagate_logout(binding, http_args) return self.logout_req_callback_func(context, internal_req) diff --git a/src/satosa/saml_util.py b/src/satosa/saml_util.py index d79b06638..8f898f309 100644 --- a/src/satosa/saml_util.py +++ b/src/satosa/saml_util.py @@ -1,4 +1,7 @@ -from saml2 import BINDING_HTTP_REDIRECT, BINDING_SOAP +import requests + +from saml2 import BINDING_HTTP_REDIRECT +from saml2 import BINDING_SOAP from .response import SeeOther, Response @@ -21,3 +24,31 @@ def make_saml_response(binding, http_args): ) return Response(http_args["data"], headers=http_args["headers"]) + + +def propagate_logout(binding, http_args): + """ + :param binding: SAML response binding + :param http_args: HTTP arguments + + :type binding: str + :type http_args: dict + """ + try: + if binding == BINDING_HTTP_REDIRECT: + headers = dict(http_args["headers"]) + requests.get(url=headers["Location"]) + elif binding == BINDING_SOAP: + requests.post( + url=http_args["url"], + headers={"Content-type": "text/xml"}, + data=http_args['data'] + ) + else: + requests.post( + url=http_args['url'], + headers=headers, + data=http_args['data'] + ) + except requests.exceptions.RequestException as err: + print("Error: {}".format(err)) From ed0a7a784dedc265a2cd224831eb65f63aff628e Mon Sep 17 00:00:00 2001 From: sebulibah Date: Mon, 18 Sep 2023 08:04:13 +0000 Subject: [PATCH 43/57] fix: make_saml_response to handle multiple binding types --- src/satosa/saml_util.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/satosa/saml_util.py b/src/satosa/saml_util.py index 8f898f309..834d3420e 100644 --- a/src/satosa/saml_util.py +++ b/src/satosa/saml_util.py @@ -16,12 +16,6 @@ def make_saml_response(binding, http_args): if binding == BINDING_HTTP_REDIRECT: headers = dict(http_args["headers"]) return SeeOther(str(headers["Location"])) - elif binding == BINDING_SOAP: - return Response( - http_args["data"], - headers=http_args["headers"], - content="application/soap+xml" - ) return Response(http_args["data"], headers=http_args["headers"]) From df3bbd1aab221415b1855f94f28366d977423fb2 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 6 Oct 2023 12:03:18 +0000 Subject: [PATCH 44/57] feat: make logout_callback optional for fontends and backends --- src/satosa/backends/base.py | 6 ++++-- src/satosa/base.py | 10 ++++++---- src/satosa/frontends/base.py | 2 +- src/satosa/plugin_loader.py | 10 +++++----- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/satosa/backends/base.py b/src/satosa/backends/base.py index 9e9ce61c3..9ea9142a6 100644 --- a/src/satosa/backends/base.py +++ b/src/satosa/backends/base.py @@ -10,14 +10,14 @@ class BackendModule(object): Base class for a backend module. """ - def __init__(self, auth_callback_func, logout_callback_func, internal_attributes, base_url, name): + def __init__(self, auth_callback_func, internal_attributes, base_url, name, logout_callback_func=None): """ :type auth_callback_func: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response - :type logout_callback_func: :type internal_attributes: dict[string, dict[str, str | list[str]]] :type base_url: str :type name: str + :type logout_callback_func: :param auth_callback_func: Callback should be called by the module after the authorization in the backend is done. @@ -26,6 +26,8 @@ def __init__(self, auth_callback_func, logout_callback_func, internal_attributes RP's expects namevice. :param base_url: base url of the service :param name: name of the plugin + :param logout_callback_func: Callback should be called by the module after + the logout in the backend is complete """ self.auth_callback_func = auth_callback_func self.logout_callback_func = logout_callback_func diff --git a/src/satosa/base.py b/src/satosa/base.py index 93c448fad..9602bdd80 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -60,12 +60,14 @@ def __init__(self, config): logger.info("Loading backend modules...") backends = load_backends(self.config, self._auth_resp_callback_func, - self._logout_resp_callback_func, - self.config["INTERNAL_ATTRIBUTES"]) + self.config["INTERNAL_ATTRIBUTES"], + self._logout_resp_callback_func + ) logger.info("Loading frontend modules...") frontends = load_frontends(self.config, self._auth_req_callback_func, - self._logout_req_callback_func, - self.config["INTERNAL_ATTRIBUTES"]) + self.config["INTERNAL_ATTRIBUTES"], + self._logout_req_callback_func + ) self.response_micro_services = [] self.request_micro_services = [] diff --git a/src/satosa/frontends/base.py b/src/satosa/frontends/base.py index 5d39dd430..a191b6b6a 100644 --- a/src/satosa/frontends/base.py +++ b/src/satosa/frontends/base.py @@ -9,7 +9,7 @@ class FrontendModule(object): Base class for a frontend module. """ - def __init__(self, auth_req_callback_func, logout_req_callback_func, internal_attributes, base_url, name): + def __init__(self, auth_req_callback_func, internal_attributes, base_url, name, logout_req_callback_func=None): """ :type auth_req_callback_func: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response diff --git a/src/satosa/plugin_loader.py b/src/satosa/plugin_loader.py index 770f8c5aa..83c7e72da 100644 --- a/src/satosa/plugin_loader.py +++ b/src/satosa/plugin_loader.py @@ -27,7 +27,7 @@ def prepend_to_import_path(import_paths): del sys.path[0:len(import_paths)] # restore sys.path -def load_backends(config, auth_callback, logout_callback, internal_attributes): +def load_backends(config, auth_callback, internal_attributes, logout_callback=None): """ Load all backend modules specified in the config @@ -55,7 +55,7 @@ def load_backends(config, auth_callback, logout_callback, internal_attributes): return backend_modules -def load_frontends(config, auth_callback, logout_callback, internal_attributes): +def load_frontends(config, auth_callback, internal_attributes, logout_callback=None): """ Load all frontend modules specified in the config @@ -160,7 +160,7 @@ def _load_plugin_config(config): raise SATOSAConfigurationError("The configuration is corrupt.") from exc -def _load_plugins(plugin_paths, plugins, plugin_filter, base_url, internal_attributes, auth_callback, logout_callback): +def _load_plugins(plugin_paths, plugins, plugin_filter, base_url, internal_attributes, auth_callback, logout_callback=None): """ Loads endpoint plugins @@ -187,8 +187,8 @@ def _load_plugins(plugin_paths, plugins, plugin_filter, base_url, internal_attri if module_class: module_config = _replace_variables_in_plugin_module_config(plugin_config["config"], base_url, plugin_config["name"]) - instance = module_class(auth_callback, logout_callback, internal_attributes, module_config, base_url, - plugin_config["name"]) + instance = module_class(auth_callback, internal_attributes, module_config, base_url, + plugin_config["name"], logout_callback) loaded_plugin_modules.append(instance) return loaded_plugin_modules From 3afae0bcd2d7de37e48030a7889202bec96eded4 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 6 Oct 2023 12:09:08 +0000 Subject: [PATCH 45/57] fix: remove logout_callback function from backend constructors --- src/satosa/backends/apple.py | 9 ++------- src/satosa/backends/bitbucket.py | 9 ++------- src/satosa/backends/github.py | 6 ++---- src/satosa/backends/linkedin.py | 9 ++------- src/satosa/backends/oauth.py | 4 ++-- src/satosa/backends/openid_connect.py | 5 ++--- src/satosa/backends/orcid.py | 9 ++------- src/satosa/backends/reflector.py | 7 ++----- 8 files changed, 16 insertions(+), 42 deletions(-) diff --git a/src/satosa/backends/apple.py b/src/satosa/backends/apple.py index bb0041c93..37f756a68 100644 --- a/src/satosa/backends/apple.py +++ b/src/satosa/backends/apple.py @@ -35,13 +35,11 @@ class AppleBackend(BackendModule): """Sign in with Apple backend""" - def __init__(self, auth_callback_func, logout_callback_func, internal_attributes, config, base_url, name): + def __init__(self, auth_callback_func, internal_attributes, config, base_url, name): """ Sign in with Apple backend module. :param auth_callback_func: Callback should be called by the module after the authorization in the backend is done. - :param logout_callback_func: Callback should be called by the module after logout in the - backend is done. :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -51,16 +49,13 @@ def __init__(self, auth_callback_func, logout_callback_func, internal_attributes :type auth_callback_func: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response - :type logout_callback_func: - (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :type config: dict[str, dict[str, str] | list[str]] :type base_url: str :type name: str """ - super().__init__(auth_callback_func, logout_callback_func, internal_attributes, base_url, name) + super().__init__(auth_callback_func, internal_attributes, base_url, name) self.auth_callback_func = auth_callback_func - self.logout_callback_func = logout_callback_func self.config = config self.client = _create_client( config["provider_metadata"], diff --git a/src/satosa/backends/bitbucket.py b/src/satosa/backends/bitbucket.py index 33b1012e8..6932ce901 100644 --- a/src/satosa/backends/bitbucket.py +++ b/src/satosa/backends/bitbucket.py @@ -19,12 +19,10 @@ class BitBucketBackend(_OAuthBackend): logprefix = "BitBucket Backend:" - def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): + def __init__(self, outgoing, internal_attributes, config, base_url, name): """BitBucket backend constructor :param outgoing: Callback should be called by the module after the authorization in the backend is done. - :param logout: Callback should be called by the module after logout in - the backend is done. :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -34,9 +32,6 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response - :type logout: - (satosa.context.Context, satosa.internal.InternalData) -> - satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :type config: dict[str, dict[str, str] | list[str] | str] :type base_url: str @@ -44,7 +39,7 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name """ config.setdefault('response_type', 'code') config['verify_accesstoken_state'] = False - super().__init__(outgoing, logout, internal_attributes, config, base_url, + super().__init__(outgoing, internal_attributes, config, base_url, name, 'bitbucket', 'account_id') def get_request_args(self, get_state=stateID): diff --git a/src/satosa/backends/github.py b/src/satosa/backends/github.py index d097882f7..70944e371 100644 --- a/src/satosa/backends/github.py +++ b/src/satosa/backends/github.py @@ -21,12 +21,10 @@ class GitHubBackend(_OAuthBackend): """GitHub OAuth 2.0 backend""" - def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): + def __init__(self, outgoing, internal_attributes, config, base_url, name): """GitHub backend constructor :param outgoing: Callback should be called by the module after the authorization in the backend is done. - :param logout: Callback should be called by the module after logout - in the backend is done. :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -44,7 +42,7 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name config.setdefault('response_type', 'code') config['verify_accesstoken_state'] = False super().__init__( - outgoing, logout, internal_attributes, config, base_url, name, 'github', + outgoing, internal_attributes, config, base_url, name, 'github', 'id') def start_auth(self, context, internal_request, get_state=stateID): diff --git a/src/satosa/backends/linkedin.py b/src/satosa/backends/linkedin.py index 17eb77092..8d3a85b4c 100644 --- a/src/satosa/backends/linkedin.py +++ b/src/satosa/backends/linkedin.py @@ -22,12 +22,10 @@ class LinkedInBackend(_OAuthBackend): """LinkedIn OAuth 2.0 backend""" - def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): + def __init__(self, outgoing, internal_attributes, config, base_url, name): """LinkedIn backend constructor :param outgoing: Callback should be called by the module after the authorization in the backend is done. - :param logout: Callback should be called by the module after logout - in the backend is done :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -37,9 +35,6 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response - :type logout: - (satosa.context.Context, satosa.internal.InternalData) -> - satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :type config: dict[str, dict[str, str] | list[str] | str] :type base_url: str @@ -48,7 +43,7 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name config.setdefault('response_type', 'code') config['verify_accesstoken_state'] = False super().__init__( - outgoing, logout, internal_attributes, config, base_url, name, 'linkedin', + outgoing, internal_attributes, config, base_url, name, 'linkedin', 'id') def start_auth(self, context, internal_request, get_state=stateID): diff --git a/src/satosa/backends/oauth.py b/src/satosa/backends/oauth.py index 56f26c143..1072d0506 100644 --- a/src/satosa/backends/oauth.py +++ b/src/satosa/backends/oauth.py @@ -32,7 +32,7 @@ class _OAuthBackend(BackendModule): See satosa.backends.oauth.FacebookBackend. """ - def __init__(self, outgoing, logout, internal_attributes, config, base_url, name, external_type, user_id_attr): + def __init__(self, outgoing, internal_attributes, config, base_url, name, external_type, user_id_attr): """ :param outgoing: Callback should be called by the module after the authorization in the backend is done. @@ -52,7 +52,7 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name :type name: str :type external_type: str """ - super().__init__(outgoing, logout, internal_attributes, base_url, name) + super().__init__(outgoing, internal_attributes, base_url, name) self.config = config self.redirect_url = "%s/%s" % (self.config["base_url"], self.config["authz_page"]) self.external_type = external_type diff --git a/src/satosa/backends/openid_connect.py b/src/satosa/backends/openid_connect.py index 3d81d4b21..58d47af9b 100644 --- a/src/satosa/backends/openid_connect.py +++ b/src/satosa/backends/openid_connect.py @@ -36,7 +36,7 @@ class OpenIDConnectBackend(BackendModule): OIDC module """ - def __init__(self, auth_callback_func, logout_callback_func, internal_attributes, config, base_url, name): + def __init__(self, auth_callback_func, internal_attributes, config, base_url, name): """ OIDC backend module. :param auth_callback_func: Callback should be called by the module after the authorization @@ -55,9 +55,8 @@ def __init__(self, auth_callback_func, logout_callback_func, internal_attributes :type base_url: str :type name: str """ - super().__init__(auth_callback_func, logout_callback_func, internal_attributes, base_url, name) + super().__init__(auth_callback_func, internal_attributes, base_url, name) self.auth_callback_func = auth_callback_func - self.logout_callback_func = logout_callback_func self.config = config cfg_verify_ssl = config["client"].get("verify_ssl", True) oidc_settings = PyoidcSettings(verify_ssl=cfg_verify_ssl) diff --git a/src/satosa/backends/orcid.py b/src/satosa/backends/orcid.py index 8026b230c..649e72451 100644 --- a/src/satosa/backends/orcid.py +++ b/src/satosa/backends/orcid.py @@ -21,12 +21,10 @@ class OrcidBackend(_OAuthBackend): """Orcid OAuth 2.0 backend""" - def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): + def __init__(self, outgoing, internal_attributes, config, base_url, name): """Orcid backend constructor :param outgoing: Callback should be called by the module after the authorization in the backend is done. - :param logout: Callback should be called by the module after - logout in the backend is done. :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -36,9 +34,6 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response - :type logout: - (satosa.context.Context, satosa.internal.InternalData) -> - satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :type config: dict[str, dict[str, str] | list[str] | str] :type base_url: str @@ -47,7 +42,7 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name config.setdefault('response_type', 'code') config['verify_accesstoken_state'] = False super().__init__( - outgoing, logout, internal_attributes, config, base_url, name, 'orcid', + outgoing, internal_attributes, config, base_url, name, 'orcid', 'orcid') def get_request_args(self, get_state=stateID): diff --git a/src/satosa/backends/reflector.py b/src/satosa/backends/reflector.py index ec349ee0f..da03fa478 100644 --- a/src/satosa/backends/reflector.py +++ b/src/satosa/backends/reflector.py @@ -17,11 +17,10 @@ class ReflectorBackend(BackendModule): ENTITY_ID = ORG_NAME = AUTH_CLASS_REF = SUBJECT_ID = "reflector" - def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): + def __init__(self, outgoing, internal_attributes, config, base_url, name): """ :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response - :type logout: :type internal_attributes: dict[str, dict[str, list[str] | str]] :type config: dict[str, Any] @@ -30,14 +29,12 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name :param outgoing: Callback should be called by the module after the authorization in the backend is done. - :param logout: Callback should be called by the module after logout - in the backend is done. :param internal_attributes: Internal attribute map :param config: The module config :param base_url: base url of the service :param name: name of the plugin """ - super().__init__(outgoing, logout, internal_attributes, base_url, name) + super().__init__(outgoing, internal_attributes, base_url, name) def start_auth(self, context, internal_req): """ From 7fa68c5c74578480210aa9c549f1dac5d3102c2a Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 6 Oct 2023 12:13:47 +0000 Subject: [PATCH 46/57] fix: remove logout_callback function from frontend constructors --- src/satosa/frontends/openid_connect.py | 4 ++-- src/satosa/frontends/ping.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/satosa/frontends/openid_connect.py b/src/satosa/frontends/openid_connect.py index b295d6533..33dc302c5 100644 --- a/src/satosa/frontends/openid_connect.py +++ b/src/satosa/frontends/openid_connect.py @@ -56,9 +56,9 @@ class OpenIDConnectFrontend(FrontendModule): A OpenID Connect frontend module """ - def __init__(self, auth_req_callback_func, logout_req_callback_func, internal_attributes, conf, base_url, name): + def __init__(self, auth_req_callback_func, internal_attributes, conf, base_url, name): self._validate_config(conf) - super().__init__(auth_req_callback_func, logout_req_callback_func, internal_attributes, base_url, name) + super().__init__(auth_req_callback_func, internal_attributes, base_url, name) self.config = conf diff --git a/src/satosa/frontends/ping.py b/src/satosa/frontends/ping.py index 828492f81..27fec279c 100644 --- a/src/satosa/frontends/ping.py +++ b/src/satosa/frontends/ping.py @@ -14,8 +14,8 @@ class PingFrontend(FrontendModule): 200 OK, intended to be used as a simple heartbeat monitor. """ - def __init__(self, auth_req_callback_func, logout_callback_func, internal_attributes, config, base_url, name): - super().__init__(auth_req_callback_func, logout_callback_func, internal_attributes, base_url, name) + def __init__(self, auth_req_callback_func, internal_attributes, config, base_url, name): + super().__init__(auth_req_callback_func, internal_attributes, base_url, name) self.config = config From 2794b6097e4eec97496ac311b43fbbc9ac953ac5 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 6 Oct 2023 13:59:13 +0000 Subject: [PATCH 47/57] test: remove unused parameter from backend test fixtures --- tests/satosa/backends/test_bitbucket.py | 2 +- tests/satosa/backends/test_oauth.py | 2 +- tests/satosa/backends/test_openid_connect.py | 2 +- tests/satosa/backends/test_orcid.py | 1 - tests/satosa/backends/test_saml2.py | 42 ++++++++++---------- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/tests/satosa/backends/test_bitbucket.py b/tests/satosa/backends/test_bitbucket.py index ea3a3b979..d6cf25bac 100644 --- a/tests/satosa/backends/test_bitbucket.py +++ b/tests/satosa/backends/test_bitbucket.py @@ -74,7 +74,7 @@ class TestBitBucketBackend(object): @pytest.fixture(autouse=True) def create_backend(self): - self.bb_backend = BitBucketBackend(Mock(), Mock(), INTERNAL_ATTRIBUTES, + self.bb_backend = BitBucketBackend(Mock(), INTERNAL_ATTRIBUTES, BB_CONFIG, "base_url", "bitbucket") @pytest.fixture diff --git a/tests/satosa/backends/test_oauth.py b/tests/satosa/backends/test_oauth.py index a8447d5fd..22afc8ee7 100644 --- a/tests/satosa/backends/test_oauth.py +++ b/tests/satosa/backends/test_oauth.py @@ -65,7 +65,7 @@ class TestFacebookBackend(object): @pytest.fixture(autouse=True) def create_backend(self): - self.fb_backend = FacebookBackend(Mock(), Mock(), INTERNAL_ATTRIBUTES, FB_CONFIG, "base_url", "facebook") + self.fb_backend = FacebookBackend(Mock(), INTERNAL_ATTRIBUTES, FB_CONFIG, "base_url", "facebook") @pytest.fixture def incoming_authn_response(self, context): diff --git a/tests/satosa/backends/test_openid_connect.py b/tests/satosa/backends/test_openid_connect.py index f3fbc5163..34bac79fe 100644 --- a/tests/satosa/backends/test_openid_connect.py +++ b/tests/satosa/backends/test_openid_connect.py @@ -25,7 +25,7 @@ class TestOpenIDConnectBackend(object): @pytest.fixture(autouse=True) def create_backend(self, internal_attributes, backend_config): - self.oidc_backend = OpenIDConnectBackend(Mock(), Mock(), internal_attributes, backend_config, "base_url", "oidc") + self.oidc_backend = OpenIDConnectBackend(Mock(), internal_attributes, backend_config, "base_url", "oidc") @pytest.fixture def backend_config(self): diff --git a/tests/satosa/backends/test_orcid.py b/tests/satosa/backends/test_orcid.py index bad2a2c8a..5120d4e89 100644 --- a/tests/satosa/backends/test_orcid.py +++ b/tests/satosa/backends/test_orcid.py @@ -24,7 +24,6 @@ class TestOrcidBackend(object): @pytest.fixture(autouse=True) def create_backend(self, internal_attributes, backend_config): self.orcid_backend = OrcidBackend( - Mock(), Mock(), internal_attributes, backend_config, diff --git a/tests/satosa/backends/test_saml2.py b/tests/satosa/backends/test_saml2.py index 6a14f205e..df35ff639 100644 --- a/tests/satosa/backends/test_saml2.py +++ b/tests/satosa/backends/test_saml2.py @@ -88,10 +88,11 @@ class TestSAMLBackend: @pytest.fixture(autouse=True) def create_backend(self, sp_conf, idp_conf): setup_test_config(sp_conf, idp_conf) - self.samlbackend = SAMLBackend(Mock(), Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, + self.samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL}, "base_url", - "samlbackend") + "samlbackend", + Mock()) def test_register_endpoints(self, sp_conf): """ @@ -172,7 +173,7 @@ def test_start_auth_redirects_directly_to_mirrored_idp( def test_redirect_to_idp_if_only_one_idp_in_metadata(self, context, sp_conf, idp_conf): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] # instantiate new backend, without any discovery service configured - samlbackend = SAMLBackend(None, None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") + samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend", None) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_idp(resp, idp_conf) @@ -217,6 +218,7 @@ def _make_authn_request(self, http_host, context, config, entity_id): config, "base_url", "samlbackend", + Mock() ) resp = self.samlbackend.authn_request(context, entity_id) req_params = dict(parse_qsl(urlparse(resp.message).query)) @@ -330,12 +332,12 @@ def test_authn_response_with_encrypted_assertion(self, sp_conf, context): sp_conf["entityid"] = "https://federation-dev-1.scienceforum.sc/Saml2/proxy_saml2_backend.xml" samlbackend = SAMLBackend( - Mock(), Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL}, "base_url", "samlbackend", + Mock() ) response_binding = BINDING_HTTP_REDIRECT relay_state = "test relay state" @@ -369,17 +371,17 @@ def test_authn_response_with_encrypted_assertion(self, sp_conf, context): def test_backend_reads_encryption_key_from_key_file(self, sp_conf): sp_conf["key_file"] = os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem") - samlbackend = SAMLBackend(Mock(), Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, + samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL}, - "base_url", "samlbackend") + "base_url", "samlbackend", Mock()) assert samlbackend.encryption_keys def test_backend_reads_encryption_key_from_encryption_keypair(self, sp_conf): del sp_conf["key_file"] sp_conf["encryption_keypairs"] = [{"key_file": os.path.join(TEST_RESOURCE_BASE_PATH, "encryption_key.pem")}] - samlbackend = SAMLBackend(Mock(), Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, + samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL}, - "base_url", "samlbackend") + "base_url", "samlbackend", Mock()) assert samlbackend.encryption_keys def test_metadata_endpoint(self, context, sp_conf): @@ -391,7 +393,7 @@ def test_metadata_endpoint(self, context, sp_conf): def test_get_metadata_desc(self, sp_conf, idp_conf): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] # instantiate new backend, with a single backing IdP - samlbackend = SAMLBackend(None, None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") + samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend", None) entity_descriptions = samlbackend.get_metadata_desc() assert len(entity_descriptions) == 1 @@ -418,7 +420,7 @@ def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf): sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] # instantiate new backend, with a single backing IdP - samlbackend = SAMLBackend(None, None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend") + samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend", None) entity_descriptions = samlbackend.get_metadata_desc() assert len(entity_descriptions) == 1 @@ -446,8 +448,8 @@ def test_default_redirect_to_discovery_service_if_using_mdq( # one IdP in the metadata, but MDQ also configured so should always redirect to the discovery service sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)] sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"] - samlbackend = SAMLBackend(None, None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL,}, - "base_url", "saml_backend") + samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL,}, + "base_url", "saml_backend", None) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) @@ -463,21 +465,21 @@ def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_not_se SAMLBackend.KEY_MEMORIZE_IDP: True, } samlbackend = SAMLBackend( - None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend", None ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"] samlbackend = SAMLBackend( - None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend", None ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_idp(resp, idp_conf) backend_conf[SAMLBackend.KEY_MEMORIZE_IDP] = False samlbackend = SAMLBackend( - None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend", None ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) @@ -486,7 +488,7 @@ def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_not_se context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"] backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True samlbackend = SAMLBackend( - None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend", None ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) @@ -507,14 +509,14 @@ def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set_tr SAMLBackend.KEY_MIRROR_FORCE_AUTHN: True, } samlbackend = SAMLBackend( - None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend", None ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True samlbackend = SAMLBackend( - None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend", None ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_idp(resp, idp_conf) @@ -535,14 +537,14 @@ def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set_1( SAMLBackend.KEY_MIRROR_FORCE_AUTHN: True, } samlbackend = SAMLBackend( - None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend", None ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL) backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True samlbackend = SAMLBackend( - None, None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend" + None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend", None ) resp = samlbackend.start_auth(context, InternalData()) assert_redirect_to_idp(resp, idp_conf) From f747cb8ae5509704df5431279bf138fdfa764dc8 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 10 Oct 2023 06:46:16 +0000 Subject: [PATCH 48/57] fix: remove logout parameter from facebook backend --- src/satosa/backends/oauth.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/satosa/backends/oauth.py b/src/satosa/backends/oauth.py index 1072d0506..3e2bd041b 100644 --- a/src/satosa/backends/oauth.py +++ b/src/satosa/backends/oauth.py @@ -189,13 +189,11 @@ class FacebookBackend(_OAuthBackend): """ DEFAULT_GRAPH_ENDPOINT = "https://graph.facebook.com/v2.5/me" - def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): + def __init__(self, outgoing, internal_attributes, config, base_url, name): """ Constructor. :param outgoing: Callback should be called by the module after the authorization in the backend is done. - :param logout: Callback should be called by the module after the logout in the backend is - done. :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and RP's expects namevice. @@ -205,8 +203,6 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response - :type logout: - (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response :type internal_attributes: dict[string, dict[str, str | list[str]]] :type config: dict[str, dict[str, str] | list[str] | str] :type base_url: str @@ -214,7 +210,7 @@ def __init__(self, outgoing, logout, internal_attributes, config, base_url, name """ config.setdefault("response_type", "code") config["verify_accesstoken_state"] = False - super().__init__(outgoing, logout, internal_attributes, config, base_url, name, "facebook", "id") + super().__init__(outgoing, internal_attributes, config, base_url, name, "facebook", "id") def get_request_args(self, get_state=stateID): request_args = super().get_request_args(get_state=get_state) From bfbbbca13a56a3bd7bf5e2da0f4a2ea57cc34133 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Mon, 16 Oct 2023 07:51:52 +0000 Subject: [PATCH 49/57] test: make logout_callback_func optional for saml2 frontend --- tests/satosa/frontends/test_saml2.py | 29 +++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/satosa/frontends/test_saml2.py b/tests/satosa/frontends/test_saml2.py index af34f8b14..d1cd2d3c3 100644 --- a/tests/satosa/frontends/test_saml2.py +++ b/tests/satosa/frontends/test_saml2.py @@ -71,8 +71,8 @@ def setup_for_authn_req(self, context, idp_conf, sp_conf, nameid_format=None, re base_url = self.construct_base_url_from_entity_id(idp_conf["entityid"]) samlfrontend = SAMLFrontend(lambda ctx, internal_req: (ctx, internal_req), - lambda ctx, internal_logout_req: (ctx, internal_logout_req), - internal_attributes, config, base_url, "saml_frontend") + internal_attributes, config, base_url, "saml_frontend", + lambda ctx, internal_logout_req: (ctx, internal_logout_req)) samlfrontend.register_endpoints(["saml"]) idp_metadata_str = create_metadata_from_config_dict(samlfrontend.idp_config) @@ -122,8 +122,9 @@ def get_auth_response(self, samlfrontend, context, internal_response, sp_conf, i ]) def test_config_error_handling(self, conf): with pytest.raises(ValueError): - SAMLFrontend(lambda ctx, req: None, lambda ctx, req: None, - INTERNAL_ATTRIBUTES, conf, "base_url", "saml_frontend") + SAMLFrontend(lambda ctx, req: None, + INTERNAL_ATTRIBUTES, conf, "base_url", "saml_frontend", + lambda ctx, req: None) def test_register_endpoints(self, idp_conf): """ @@ -137,8 +138,8 @@ def get_path_from_url(url): base_url = self.construct_base_url_from_entity_id(idp_conf["entityid"]) samlfrontend = SAMLFrontend(lambda context, internal_req: (context, internal_req), - lambda context, internal_logout_req: (context, internal_logout_req), - INTERNAL_ATTRIBUTES, config, base_url, "saml_frontend") + INTERNAL_ATTRIBUTES, config, base_url, "saml_frontend", + lambda context, internal_logout_req: (context, internal_logout_req)) providers = ["foo", "bar"] url_map = samlfrontend.register_endpoints(providers) @@ -252,7 +253,7 @@ def test_get_filter_attributes_with_sp_requested_attributes_without_friendlyname "eduPersonAffiliation", "mail", "displayName", "sn", "givenName"]}} # no op mapping for saml attribute names - samlfrontend = SAMLFrontend(None, None, internal_attributes, conf, base_url, "saml_frontend") + samlfrontend = SAMLFrontend(None, internal_attributes, conf, base_url, "saml_frontend", None) samlfrontend.register_endpoints(["testprovider"]) internal_req = InternalData( @@ -362,8 +363,9 @@ def test_sp_metadata_without_uiinfo(self, context, idp_conf, sp_conf): def test_metadata_endpoint(self, context, idp_conf): conf = {"idp_config": idp_conf, "endpoints": ENDPOINTS} - samlfrontend = SAMLFrontend(lambda ctx, req: None, lambda ctx, req: None, - INTERNAL_ATTRIBUTES, conf, "base_url", "saml_frontend") + samlfrontend = SAMLFrontend(lambda ctx, req: None, + INTERNAL_ATTRIBUTES, conf, "base_url", "saml_frontend", + lambda ctx, req: None) samlfrontend.register_endpoints(["todo"]) resp = samlfrontend._metadata_endpoint(context) headers = dict(resp.headers) @@ -405,9 +407,10 @@ class TestSAMLMirrorFrontend: @pytest.fixture(autouse=True) def create_frontend(self, idp_conf): conf = {"idp_config": idp_conf, "endpoints": ENDPOINTS} - self.frontend = SAMLMirrorFrontend(lambda ctx, req: None, lambda ctx, req: None, + self.frontend = SAMLMirrorFrontend(lambda ctx, req: None, INTERNAL_ATTRIBUTES, conf, BASE_URL, - "saml_mirror_frontend") + "saml_mirror_frontend", + lambda ctx, req: None) self.frontend.register_endpoints([self.BACKEND]) def assert_dynamic_endpoints(self, sso_endpoints): @@ -497,11 +500,11 @@ def frontend(self, idp_conf, sp_conf): # Create, register the endpoints, and then return the frontend # instance. frontend = SAMLVirtualCoFrontend(lambda ctx, req: None, - lambda ctx, logout_req: None, internal_attributes, conf, BASE_URL, - "saml_virtual_co_frontend") + "saml_virtual_co_frontend", + lambda ctx, req: None,) frontend.register_endpoints([self.BACKEND]) return frontend From 71f2af32778d28ba3cff011d3aba7e8095d837bc Mon Sep 17 00:00:00 2001 From: sebulibah Date: Mon, 16 Oct 2023 08:43:10 +0000 Subject: [PATCH 50/57] fix: move logout callback to the end in saml backend module --- src/satosa/backends/saml2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index 0acb21e7d..ea4522787 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -95,25 +95,25 @@ class SAMLBackend(BackendModule, SAMLBaseModule): VALUE_ACR_COMPARISON_DEFAULT = 'exact' - def __init__(self, outgoing, logout, internal_attributes, config, base_url, name): + def __init__(self, outgoing, internal_attributes, config, base_url, name, logout): """ :type outgoing: (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response - :type logout: :type internal_attributes: dict[str, dict[str, list[str] | str]] :type config: dict[str, Any] :type base_url: str :type name: str + :type logout: :param outgoing: Callback should be called by the module after the authorization in the backend is done. - :param logout: Logout callback :param internal_attributes: Internal attribute map :param config: The module config :param base_url: base url of the service :param name: name of the plugin + :param logout: Logout callback """ - super().__init__(outgoing, logout, internal_attributes, base_url, name) + super().__init__(outgoing, internal_attributes, base_url, name, logout) self.config = self.init_config(config) self.discosrv = config.get(SAMLBackend.KEY_DISCO_SRV) From c5f022878011b3ae52af4c8ce1b804ddcdb53613 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Mon, 16 Oct 2023 09:03:09 +0000 Subject: [PATCH 51/57] fix: move logout callback to the end in saml frontend module class constructor --- src/satosa/frontends/saml2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 95de35563..1a046d97d 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -68,10 +68,10 @@ class SAMLFrontend(FrontendModule, SAMLBaseModule): KEY_ENDPOINTS = 'endpoints' KEY_IDP_CONFIG = 'idp_config' - def __init__(self, auth_req_callback_func, logout_req_callback_func, internal_attributes, config, base_url, name): + def __init__(self, auth_req_callback_func, internal_attributes, config, base_url, name, logout_req_callback_func=None): self._validate_config(config) - super().__init__(auth_req_callback_func, logout_req_callback_func, internal_attributes, base_url, name) + super().__init__(auth_req_callback_func, internal_attributes, base_url, name, logout_req_callback_func) self.config = self.init_config(config) self.endpoints = config[self.KEY_ENDPOINTS] @@ -1001,9 +1001,9 @@ class SAMLVirtualCoFrontend(SAMLFrontend): KEY_ORGANIZATION = 'organization' KEY_ORGANIZATION_KEYS = ['display_name', 'name', 'url'] - def __init__(self, auth_req_callback_func, logout_req_callback, internal_attributes, config, base_url, name): + def __init__(self, auth_req_callback_func, internal_attributes, config, base_url, name): self.has_multiple_backends = False - super().__init__(auth_req_callback_func, logout_req_callback, internal_attributes, config, base_url, name) + super().__init__(auth_req_callback_func, internal_attributes, config, base_url, name) def handle_authn_request(self, context, binding_in): """ From bdc6942440bb24ea70e2393f28d73b886aae9683 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Mon, 16 Oct 2023 09:31:19 +0000 Subject: [PATCH 52/57] fix: make logout callback argument optional --- src/satosa/metadata_creation/saml_metadata.py | 4 ++-- tests/satosa/frontends/test_openid_connect.py | 5 ++--- tests/satosa/test_routing.py | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/satosa/metadata_creation/saml_metadata.py b/src/satosa/metadata_creation/saml_metadata.py index 2a8f2d750..f88bbaaec 100644 --- a/src/satosa/metadata_creation/saml_metadata.py +++ b/src/satosa/metadata_creation/saml_metadata.py @@ -104,8 +104,8 @@ def create_entity_descriptors(satosa_config): :type satosa_config: satosa.satosa_config.SATOSAConfig :rtype: Tuple[str, str] """ - frontend_modules = load_frontends(satosa_config, None, None, satosa_config["INTERNAL_ATTRIBUTES"]) - backend_modules = load_backends(satosa_config, None, None, satosa_config["INTERNAL_ATTRIBUTES"]) + frontend_modules = load_frontends(satosa_config, None, satosa_config["INTERNAL_ATTRIBUTES"]) + backend_modules = load_backends(satosa_config, None, satosa_config["INTERNAL_ATTRIBUTES"]) logger.info("Loaded frontend plugins: {}".format([frontend.name for frontend in frontend_modules])) logger.info("Loaded backend plugins: {}".format([backend.name for backend in backend_modules])) diff --git a/tests/satosa/frontends/test_openid_connect.py b/tests/satosa/frontends/test_openid_connect.py index bc1d7f199..ce1eec8aa 100644 --- a/tests/satosa/frontends/test_openid_connect.py +++ b/tests/satosa/frontends/test_openid_connect.py @@ -88,7 +88,7 @@ def frontend_config_with_extra_id_token_claims(self, signing_key_path): def create_frontend(self, frontend_config): # will use in-memory storage - instance = OpenIDConnectFrontend(lambda ctx, req: None, lambda ctx, req: None, + instance = OpenIDConnectFrontend(lambda ctx, req: None, INTERNAL_ATTRIBUTES, frontend_config, BASE_URL, "oidc_frontend") instance.register_endpoints(["foo_backend"]) @@ -99,7 +99,6 @@ def create_frontend_with_extra_scopes(self, frontend_config_with_extra_scopes): internal_attributes_with_extra_scopes = copy.deepcopy(INTERNAL_ATTRIBUTES) internal_attributes_with_extra_scopes["attributes"].update(EXTRA_CLAIMS) instance = OpenIDConnectFrontend( - lambda ctx, req: None, lambda ctx, req: None, internal_attributes_with_extra_scopes, frontend_config_with_extra_scopes, @@ -469,7 +468,7 @@ def test_token_endpoint_with_extra_claims(self, context, frontend_config_with_ex def test_token_endpoint_issues_refresh_tokens_if_configured(self, context, frontend_config, authn_req): frontend_config["provider"]["refresh_token_lifetime"] = 60 * 60 * 24 * 365 - frontend = OpenIDConnectFrontend(lambda ctx, req: None, lambda ctx, req: None, INTERNAL_ATTRIBUTES, + frontend = OpenIDConnectFrontend(lambda ctx, req: None, INTERNAL_ATTRIBUTES, frontend_config, BASE_URL, "oidc_frontend") frontend.register_endpoints(["test_backend"]) diff --git a/tests/satosa/test_routing.py b/tests/satosa/test_routing.py index 76f7f330f..9d9f160ce 100644 --- a/tests/satosa/test_routing.py +++ b/tests/satosa/test_routing.py @@ -13,11 +13,11 @@ class TestModuleRouter: def create_router(self): backends = [] for provider in BACKEND_NAMES: - backends.append(TestBackend(None, None, {"attributes": {}}, None, None, provider)) + backends.append(TestBackend(None, {"attributes": {}}, None, None, provider, None)) frontends = [] for receiver in FRONTEND_NAMES: - frontends.append(TestFrontend(None, None, {"attributes": {}}, None, None, receiver)) + frontends.append(TestFrontend(None, {"attributes": {}}, None, None, receiver, None)) request_micro_service_name = "RequestService" response_micro_service_name = "ResponseService" From b2ed22f4ac36c59bf19b5daa659c4ca443859a9a Mon Sep 17 00:00:00 2001 From: sebulibah Date: Mon, 16 Oct 2023 12:03:59 +0000 Subject: [PATCH 53/57] feat: introduce proxy config parameter to enable slo and load database conditionally --- src/satosa/base.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/satosa/base.py b/src/satosa/base.py index 9602bdd80..81067abb6 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -87,8 +87,10 @@ def __init__(self, config): self.config["BASE"])) self._link_micro_services(self.response_micro_services, self._auth_resp_finish) - logger.info("Loading database...") - self.db = load_database(self.config) + load_db = self.config.get("LOGOUT_ENABLED", False) + if load_db: + logger.info("Loading database...") + self.db = load_database(self.config) self.module_router = ModuleRouter(frontends, backends, self.request_micro_services + self.response_micro_services) @@ -155,8 +157,12 @@ def _auth_req_finish(self, context, internal_request): def _logout_req_finish(self, context, internal_request): backend = self.module_router.backend_routing(context) context.request = None - internal_authn_resp = self.db.get_authn_resp(context.state) - self.db.delete_session(context.state) + if hasattr(self, "db"): + internal_authn_resp = self.db.get_authn_resp(context.state) + self.db.delete_session(context.state) + else: + internal_authn_resp = None + context.state.__delete = self.config.get("CONTEXT_STATE_DELETE", True) return backend.start_logout(context, internal_request, internal_authn_resp) def _auth_resp_finish(self, context, internal_response): @@ -165,10 +171,12 @@ def _auth_resp_finish(self, context, internal_response): internal_response.attributes[user_id_to_attr] = [internal_response.subject_id] # remove all session state unless CONTEXT_STATE_DELETE is False - #context.state.delete = self.config.get("CONTEXT_STATE_DELETE", True) context.request = None - self.db.store_authn_resp(context.state, internal_response) - self.db.get_authn_resp(context.state) + if hasattr(self, "db"): + self.db.store_authn_resp(context.state, internal_response) + self.db.get_authn_resp(context.state) + else: + context.state.delete = self.config.get("CONTEXT_STATE_DELETE", True) frontend = self.module_router.frontend_routing(context) return frontend.handle_authn_response(context, internal_response) From 4f3906c3eedf6449ddbd64c6a25b25bc267a7214 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Wed, 18 Oct 2023 06:23:09 +0000 Subject: [PATCH 54/57] fix: correct typo when deleting context --- src/satosa/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/satosa/base.py b/src/satosa/base.py index 81067abb6..001694fc5 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -162,7 +162,7 @@ def _logout_req_finish(self, context, internal_request): self.db.delete_session(context.state) else: internal_authn_resp = None - context.state.__delete = self.config.get("CONTEXT_STATE_DELETE", True) + context.state.delete = self.config.get("CONTEXT_STATE_DELETE", True) return backend.start_logout(context, internal_request, internal_authn_resp) def _auth_resp_finish(self, context, internal_response): From b4e0c669fabd053f694d1326e943b2c05d03d754 Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 27 Oct 2023 08:33:30 +0000 Subject: [PATCH 55/57] feat: make logout request signing configurable for saml frontend and backend --- src/satosa/backends/saml2.py | 3 ++- src/satosa/frontends/saml2.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/satosa/backends/saml2.py b/src/satosa/backends/saml2.py index ea4522787..5fb18f52e 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -522,9 +522,10 @@ def logout_request(self, context, entity_id, internal_authn_resp): name_id = internal_authn_resp["subject_id"] name_id = NameID(format=name_id_format, text=name_id) session_indexes = internal_authn_resp["auth_info"]["session_index"] + sign = self.sp.config.getattr("logout_requests_signed", "sp") req_id, req = self.sp.create_logout_request( destination, issuer_entity_id=entity_id, name_id=name_id, - session_indexes=session_indexes, sign=True + session_indexes=session_indexes, sign=sign ) msg = "req_id: {}, req: {}".format(req_id, req) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index 1a046d97d..d441e4aa5 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -360,6 +360,7 @@ def _handle_logout_request(self, context, binding_in, idp): name_id_value = logout_req.name_id.text name_id_format = logout_req.name_id.format + sign = self.idp_config.get("service", {}).get("idp", {}).get("logout_requests_signed", True) internal_req = InternalData( subject_id=name_id_value, subject_type=name_id_format, @@ -382,7 +383,7 @@ def _handle_logout_request(self, context, binding_in, idp): issuer_entity_id=sp_info[0][0], name_id=NameID(text=sp_info[0][1].text), session_indexes=[authn_statement[0].session_index], - sign=True + sign=sign ) http_args = self.idp.apply_binding(binding, "%s" % lreq, slo_destination) From b9c1a6b7add006a2d48510fd90a5064b73ca330c Mon Sep 17 00:00:00 2001 From: sebulibah Date: Fri, 27 Oct 2023 09:30:41 +0000 Subject: [PATCH 56/57] fix: handle errors from SPs that don't support SLO during frontend propagation --- src/satosa/frontends/saml2.py | 43 ++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/satosa/frontends/saml2.py b/src/satosa/frontends/saml2.py index d441e4aa5..4efc38045 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -374,24 +374,31 @@ def _handle_logout_request(self, context, binding_in, idp): if authn_statement[0].session_index == resp_args["session_indexes"][0]: continue else: - binding, slo_destination = self.idp.pick_binding( - "single_logout_service", None, "spsso", entity_id=sp_info[0][0] - ) - - lreq_id, lreq = self.idp.create_logout_request( - destination=slo_destination, - issuer_entity_id=sp_info[0][0], - name_id=NameID(text=sp_info[0][1].text), - session_indexes=[authn_statement[0].session_index], - sign=sign - ) - - http_args = self.idp.apply_binding(binding, "%s" % lreq, slo_destination) - msg = "http_args: {}".format(http_args) - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) - propagate_logout(binding, http_args) - - # Return logout response to SP that initiated logout if logout request contains + try: + binding, slo_destination = self.idp.pick_binding( + "single_logout_service", None, "spsso", entity_id=sp_info[0][0] + ) + + lreq_id, lreq = self.idp.create_logout_request( + destination=slo_destination, + issuer_entity_id=sp_info[0][0], + name_id=NameID(text=sp_info[0][1].text), + session_indexes=[authn_statement[0].session_index], + sign=sign + ) + + http_args = self.idp.apply_binding(binding, "%s" % lreq, slo_destination) + msg = "http_args: {}".format(http_args) + logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + propagate_logout(binding, http_args) + except: + msg = { + "message": "LogoutRequest Failed", + "error": "Failed to construct the LogoutRequest for SP - {}".format(sp_info[0][0]) + } + logline = lu.LOG_FMT(id=lu.get_session_id(context.state), message=msg) + + # Return logout response to the SP that initiated logout if the logout request doesn't contain # the element within the element extensions = logout_req.extensions if logout_req.extensions else None if extensions is not None: From ce0a00242ae77d6618198927e2ac1cf9b1bea30b Mon Sep 17 00:00:00 2001 From: sebulibah Date: Tue, 28 Nov 2023 10:05:11 +0000 Subject: [PATCH 57/57] test: remove logout callback from SAMLVirtualCoFrontend --- tests/satosa/frontends/test_saml2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/satosa/frontends/test_saml2.py b/tests/satosa/frontends/test_saml2.py index d1cd2d3c3..b56b1bbef 100644 --- a/tests/satosa/frontends/test_saml2.py +++ b/tests/satosa/frontends/test_saml2.py @@ -504,7 +504,7 @@ def frontend(self, idp_conf, sp_conf): conf, BASE_URL, "saml_virtual_co_frontend", - lambda ctx, req: None,) + ) frontend.register_endpoints([self.BACKEND]) return frontend