From f4e909901d46b2418986a3b8ca40cfa1504145b4 Mon Sep 17 00:00:00 2001 From: peppelinux Date: Tue, 30 Mar 2021 19:08:41 +0200 Subject: [PATCH] feat: cookie parameters in proxy configuration Secure Now can be disabled via `COOKIE_SECURE: no` in proxy_conf.yaml. Default: True HttpOnly To avoid cross-site scripting (XSS) attacks, cookies set with the HttpOnly directive are inaccessible to the JavaScript Document.cookie (en-US) API; they are only sent to the server. For example, session cookies do not need to be accessed by JavaScript and should therefore be set with the HttpOnly flag. Default: True proxy_conf.yaml parameter name eg: `COOKIE_HTTPONLY: no` minor code linting --- src/satosa/base.py | 15 +++++++++++---- src/satosa/satosa_config.py | 20 ++++++++++++++------ src/satosa/state.py | 28 +++++++++++++++++++--------- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/satosa/base.py b/src/satosa/base.py index d458293e1..2402a77b5 100644 --- a/src/satosa/base.py +++ b/src/satosa/base.py @@ -199,13 +199,13 @@ def _load_state(self, context): state = cookie_to_state( context.cookie, self.config["COOKIE_STATE_NAME"], - self.config["STATE_ENCRYPTION_KEY"], + self.config["STATE_ENCRYPTION_KEY"] ) except SATOSAStateError as e: state = State() finally: context.state = state - msg = "Loaded state {state} from cookie {cookie}".format(state=state, cookie=context.cookie) + msg = f"Loaded state {state} from cookie {context.cookie}" logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg) logger.info(logline) @@ -220,8 +220,15 @@ def _save_state(self, resp, context): :param context: Session context """ - cookie = state_to_cookie(context.state, self.config["COOKIE_STATE_NAME"], "/", - self.config["STATE_ENCRYPTION_KEY"]) + cookie = state_to_cookie(context.state, + self.config["COOKIE_STATE_NAME"], + "/", + self.config["STATE_ENCRYPTION_KEY"], + self.config.get("COOKIE_DOMAIN"), + self.config.get("COOKIE_SECURE", True), + self.config.get("COOKIE_HTTPONLY", True), + self.config.get("COOKIE_MAX_AGE", "") + ) resp.headers.append(tuple(cookie.output().split(": ", 1))) def run(self, context): diff --git a/src/satosa/satosa_config.py b/src/satosa/satosa_config.py index b107e5728..b6f3a96ce 100644 --- a/src/satosa/satosa_config.py +++ b/src/satosa/satosa_config.py @@ -40,7 +40,7 @@ def __init__(self, config): # Load sensitive config from environment variables for key in SATOSAConfig.sensitive_dict_keys: - val = os.environ.get("SATOSA_{key}".format(key=key)) + val = os.environ.get(f"SATOSA_{key}") if val: self._config[key] = val @@ -56,16 +56,22 @@ def __init__(self, config): plugin_configs.append(plugin_config) break else: - raise SATOSAConfigurationError('Failed to load plugin config \'{}\''.format(config)) + raise SATOSAConfigurationError( + f"Failed to load plugin config '{config}'" + ) self._config[key] = plugin_configs for parser in parsers: - _internal_attributes = parser(self._config["INTERNAL_ATTRIBUTES"]) + _internal_attributes = parser( + self._config["INTERNAL_ATTRIBUTES"] + ) if _internal_attributes is not None: self._config["INTERNAL_ATTRIBUTES"] = _internal_attributes break if not self._config["INTERNAL_ATTRIBUTES"]: - raise SATOSAConfigurationError("Could not load attribute mapping from 'INTERNAL_ATTRIBUTES.") + raise SATOSAConfigurationError( + "Could not load attribute mapping from 'INTERNAL_ATTRIBUTES." + ) def _verify_dict(self, conf): """ @@ -86,8 +92,10 @@ def _verify_dict(self, conf): raise SATOSAConfigurationError("Missing key '%s' in config" % key) for key in SATOSAConfig.sensitive_dict_keys: - if key not in conf and "SATOSA_{key}".format(key=key) not in os.environ: - raise SATOSAConfigurationError("Missing key '%s' from config and ENVIRONMENT" % key) + if key not in conf and f"SATOSA_{key}" not in os.environ: + raise SATOSAConfigurationError( + f"Missing key '{key}' from config and ENVIRONMENT" + ) def __getitem__(self, item): """ diff --git a/src/satosa/state.py b/src/satosa/state.py index 6aaa5154b..7c46f7c36 100644 --- a/src/satosa/state.py +++ b/src/satosa/state.py @@ -26,7 +26,14 @@ _SESSION_ID_KEY = "SESSION_ID" -def state_to_cookie(state, name, path, encryption_key): +def state_to_cookie(state:str, + name:str, + path:str, + encryption_key:str, + domain:str=None, + secure:bool=True, + httponly:bool=True, + max_age:str=""): """ Saves a state to a cookie @@ -42,15 +49,17 @@ def state_to_cookie(state, name, path, encryption_key): :param encryption_key: Key to encrypt the state information :return: A cookie """ - cookie_data = "" if state.delete else state.urlstate(encryption_key) - cookie = SimpleCookie() cookie[name] = cookie_data cookie[name]["samesite"] = "None" - cookie[name]["secure"] = True + cookie[name]["secure"] = secure + if httponly: + cookie[name]["httponly"] = httponly + if domain: + cookie[name]["domain"] = domain cookie[name]["path"] = path - cookie[name]["max-age"] = 0 if state.delete else "" + cookie[name]["max-age"] = 0 if state.delete else max_age msg = "Saved state in cookie {name} with properties {props}".format( name=name, props=list(cookie[name].items()) @@ -61,7 +70,7 @@ def state_to_cookie(state, name, path, encryption_key): return cookie -def cookie_to_state(cookie_str, name, encryption_key): +def cookie_to_state(cookie_str:str, name:str, encryption_key:str): """ Loads a state from a cookie @@ -79,8 +88,7 @@ def cookie_to_state(cookie_str, name, encryption_key): cookie = SimpleCookie(cookie_str) state = State(cookie[name].value, encryption_key) except KeyError as e: - msg_tmpl = 'No cookie named {name} in {data}' - msg = msg_tmpl.format(name=name, data=cookie_str) + msg = f'No cookie named {name} in {cookie_str}' raise SATOSAStateError(msg) from e except ValueError as e: msg_tmpl = 'Failed to process {name} from {data}' @@ -183,7 +191,9 @@ def __init__(self, urlstate_data=None, encryption_key=None): urlstate_data = {} if urlstate_data is None else urlstate_data if urlstate_data and not encryption_key: - raise ValueError("If an 'urlstate_data' is supplied 'encrypt_key' must be specified.") + raise ValueError( + "If an 'urlstate_data' is supplied 'encrypt_key' must be specified." + ) if urlstate_data: urlstate_data = urlstate_data.encode("utf-8")