Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions saml_reader/saml/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ def from_base64(cls, base64, url_decode=False):
(BaseSamlParser) parsed SAML response object
"""
value = base64 if not url_decode else unquote(base64)
value = re.sub(r"\s+", "", value) # Remove whitespace
# Check to see if this is valid base64
rx = r"[^a-zA-Z0-9/+=]"
if re.search(rx, value):
Expand Down
87 changes: 68 additions & 19 deletions saml_reader/web/callbacks/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@
from saml_reader.web.callbacks.crypto import (
encrypt_string,
decrypt_string,
generate_session_id,
CRYPTO_STATE,
)

USER_AGENT = f"saml-reader/{__version__}"

_SESSION_CACHE: dict[str, str] = {}


def submit_analysis_to_backend(data_type, saml_data, comparison_data):
"""Sends data to the SAML reader backend after compilation from the
Expand Down Expand Up @@ -363,7 +366,12 @@ def validate_url_and_authenticate_sdk(
url_value,
)
if not rx or not rx.group("id"):
return "Invalid URL", {"color": "red"}, False
return (
html.P("Invalid URL", style={"color": "red"}),
False,
{"color": "red"},
False,
)

federation_id = rx.group("id")

Expand Down Expand Up @@ -468,7 +476,8 @@ def check_sdk_authentication(n_intervals: int | None) -> tuple[Any, bool, Any, b
if token is None:
raise PreventUpdate

write_token_to_cookie(token)
session_id = write_session_id_cookie()
write_token_to_cache(session_id, token)
federation_id = get_cookie("saml-reader-federation-id", decrypt=False)
return (
html.P(f"Looking up federation {federation_id}", style={"color": "black"}),
Expand Down Expand Up @@ -500,6 +509,14 @@ def do_idp_lookup(children: Any) -> tuple[Any, bool]:
raise PreventUpdate

client = get_atlas_client()
if not client:
return (
html.P(
"There was an error connecting to Atlas.",
style={"color": "red"},
),
False,
)

federation_id = get_cookie("saml-reader-federation-id", decrypt=False)

Expand All @@ -519,7 +536,7 @@ def do_idp_lookup(children: Any) -> tuple[Any, bool]:
idps = [
{
"label": html.Span(
[f'{x["displayName"]} ({x["status"]}, ID: {x["id"]})'],
[f"{x['displayName']} ({x['status']}, ID: {x['id']})"],
style={"font-size": 14},
),
"value": json.dumps(x),
Expand Down Expand Up @@ -618,7 +635,10 @@ def get_atlas_client() -> PublicV2ApiClient | None:
PublicV2ApiClient | None: If token is valid, returns the client,
otherwise returns None to indicate the user needs to reauthenticate.
"""
token = read_token_from_cookie()
session_id = read_session_id_cookie()
if not session_id:
return None
token = read_token_from_cache(session_id)
if not token:
return None

Expand All @@ -632,7 +652,7 @@ def get_atlas_client() -> PublicV2ApiClient | None:
if not client.test_auth():
return None

write_token_to_cookie(client.profile.token)
write_token_to_cache(session_id, client.profile.token)
return client


Expand Down Expand Up @@ -668,41 +688,70 @@ def set_cookie(name: str, value: str, /, secure: bool = False, **kwargs):
name (str): name of the cookie
value (str): value of the cookie
secure (bool, optional): whether the cookie will be marked as secure.
If True, the value will be encrypted. Defaults to True.
If True, the value will be encrypted. Defaults to False.
**kwargs: other configuration values for the cookie
"""
if secure:
value = encrypt_string(value)
ctx.response.set_cookie(name, value, secure=secure, **kwargs)


def write_token_to_cookie(token: Token):
"""Write the OAuth token to a cookie in the response.
def write_session_id_cookie(session_id: str | None = None) -> str:
"""Generate a session ID cookie and optionally cache the session ID.

Args:
token (Token): the token object
session_id (str | None, optional): the session ID to be set in the cookie.
If None, a new session ID will be generated. Defaults to None.

Returns:
str: the session ID that was set in the cookie.
"""
token_dict = asdict(token)
token_dict["issue_time"] = token_dict["issue_time"].timestamp()

if session_id is None:
session_id = generate_session_id()
set_cookie(
"saml-reader-atlas-token",
json.dumps(token_dict),
"saml-reader-atlas-session",
session_id,
secure=True,
httponly=True,
)
return session_id


def read_token_from_cookie() -> Token | None:
"""Read the OAuth token from a cookie in the request.
def read_session_id_cookie() -> str | None:
"""Read the session ID from a cookie in the request.

Returns:
Token: the token object, if present and decryptable,
otherwise None
str | None: the session ID, if present, otherwise None
"""
return get_cookie("saml-reader-atlas-session")


def write_token_to_cache(session_id: str, token: Token):
"""Write the OAuth token to the session cache for session ID.

Args:
session_id (str): the session ID for which the token is being cached.
token (Token): the token object
"""
token_dict = asdict(token)
token_dict["issue_time"] = token_dict["issue_time"].timestamp()
_SESSION_CACHE[session_id] = encrypt_string(json.dumps(token_dict))


def read_token_from_cache(session_id: str) -> Token | None:
"""Read the OAuth token from the session cache for the given session ID.

Args:
session_id (str): the session ID for which the token is being retrieved.

Returns:
Token: the token object, if present, otherwise None
"""
token_json = get_cookie("saml-reader-atlas-token")
token_json = _SESSION_CACHE.get(session_id, None)
if not token_json:
return None
token_dict = json.loads(token_json)
token_dict = json.loads(decrypt_string(token_json))
token_dict["issue_time"] = datetime.fromtimestamp(token_dict["issue_time"])
return Token(**token_dict)

Expand Down
11 changes: 11 additions & 0 deletions saml_reader/web/callbacks/crypto.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import secrets

from cryptography.fernet import Fernet

# Encryption object unique to this app session
Expand Down Expand Up @@ -32,3 +34,12 @@ def decrypt_string(data: str) -> str:
if not data:
return ""
return str(_crypto.decrypt(bytes(data, "UTF-8")), "UTF-8")


def generate_session_id() -> str:
"""Generate a unique session ID.

Returns:
str: unique session ID
"""
return secrets.token_urlsafe(32)
13 changes: 9 additions & 4 deletions saml_reader/web/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ def run_web_app(host="localhost", port=8050, server_timeout=None, **options):
shutting it down as a security measure.
**options (optional): Keyword arguments sent to underlying Dash/Flask launcher.
"""
server_func = partial(app.run_server, host=host, port=port, **options)
if hasattr(app, "run"):
# Modern dash
server_func = partial(app.run, host=host, port=port, **options)
else:
# Legacy dash
server_func = partial(app.run_server, host=host, port=port, **options)
if server_timeout is not None:
# Avoiding spawning extra thread in case bad clash with gunicorn
thread = Thread(target=server_func)
Expand All @@ -52,14 +57,14 @@ def run_web_app(host="localhost", port=8050, server_timeout=None, **options):

if __name__ == "__main__":
# Defaults
host = "0.0.0.0"
host = "localhost"
use_flask_debug_mode = True

# Checking command line arguments
# TODO: Maybe replace this with argparse
if len(sys.argv) > 1:
if "--local" in sys.argv:
host = "localhost"
if "--deploy" in sys.argv:
host = "0.0.0.0"
if "--using-debugger" in sys.argv:
use_flask_debug_mode = False

Expand Down