Skip to content

Commit

Permalink
feat: make cookie parameters configurable
Browse files Browse the repository at this point in the history
Signed-off-by: Ivan Kanakarakis <ivan.kanak@gmail.com>
  • Loading branch information
c00kiemon5ter committed Jun 11, 2023
1 parent 4041df2 commit 1206ea5
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 29 deletions.
4 changes: 4 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ bind_password: !ENVFILE LDAP_BIND_PASSWORD_FILE
| -------------- | --------- | ------------- | ----------- |
| `BASE` | string | `https://proxy.example.com` | base url of the proxy |
| `COOKIE_STATE_NAME` | string | `satosa_state` | name of the cookie SATOSA uses for preserving state between requests |
| `COOKIE_SECURE` | bool | `True` | whether to include the cookie only when the request is transmitted over a secure channel |
| `COOKIE_HTTPONLY` | bool | `True` | whether the cookie should only be accessed only by the server |
| `COOKIE_SAMESITE` | string | `"None"` | whether the cookie should only be sent with requests initiated from the same registrable domain |
| `COOKIE_MAX_AGE` | string | `"1200"` | indicates the maximum lifetime of the cookie represented as the number of seconds until the cookie expires |
| `CONTEXT_STATE_DELETE` | bool | `True` | controls whether SATOSA will delete the state cookie after receiving the authentication response from the upstream IdP|
| `STATE_ENCRYPTION_KEY` | string | `52fddd3528a44157` | key used for encrypting the state cookie, will be overridden by the environment variable `SATOSA_STATE_ENCRYPTION_KEY` if it is set |
| `INTERNAL_ATTRIBUTES` | string | `example/internal_attributes.yaml` | path to attribute mapping
Expand Down
6 changes: 5 additions & 1 deletion src/satosa/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def _load_state(self, context):
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.debug(logline)

Expand All @@ -225,6 +225,10 @@ def _save_state(self, resp, context):
name=cookie_name,
path="/",
encryption_key=self.config["STATE_ENCRYPTION_KEY"],
secure=self.config.get("COOKIE_SECURE"),
httponly=self.config.get("COOKIE_HTTPONLY"),
samesite=self.config.get("COOKIE_SAMESITE"),
max_age=self.config.get("COOKIE_MAX_AGE"),
)
resp.headers = [
(name, value)
Expand Down
8 changes: 4 additions & 4 deletions src/satosa/satosa_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -56,7 +56,7 @@ 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:
Expand Down Expand Up @@ -86,8 +86,8 @@ 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):
"""
Expand Down
58 changes: 36 additions & 22 deletions src/satosa/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,31 +128,46 @@ def state_dict(self):
return copy.deepcopy(self.data)


def state_to_cookie(state, name, path, encryption_key):
def state_to_cookie(
state: State,
*,
name: str,
path: str,
encryption_key: str,
secure: bool = None,
httponly: bool = None,
samesite: str = None,
max_age: str = None,
) -> SimpleCookie:
"""
Saves a state to a cookie
:type state: satosa.state.State
:type name: str
:type path: str
:type encryption_key: str
:rtype: satosa.cookies.SimpleCookie
:param state: The state to save
:param name: Name identifier of the cookie
:param path: Endpoint path the cookie will be associated to
:param encryption_key: Key to encrypt the state information
:return: A cookie
:param state: the data to save
:param name: identifier of the cookie
:param path: path the cookie will be associated to
:param encryption_key: the key to use to encrypt the state information
:param secure: whether to include the cookie only when the request is transmitted
over a secure channel
:param httponly: whether the cookie should only be accessed only by the server
:param samesite: whether the cookie should only be sent with requests
initiated from the same registrable domain
:param max_age: indicates the maximum lifetime of the cookie,
represented as the number of seconds until the cookie expires
:return: A cookie object
"""

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] = "" if state.delete else state.urlstate(encryption_key)
cookie[name]["path"] = path
cookie[name]["max-age"] = 0 if state.delete else ""
cookie[name]["secure"] = secure if secure is not None else True
cookie[name]["httponly"] = httponly if httponly is not None else ""
cookie[name]["samesite"] = samesite if samesite is not None else "None"
cookie[name]["max-age"] = (
0
if state.delete
else max_age
if max_age is not None
else ""
)

msg = "Saved state in cookie {name} with properties {props}".format(
name=name, props=list(cookie[name].items())
Expand All @@ -163,7 +178,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) -> State:
"""
Loads a state from a cookie
Expand All @@ -181,8 +196,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}'
Expand Down
4 changes: 2 additions & 2 deletions tests/satosa/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def test_encode_decode_of_state(self):
path = "/"
encrypt_key = "2781y4hef90"

cookie = state_to_cookie(state, cookie_name, path, encrypt_key)
cookie = state_to_cookie(state, name=cookie_name, path=path, encryption_key=encrypt_key)
cookie_str = cookie[cookie_name].OutputString()
loaded_state = cookie_to_state(cookie_str, cookie_name, encrypt_key)

Expand All @@ -117,7 +117,7 @@ def test_state_to_cookie_produces_cookie_without_max_age_for_state_that_should_b
path = "/"
encrypt_key = "2781y4hef90"

cookie = state_to_cookie(state, cookie_name, path, encrypt_key)
cookie = state_to_cookie(state, name=cookie_name, path=path, encryption_key=encrypt_key)
cookie_str = cookie[cookie_name].OutputString()

parsed_cookie = SimpleCookie(cookie_str)
Expand Down

0 comments on commit 1206ea5

Please sign in to comment.