From 1991171f39c88234a742e30264c580b2d99b71e3 Mon Sep 17 00:00:00 2001 From: Alastair McFarlane Date: Fri, 30 Jan 2026 17:36:16 +0000 Subject: [PATCH 1/6] Add created at timestamp to cookie sessions --- src/asfquart/session.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/asfquart/session.py b/src/asfquart/session.py index 163ccfe..5ea1f61 100644 --- a/src/asfquart/session.py +++ b/src/asfquart/session.py @@ -106,6 +106,7 @@ def write(session_data: dict, app=None): cookie_id = app.app_id dict_copy = session_data.copy() # Copy dict so we don't mess with the original data + dict_copy["cts"] = time.time() # Set created at timestamp for session length checks later dict_copy["uts"] = time.time() # Set last access timestamp for expiry checks later quart.session[cookie_id] = dict_copy From 48445db1c31bd0c1d59e6214f8f322db520a9d68 Mon Sep 17 00:00:00 2001 From: Alastair McFarlane Date: Mon, 2 Feb 2026 09:57:21 +0000 Subject: [PATCH 2/6] Enforce max session age if set --- src/asfquart/session.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/asfquart/session.py b/src/asfquart/session.py index 5ea1f61..78fa87a 100644 --- a/src/asfquart/session.py +++ b/src/asfquart/session.py @@ -32,7 +32,7 @@ def __init__(self, raw_data: dict): self.update(self.__dict__.items()) -async def read(expiry_time=86400*7, app=None) -> typing.Optional[ClientSession]: +async def read(expiry_time=86400*7, max_session_age=0, app=None) -> typing.Optional[ClientSession]: """Fetches a cookie-based session if found (and valid), and updates the last access timestamp for the session.""" @@ -45,12 +45,17 @@ async def read(expiry_time=86400*7, app=None) -> typing.Optional[ClientSession]: if cookie_id in quart.session: now = time.time() cookie_expiry_deadline = now - expiry_time + cookie_session_age_limit = now - max_session_age session_dict = quart.session[cookie_id] if isinstance(session_dict, dict): + session_create_timestamp = session_dict.get("cts", 0) session_update_timestamp = session_dict.get("uts", 0) # If a session cookie has expired (not updated/used for seven days), we delete it instead of returning it if session_update_timestamp < cookie_expiry_deadline: del quart.session[cookie_id] + # If max session lifetime is set and the cookie has exceeded it, we delete it + elif max_session_age > 0 and session_create_timestamp < cookie_session_age_limit: + del quart.session[cookie_id] # If it's still valid, use it else: # Update the timestamp, since the session has been requested (and thus used) From 081fb21b8565a36975a493ac41fadcd970ed8900 Mon Sep 17 00:00:00 2001 From: Alastair McFarlane Date: Mon, 2 Feb 2026 10:14:27 +0000 Subject: [PATCH 3/6] Documentation update for max_session_age --- docs/sessions.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/sessions.md b/docs/sessions.md index 64c4a28..1587ca8 100644 --- a/docs/sessions.md +++ b/docs/sessions.md @@ -14,13 +14,20 @@ async def endpoint_with_session(): asfquart.session.write(session) # Store our changes in the user session ``` -Session timeouts can be handled by passing the `expiry_time` argument to the `read()` call: +Session timeouts can be handled by passing the `expiry_time` argument (default 7 days) to the `read()` call: ```python session = await asfquart.session.read(expiry_time=24*3600) # Require a session that has been accessed in the past 24 hours. assert session, "No session found or session expired" # If too old or not found, read() returns None ``` +Maximum session lifetime can be handled by passing the `max_session_age` argument to the `read()` call: + +```python +session = await asfquart.session.read(max_session_age=24*3600) # Require a session expire after a finite lifetime. +assert session, "No session found or session expired" # If too old or not found, read() returns None +``` + ## Role account management via declared PAT handler Role accounts (or regular users) can access asfquart apps by using a bearer token, so long as a personal app token (PAT) handler is declared: From 540f57d4e681e7ad6d6954e56a7640d965b2e820 Mon Sep 17 00:00:00 2001 From: Alastair McFarlane Date: Mon, 2 Feb 2026 15:22:08 +0000 Subject: [PATCH 4/6] Move max_session_age to config --- src/asfquart/session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/asfquart/session.py b/src/asfquart/session.py index 78fa87a..d2ae8b6 100644 --- a/src/asfquart/session.py +++ b/src/asfquart/session.py @@ -32,7 +32,7 @@ def __init__(self, raw_data: dict): self.update(self.__dict__.items()) -async def read(expiry_time=86400*7, max_session_age=0, app=None) -> typing.Optional[ClientSession]: +async def read(expiry_time=86400*7, app=None) -> typing.Optional[ClientSession]: """Fetches a cookie-based session if found (and valid), and updates the last access timestamp for the session.""" @@ -44,6 +44,7 @@ async def read(expiry_time=86400*7, max_session_age=0, app=None) -> typing.Optio cookie_id = app.app_id if cookie_id in quart.session: now = time.time() + app.config.get("MAX_SESSION_AGE", 0) cookie_expiry_deadline = now - expiry_time cookie_session_age_limit = now - max_session_age session_dict = quart.session[cookie_id] From 65347fb11141472625639f113034faecfe8224a3 Mon Sep 17 00:00:00 2001 From: Alastair McFarlane Date: Mon, 2 Feb 2026 15:24:00 +0000 Subject: [PATCH 5/6] Revise session lifetime handling in documentation Updated documentation to reflect changes in session handling options. --- docs/sessions.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/sessions.md b/docs/sessions.md index 1587ca8..bcaf7e3 100644 --- a/docs/sessions.md +++ b/docs/sessions.md @@ -21,12 +21,7 @@ session = await asfquart.session.read(expiry_time=24*3600) # Require a session assert session, "No session found or session expired" # If too old or not found, read() returns None ``` -Maximum session lifetime can be handled by passing the `max_session_age` argument to the `read()` call: - -```python -session = await asfquart.session.read(max_session_age=24*3600) # Require a session expire after a finite lifetime. -assert session, "No session found or session expired" # If too old or not found, read() returns None -``` +Maximum session lifetime can be handled by passing the `MAX_SESSION_AGE` option in config.yaml. ## Role account management via declared PAT handler Role accounts (or regular users) can access asfquart apps by using a bearer token, so long as a personal app token (PAT) handler From 4a3d02ecf7322e567c559d6ae86b2b14732dc7ea Mon Sep 17 00:00:00 2001 From: Alastair McFarlane Date: Mon, 2 Feb 2026 15:37:54 +0000 Subject: [PATCH 6/6] Fix session age retrieval in session.py --- src/asfquart/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/asfquart/session.py b/src/asfquart/session.py index d2ae8b6..2dc86ef 100644 --- a/src/asfquart/session.py +++ b/src/asfquart/session.py @@ -44,7 +44,7 @@ async def read(expiry_time=86400*7, app=None) -> typing.Optional[ClientSession]: cookie_id = app.app_id if cookie_id in quart.session: now = time.time() - app.config.get("MAX_SESSION_AGE", 0) + max_session_age = app.config.get("MAX_SESSION_AGE", 0) cookie_expiry_deadline = now - expiry_time cookie_session_age_limit = now - max_session_age session_dict = quart.session[cookie_id]