From 28e6faa71de09493b031460d5b093f675fcbbcb5 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 cecd533db..4fe60af75 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -101,6 +101,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 @@ -560,8 +573,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 266f1e7e4aad5841a38b14883bfefd6df1107117 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 8be4572d4..d022dba47 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -92,10 +92,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 @@ -103,12 +104,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 40af19979..3394cb644 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -54,9 +54,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 = [] @@ -115,6 +117,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 @@ -163,6 +168,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 4fe60af75..55f0bf5ac 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -67,10 +67,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 f88bbaaec..2a8f2d750 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 08c5b6692b7c7638b4562efdbe1df94baeffecf8 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 3394cb644..7751b951b 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -118,13 +118,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 55f0bf5ac..361964ee7 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -112,7 +112,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): """ @@ -299,6 +299,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 ac60641711f6e05f093517d1450d96afe69dea52 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 | 124 +++++++++++++++++++++++++++++------ 2 files changed, 118 insertions(+), 20 deletions(-) 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 d022dba47..14083a545 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 @@ -29,6 +30,7 @@ from satosa.exception import SATOSAAuthenticationError from satosa.exception import SATOSAMissingStateError from satosa.exception import SATOSAAuthenticationFlowError +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 ( @@ -198,6 +200,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 @@ -284,13 +298,15 @@ def authn_request(self, context, entity_id): "message": "AuthnRequest Failed", "error": f"Selected IdP with EntityID {entity_id} is blacklisted for this backend", } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.info(logline) raise SATOSAAuthenticationError(context.state, msg) kwargs = {} target_accr = context.state.get(Context.KEY_TARGET_AUTHN_CONTEXT_CLASS_REF) - authn_context = self.construct_requested_authn_context(entity_id, target_accr=target_accr) + authn_context = self.construct_requested_authn_context( + entity_id, target_accr=target_accr) if authn_context: kwargs["requested_authn_context"] = authn_context if self.config.get(SAMLBackend.KEY_MIRROR_FORCE_AUTHN): @@ -318,7 +334,8 @@ def authn_request(self, context, entity_id): "message": "AuthnRequest Failed", "error": f"Failed to construct the AuthnRequest for state: {e}", } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.info(logline) raise SATOSAAuthenticationError(context.state, msg) from e @@ -328,7 +345,8 @@ def authn_request(self, context, entity_id): "message": "AuthnRequest Failed", "error": f"Request with duplicate id {req_id}", } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.info(logline) raise SATOSAAuthenticationError(context.state, msg) self.outstanding_queries[req_id] = req_id @@ -416,7 +434,8 @@ def authn_response(self, context, binding): "message": "Authentication failed", "error": "Received AuthN response without a SATOSA session cookie", } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.info(logline) raise SATOSAMissingStateError(msg) @@ -426,7 +445,8 @@ def authn_response(self, context, binding): "message": "Authentication failed", "error": "SAML Response not found in context.request", } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.info(logline) raise SATOSAAuthenticationError(context.state, msg) @@ -439,7 +459,8 @@ def authn_response(self, context, binding): "message": "Authentication failed", "error": f"Failed to parse Authn response: {e}", } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.debug(logline, exc_info=True) logger.info(logline) raise SATOSAAuthenticationError(context.state, msg) from e @@ -451,7 +472,8 @@ def authn_response(self, context, binding): "message": "Authentication failed", "error": f"No corresponding request with id: {req_id}", } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.info(logline) raise SATOSAAuthenticationError(context.state, msg) del self.outstanding_queries[req_id] @@ -462,7 +484,8 @@ def authn_response(self, context, binding): "message": "Authentication failed", "error": "Response state query param did not match relay state for request", } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.info(logline) raise SATOSAAuthenticationError(context.state, msg) @@ -473,6 +496,54 @@ 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 @@ -487,7 +558,8 @@ def disco_response(self, context): state = context.state if 'SATOSA_BASE' not in state: - raise SATOSAAuthenticationFlowError("Discovery response without AuthN request") + raise SATOSAAuthenticationFlowError( + "Discovery response without AuthN request") entity_id = info.get("entityID") msg = { @@ -525,11 +597,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. @@ -573,7 +647,8 @@ def _metadata_endpoint(self, context): :param context: The current context :return: response with metadata """ - msg = "Sending metadata response for entityId = {}".format(self.sp.config.entityid) + msg = "Sending metadata response for entityId = {}".format( + self.sp.config.entityid) logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.debug(logline) @@ -591,7 +666,8 @@ def register_endpoints(self): sp_endpoints = self.sp.config.getattr("endpoints", "sp") for endp, binding in sp_endpoints["assertion_consumer_service"]: parsed_endp = urlparse(endp) - url_map.append(("^%s$" % parsed_endp.path[1:], functools.partial(self.authn_response, binding=binding))) + url_map.append(("^%s$" % parsed_endp.path[1:], functools.partial( + self.authn_response, binding=binding))) if binding == BINDING_HTTP_REDIRECT: msg = " ".join( [ @@ -614,7 +690,8 @@ def register_endpoints(self): ("^%s$" % parsed_endp.path[1:], self.disco_response)) if self.expose_entityid_endpoint(): - logger.debug("Exposing backend entity endpoint = {}".format(self.sp.config.entityid)) + logger.debug("Exposing backend entity endpoint = {}".format( + self.sp.config.entityid)) parsed_entity_id = urlparse(self.sp.config.entityid) url_map.append(("^{0}".format(parsed_entity_id.path[1:]), self._metadata_endpoint)) @@ -646,7 +723,8 @@ def get_metadata_desc(self): idp_entities = self.sp.metadata.with_descriptor("idpsso") for entity_id, entity in idp_entities.items(): - description = MetadataDescription(urlsafe_b64encode(entity_id.encode("utf-8")).decode("utf-8")) + description = MetadataDescription(urlsafe_b64encode( + entity_id.encode("utf-8")).decode("utf-8")) # Add organization info try: @@ -658,7 +736,8 @@ def get_metadata_desc(self): for name_info in organization_info.get("organization_name", []): organization.add_name(name_info["text"], name_info["lang"]) for display_name_info in organization_info.get("organization_display_name", []): - organization.add_display_name(display_name_info["text"], display_name_info["lang"]) + organization.add_display_name( + display_name_info["text"], display_name_info["lang"]) for url_info in organization_info.get("organization_url", []): organization.add_url(url_info["text"], url_info["lang"]) description.organization = organization @@ -682,7 +761,8 @@ def get_metadata_desc(self): description.add_contact_person(person_desc) # Add UI info - ui_info = self.sp.metadata.extension(entity_id, "idpsso_descriptor", "{}&UIInfo".format(UI_NAMESPACE)) + ui_info = self.sp.metadata.extension( + entity_id, "idpsso_descriptor", "{}&UIInfo".format(UI_NAMESPACE)) if ui_info: ui_info = ui_info[0] ui_info_desc = UIInfoDesc() @@ -691,14 +771,18 @@ def get_metadata_desc(self): for name in ui_info.get("display_name", []): ui_info_desc.add_display_name(name["text"], name["lang"]) for logo in ui_info.get("logo", []): - ui_info_desc.add_logo(logo["text"], logo["width"], logo["height"], logo.get("lang")) + ui_info_desc.add_logo( + logo["text"], logo["width"], logo["height"], logo.get("lang")) for keywords in ui_info.get("keywords", []): - ui_info_desc.add_keywords(keywords.get("text", []), keywords.get("lang")) + ui_info_desc.add_keywords(keywords.get( + "text", []), keywords.get("lang")) for information_url in ui_info.get("information_url", []): - ui_info_desc.add_information_url(information_url.get("text"), information_url.get("lang")) + ui_info_desc.add_information_url( + information_url.get("text"), information_url.get("lang")) for privacy_statement_url in ui_info.get("privacy_statement_url", []): ui_info_desc.add_privacy_statement_url( - privacy_statement_url.get("text"), privacy_statement_url.get("lang") + privacy_statement_url.get( + "text"), privacy_statement_url.get("lang") ) description.ui_info = ui_info_desc From d7837d1f3261a80facc69eaccd126c8680e88bcd 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 361964ee7..80b873803 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -316,7 +316,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 818bd250c57bf9919751e5aa20aa9055c7141393 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 | 43 ++++++++++++++++++++++++++----------- src/satosa/plugin_loader.py | 28 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/satosa/base.py b/src/satosa/base.py index 7751b951b..85597f6e7 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -24,6 +24,7 @@ from .plugin_loader import load_frontends from .plugin_loader import load_request_microservices from .plugin_loader import load_response_microservices +from .plugin_loader import load_database from .routing import ModuleRouter from .state import State from .state import cookie_to_state @@ -70,14 +71,19 @@ def __init__(self, config): self.config["MICRO_SERVICES"], self.config["INTERNAL_ATTRIBUTES"], self.config["BASE"])) - self._link_micro_services(self.request_micro_services, self._auth_req_finish) + self._link_micro_services( + self.request_micro_services, self._auth_req_finish) self.response_micro_services.extend( load_response_microservices(self.config.get("CUSTOM_PLUGIN_MODULE_PATHS"), self.config["MICRO_SERVICES"], self.config["INTERNAL_ATTRIBUTES"], self.config["BASE"])) - self._link_micro_services(self.response_micro_services, self._auth_resp_finish) + 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) @@ -147,9 +153,11 @@ def _logout_req_finish(self, context, internal_request): 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) + user_id_to_attr = self.config["INTERNAL_ATTRIBUTES"].get( + "user_id_to_attr", None) if user_id_to_attr: - internal_response.attributes[user_id_to_attr] = [internal_response.subject_id] + 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) @@ -226,7 +234,8 @@ def _run_bound_endpoint(self, context, spec): msg = "ERROR_ID [{err_id}]\nSTATE:\n{state}".format( err_id=error.error_id, state=state ) - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.error(logline, exc_info=True) return self._handle_satosa_authentication_error(error) @@ -248,7 +257,8 @@ def _load_state(self, context): finally: context.state = state msg = f"Loaded state {state} from cookie {context.cookie}" - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.debug(logline) def _save_state(self, resp, context): @@ -303,7 +313,8 @@ def run(self, context): "error": str(e), "error_id": error_id, } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.error(logline) generic_error_url = self.config.get("ERROR_URL") if generic_error_url: @@ -317,7 +328,8 @@ def run(self, context): "error": str(e), "error_id": error_id, } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.error(logline) generic_error_url = self.config.get("ERROR_URL") if generic_error_url: @@ -331,7 +343,8 @@ def run(self, context): "error": str(e), "error_id": error_id, } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.error(logline) generic_error_url = self.config.get("ERROR_URL") if generic_error_url: @@ -345,7 +358,8 @@ def run(self, context): "error": str(e), "error_id": error_id, } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.error(logline) generic_error_url = self.config.get("ERROR_URL") if generic_error_url: @@ -359,7 +373,8 @@ def run(self, context): "error": str(e), "error_id": error_id, } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.error(logline) generic_error_url = self.config.get("ERROR_URL") if generic_error_url: @@ -373,7 +388,8 @@ def run(self, context): "error": str(e), "error_id": error_id, } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.error(logline) generic_error_url = self.config.get("ERROR_URL") if generic_error_url: @@ -387,7 +403,8 @@ def run(self, context): "error": str(e), "error_id": error_id, } - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.error(logline) generic_error_url = self.config.get("ERROR_URL") if generic_error_url: 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 02d1e417b9a4337c1e44bf34e19da7f4b581a606 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 14083a545..d493afac2 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -200,7 +200,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 @@ -210,7 +210,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): """ @@ -496,7 +496,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. @@ -521,9 +521,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( @@ -597,13 +600,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 85597f6e7..c378c2b8d 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -145,12 +145,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( @@ -162,6 +165,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 fbd1eb7e44c9107d149182f372cfe17a8a92f629 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 d493afac2..2c2a5b1ee 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -547,6 +547,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 @@ -704,6 +719,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 e453a3abdc28710fcb3e3e45941557edf0b1255e 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 80b873803..f7e8fca90 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -101,6 +101,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. @@ -114,6 +133,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 @@ -618,7 +640,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 ff8c4aeb8d0e7cc1a04f1e3d0a178262e5a6e991 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 2c2a5b1ee..9f085d142 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 SATOSAMissingStateError @@ -559,7 +560,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): @@ -657,6 +674,25 @@ def _translate_response(self, response, state): 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 a96b19b1f..0b9b9576d 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 0364a216d2009d1d46fa04808148fab67763c627 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 c378c2b8d..3fe962ab8 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -145,13 +145,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) @@ -163,7 +161,7 @@ def _auth_resp_finish(self, context, internal_response): 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) @@ -171,6 +169,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 @@ -203,7 +208,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 f7e8fca90..5100cf982 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -116,7 +116,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() @@ -133,8 +133,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): """ @@ -563,6 +568,19 @@ def _handle_authn_response(self, context, internal_response, idp): 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 c415ac716f74afe73427dd30c3e513c0c2ef3243 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 5100cf982..79985abd3 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -363,6 +363,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 3de2f3364169574ac469663692919a5fa01579ea 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 79985abd3..45189d442 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -166,6 +166,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): @@ -364,6 +365,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 @@ -383,6 +407,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): """ @@ -487,6 +527,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 276275584108185a2b2620c7a4a4c88b8dae6137 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 9f085d142..6e9f4dbd7 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -574,10 +574,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 b0f9085bf057eaaaf07d0ae579211aafd279447d 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 87f1027c633a7c902f06f72efebdc7b68c7ebaa4 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 c3daa69b1c9a039658358978465a8e7bb51e919b 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 f769b2c66..bc1d7f199 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, @@ -467,7 +469,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 7a82a954c18207caa315eb181796b18ace4ef019 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 45189d442..d61ecf11f 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -992,9 +992,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 b769079a7ec76c3b45e54a6db16c276f1a9dd3ad 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 | 35 +++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/satosa/frontends/openid_connect.py b/src/satosa/frontends/openid_connect.py index 88041b373..d41fd283c 100644 --- a/src/satosa/frontends/openid_connect.py +++ b/src/satosa/frontends/openid_connect.py @@ -56,9 +56,10 @@ 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): _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 provider_config = self.config["provider"] @@ -143,7 +144,8 @@ def handle_authn_response(self, context, internal_resp): del self.user_db[internal_resp.subject_id] del context.state[self.name] - http_response = auth_resp.request(auth_req["redirect_uri"], should_fragment_encode(auth_req)) + http_response = auth_resp.request( + auth_req["redirect_uri"], should_fragment_encode(auth_req)) return SeeOther(http_response) def handle_backend_error(self, exception): @@ -194,7 +196,8 @@ def register_endpoints(self, backend_names): if backend_name: # if there is only one backend, include its name in the path so the default routing can work - auth_endpoint = "{}/{}/{}/{}".format(self.base_url, backend_name, self.name, AuthorizationEndpoint.url) + auth_endpoint = "{}/{}/{}/{}".format(self.base_url, + backend_name, self.name, AuthorizationEndpoint.url) self.provider.configuration_information["authorization_endpoint"] = auth_endpoint auth_path = urlparse(auth_endpoint).path.lstrip("/") else: @@ -250,7 +253,8 @@ def client_registration(self, context): :return: HTTP response to the client """ try: - resp = self.provider.handle_client_registration_request(json.dumps(context.request)) + resp = self.provider.handle_client_registration_request( + json.dumps(context.request)) return Created(resp.to_json(), content="application/json") except InvalidClientRegistrationRequest as e: return BadRequest(e.to_json(), content="application/json") @@ -296,7 +300,8 @@ def _handle_authn_request(self, context): authn_req = self.provider.parse_authentication_request(request) except InvalidAuthenticationRequest as e: msg = "Error in authn req: {}".format(str(e)) - logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) + logline = lu.LOG_FMT.format( + id=lu.get_session_id(context.state), message=msg) logger.error(logline) error_url = e.to_error_url() @@ -369,14 +374,16 @@ def token_endpoint(self, context): except InvalidClientAuthentication as e: logline = "invalid client authentication at token endpoint" logger.debug(logline, exc_info=True) - error_resp = TokenErrorResponse(error='invalid_client', error_description=str(e)) + error_resp = TokenErrorResponse( + error='invalid_client', error_description=str(e)) response = Unauthorized(error_resp.to_json(), headers=[("WWW-Authenticate", "Basic")], content="application/json") return response except OAuthError as e: logline = "invalid request: {}".format(str(e)) logger.debug(logline, exc_info=True) - error_resp = TokenErrorResponse(error=e.oauth_error, error_description=str(e)) + error_resp = TokenErrorResponse( + error=e.oauth_error, error_description=str(e)) return BadRequest(error_resp.to_json(), content="application/json") def userinfo_endpoint(self, context): @@ -389,7 +396,8 @@ def userinfo_endpoint(self, context): ) return Response(response.to_json(), content="application/json") except (BearerTokenError, InvalidAccessToken) as e: - error_resp = UserInfoErrorResponse(error='invalid_token', error_description=str(e)) + error_resp = UserInfoErrorResponse( + error='invalid_token', error_description=str(e)) response = Unauthorized(error_resp.to_json(), headers=[("WWW-Authenticate", AccessToken.BEARER_TOKEN_TYPE)], content="application/json") return response @@ -406,7 +414,8 @@ def _validate_config(config): for k in {"signing_key_path", "provider"}: if k not in config: - raise ValueError("Missing configuration parameter '{}' for OpenID Connect frontend.".format(k)) + raise ValueError( + "Missing configuration parameter '{}' for OpenID Connect frontend.".format(k)) if "signing_key_id" in config and type(config["signing_key_id"]) is not str: raise ValueError( @@ -422,8 +431,10 @@ def _create_provider( user_db, cdb, ): - response_types_supported = provider_config.get("response_types_supported", ["id_token"]) - subject_types_supported = provider_config.get("subject_types_supported", ["pairwise"]) + response_types_supported = provider_config.get( + "response_types_supported", ["id_token"]) + subject_types_supported = provider_config.get( + "subject_types_supported", ["pairwise"]) scopes_supported = provider_config.get("scopes_supported", ["openid"]) extra_scopes = provider_config.get("extra_scopes") capabilities = { From f92ff927858932b4350b6b8cbaab8f8169c3b543 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 e1cc96466..6a14f205e 100644 --- a/tests/satosa/backends/test_saml2.py +++ b/tests/satosa/backends/test_saml2.py @@ -88,7 +88,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") @@ -172,7 +172,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) @@ -330,6 +330,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}, @@ -368,7 +369,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 @@ -376,7 +377,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 @@ -390,7 +391,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 @@ -417,7 +418,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 @@ -445,7 +446,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) @@ -462,21 +463,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) @@ -485,7 +486,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) @@ -506,14 +507,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) @@ -534,14 +535,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 9464941e1636be539bb691f7fe0ebbd411547708 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 d6cf25bac..ea3a3b979 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 22afc8ee7..a8447d5fd 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 34bac79fe..f3fbc5163 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 3f4e879c3badb29f6cb436d5b3323bb912a126d8 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/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 ++++++-- 7 files changed, 42 insertions(+), 16 deletions(-) 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 70944e371..d097882f7 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 8d3a85b4c..17eb77092 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 3e2bd041b..56f26c143 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 @@ -189,11 +189,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. @@ -203,6 +205,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 @@ -210,7 +214,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 58d47af9b..3d81d4b21 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, 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 @@ -55,8 +55,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 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 649e72451..8026b230c 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 6a9055485..ec349ee0f 100644 --- a/src/satosa/backends/reflector.py +++ b/src/satosa/backends/reflector.py @@ -17,10 +17,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 @@ -28,12 +30,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 845f607bb507c3750f90fbd35598c3f644776e2a 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 27fec279c..828492f81 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, 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 a315e768d036926471dcf93685001d101c56932f 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 9abce1fc4e9cd6c1549e52c8cce764ec8ecb94d5 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 6e9f4dbd7..93a47e91a 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -207,10 +207,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 8615b53515042f0e366bad00d5af67286ef30add 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 f0602a028..ae5de7275 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], @@ -135,6 +143,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} } @@ -191,7 +200,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, @@ -225,7 +235,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"} } } } @@ -256,8 +268,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 1b8dbba0e666371b703af35b7aaa6b5421829ed2 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 ccc456f4fa21bc2af523c3135eb060b47e1da310 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 0f4a2617f021868c656b169879c747ff3217b9d7 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 3aec8d26b7cbdc8c81e9a774cc42e4412fd69620 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 ab04ab9e79a9518cd56c4abd72ea2e26c8fb8dd6 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 93a47e91a..e3f1a20ad 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -549,7 +549,8 @@ def logout_request(self, context, entity_id, internal_authn_resp): 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 37c52cba2614c5facb3413664624ed6937bd7d4f 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 e3f1a20ad..176c895d9 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -578,10 +578,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 137b9083c8ba3d1826c3b6b580ffa501b9c86b2b 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 3fe962ab8..02722b420 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -151,6 +151,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): @@ -169,12 +170,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): """ @@ -207,7 +207,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 @@ -220,7 +220,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 d61ecf11f..ac05d66fd 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -133,13 +133,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): """ @@ -632,7 +632,7 @@ def _handle_authn_response(self, context, internal_response, idp): 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 @@ -643,7 +643,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 502dd5720f159dfef1ac51f8eb0a81f0b199ac66 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 cc5ea6cba1b5cc3b8d736d8e281b0355a08fc2f5 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 ac05d66fd..01909320b 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -366,27 +366,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 @@ -422,7 +422,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 e371f853c9a4deccc54334facda4ab4dad56b3fb 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 01909320b..c62bcf25a 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -391,19 +391,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 b3a41f207d30ad1acf279f3ba1abc058e986f3c3 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 d972b1357c9e21763f7ee033d6c918da19e16571 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 c62bcf25a..05099d0f8 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -113,9 +113,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 ad77c2c019e47d0320303e59f3b0783e80220733 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 05099d0f8..6d444411f 100644 --- a/src/satosa/frontends/saml2.py +++ b/src/satosa/frontends/saml2.py @@ -380,7 +380,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 9598eb32b2f6262e6e124166a1aa7a67664d1b0c 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 176c895d9..6f2002716 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -210,6 +210,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 9fb695d2f9f5a1e937a1f13f61827b70e71ed9d8 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 e58e0ef7a88c5108f58b5abd7532b749c81ae233 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 6d444411f..95de35563 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 from satosa.exception import SATOSABadRequestError from satosa.exception import SATOSAMissingStateError @@ -116,7 +117,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() @@ -387,7 +388,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 @@ -405,7 +406,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 814c47d9d90a367c76a818e8a44dbfb9859b10af 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 49680cd85aca2de057a5c1aa4b6fb1f3404743ce 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 02722b420..779c89988 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -55,12 +55,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 fdf251b5b19c3e4bca64068dab36653f9628669a 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/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 ++----- 7 files changed, 14 insertions(+), 35 deletions(-) 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 fdeb50a9c788eaeba035bb96375688f863a8f795 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 d41fd283c..1afe05974 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): _validate_config(conf) - super().__init__(auth_req_callback_func, logout_req_callback_func, + 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 c958e1b9b401c5c28704bad6a5de57fb7821ee4e 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 9d734bd08fdf4f6d099a2dc4d2228dfb46d8ea83 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 014e8788ebf1cea77a6f6297ac548220fe4ea5b5 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 af6bc4a04571037aae71d2027ef2e333ed3604b8 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 6f2002716..e2e6f2443 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 b165c55e1150cdb2d724b7dd1abb0897f450da41 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 e3dcd83b7abe59a2875e13e349d58c9c872492f0 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 29d5545bb0f1f4e1abdfc18612353d6c1d45eceb 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 779c89988..897c1996d 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -84,8 +84,10 @@ def __init__(self, config): 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) @@ -152,8 +154,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): @@ -164,10 +170,12 @@ def _auth_resp_finish(self, context, internal_response): 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 ca070ab4cd46d04b7a50c9e7b3c57f245077e012 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 897c1996d..7ce73eedf 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -159,7 +159,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 b9c9dd8c3d818f1e00aef6f30671b2382a5462ea 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 e2e6f2443..a2dcfdfdc 100644 --- a/src/satosa/backends/saml2.py +++ b/src/satosa/backends/saml2.py @@ -533,9 +533,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( 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 1c5fd724f12bee6bbf55dcc879e17e442b0fe0d7 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 7fd6379ca40c98b0a6a79473f1af52b8e41b5cea 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