From 3d9fe199292f9d671320c1db7860be63ff48cc5b Mon Sep 17 00:00:00 2001 From: Jefry Date: Tue, 9 Jun 2026 20:58:42 +0200 Subject: [PATCH 1/2] added environment variable that allows the cookie_name to allow Lax in development modes --- backend/src/apiserver/app.py | 7 +++++-- backend/src/apiserver/handlers/auth.py | 18 ++++++++++++------ backend/src/apiserver/server.py | 8 ++++++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/backend/src/apiserver/app.py b/backend/src/apiserver/app.py index 23863b5..4fe9d0f 100644 --- a/backend/src/apiserver/app.py +++ b/backend/src/apiserver/app.py @@ -79,6 +79,7 @@ def handler_with_client( frontend_origin: str, smtp_config: SmtpConfig | None = None, smtp_send: bool = False, + environment: str = "production", ) -> Response: """Dispatch a request to its handler based on the route table.""" if store_queue is None: @@ -103,13 +104,14 @@ def h_set_sess(): smtp_config, smtp_send, frontend_origin, + environment, ) def h_clear_sess(): - return clear_session(req) + return clear_session(req, environment) def h_sess_info(): - return session_info(auth_client, req, headers, store_queue) + return session_info(auth_client, req, headers, store_queue, environment) def h_add_perm(): return add_user_permission(req, store_queue) @@ -432,6 +434,7 @@ def handler(req: Request, store_queue: StorageQueue | None) -> Response: settings.frontend_origin, settings.smtp, settings.smtp_send, + settings.environment, ) if ready_event is None: diff --git a/backend/src/apiserver/handlers/auth.py b/backend/src/apiserver/handlers/auth.py index 3dcb0a1..f2d6fc2 100644 --- a/backend/src/apiserver/handlers/auth.py +++ b/backend/src/apiserver/handlers/auth.py @@ -211,6 +211,7 @@ def set_session( smtp_config=None, smtp_send: bool = False, frontend_origin: str = "", + environment: str = "production", ) -> Response: """Handle /auth/set_session/ — validate and set session cookie.""" try: @@ -237,8 +238,11 @@ def set_session( cookie = SimpleCookie() cookie[cookie_name] = session_token cookie[cookie_name]["httponly"] = True - cookie[cookie_name]["samesite"] = "None" - cookie[cookie_name]["secure"] = True + if environment == "production": + cookie[cookie_name]["samesite"] = "None" + cookie[cookie_name]["secure"] = True + else: + cookie[cookie_name]["samesite"] = "Lax" cookie[cookie_name]["path"] = "/" cookie[cookie_name]["max-age"] = max_session_age @@ -251,7 +255,7 @@ def set_session( ) -def clear_session(req: Request) -> Response: +def clear_session(req: Request, environment: str = "production") -> Response: """Handle /auth/clear_session/ — clear session cookie.""" secondary = False if req.body: @@ -263,7 +267,8 @@ def clear_session(req: Request) -> Response: cookie_name = SESSION_COOKIE_SECONDARY if secondary else SESSION_COOKIE_PRIMARY logger.info(f"clear_session: Clearing {cookie_name} cookie") - return Response.empty(headers=[make_clear_session_cookie_header(cookie_name)]) + header = make_clear_session_cookie_header(cookie_name, environment) + return Response.empty(headers=[header]) def session_info( @@ -271,6 +276,7 @@ def session_info( req: Request, headers: dict[str, str], store_queue: StorageQueue, + environment: str = "production", ) -> Response: """Handle /auth/session_info/ — returns current user info and permissions. @@ -294,7 +300,7 @@ def session_info( return Response.json( {"error": "invalid_session"}, status_code=401, - headers=[make_clear_session_cookie_header(cookie_name)], + headers=[make_clear_session_cookie_header(cookie_name, environment)], ) timestamp = int(time.time()) @@ -318,7 +324,7 @@ def get_session_data( return Response.json( {"error": "invalid_session"}, status_code=401, - headers=[make_clear_session_cookie_header(cookie_name)], + headers=[make_clear_session_cookie_header(cookie_name, environment)], ) session = session_result diff --git a/backend/src/apiserver/server.py b/backend/src/apiserver/server.py index a153932..4b5902b 100644 --- a/backend/src/apiserver/server.py +++ b/backend/src/apiserver/server.py @@ -205,13 +205,17 @@ def get_cookie_value(headers: dict[str, str], cookie_name: str) -> str | None: def make_clear_session_cookie_header( cookie_name: str = SESSION_COOKIE_PRIMARY, + environment: str = "production", ) -> tuple[bytes, bytes]: """Create a Set-Cookie header that clears a session cookie.""" cookie = SimpleCookie() cookie[cookie_name] = "" cookie[cookie_name]["httponly"] = True - cookie[cookie_name]["samesite"] = "None" - cookie[cookie_name]["secure"] = True + if environment == "production": + cookie[cookie_name]["samesite"] = "None" + cookie[cookie_name]["secure"] = True + else: + cookie[cookie_name]["samesite"] = "Lax" cookie[cookie_name]["path"] = "/" cookie[cookie_name]["max-age"] = 0 return (b"Set-Cookie", cookie[cookie_name].OutputString().encode("utf-8")) From d5afae34b33b7c5dfd5e518eb75c06c464717ae6 Mon Sep 17 00:00:00 2001 From: Tip ten Brink <75669206+tiptenbrink@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:02:02 +0200 Subject: [PATCH 2/2] actually use SameSite=Strict and only drop Secure on macOS+test --- backend/src/apiserver/handlers/auth.py | 9 ++------- backend/src/apiserver/server.py | 24 +++++++++++++++++------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/backend/src/apiserver/handlers/auth.py b/backend/src/apiserver/handlers/auth.py index f2d6fc2..9d6e925 100644 --- a/backend/src/apiserver/handlers/auth.py +++ b/backend/src/apiserver/handlers/auth.py @@ -31,6 +31,7 @@ InvalidSessionToken, SESSION_COOKIE_PRIMARY, SESSION_COOKIE_SECONDARY, + apply_session_cookie_security, get_cookie_value, make_clear_session_cookie_header, validate_session, @@ -237,13 +238,7 @@ def set_session( cookie = SimpleCookie() cookie[cookie_name] = session_token - cookie[cookie_name]["httponly"] = True - if environment == "production": - cookie[cookie_name]["samesite"] = "None" - cookie[cookie_name]["secure"] = True - else: - cookie[cookie_name]["samesite"] = "Lax" - cookie[cookie_name]["path"] = "/" + apply_session_cookie_security(cookie, cookie_name, environment) cookie[cookie_name]["max-age"] = max_session_age cookie_header = cookie[cookie_name].OutputString() diff --git a/backend/src/apiserver/server.py b/backend/src/apiserver/server.py index 4b5902b..947462d 100644 --- a/backend/src/apiserver/server.py +++ b/backend/src/apiserver/server.py @@ -6,6 +6,7 @@ import enum import logging +import sys import time from dataclasses import dataclass, field from http.cookies import SimpleCookie @@ -203,6 +204,21 @@ def get_cookie_value(headers: dict[str, str], cookie_name: str) -> str | None: return None +def apply_session_cookie_security( + cookie: SimpleCookie, cookie_name: str, environment: str +) -> None: + """Apply HttpOnly / SameSite / Secure / Path to a session cookie. + + Secure is dropped only on macOS in the test env: Safari rejects Secure + cookies on the HTTP loopback (https://bugs.webkit.org/show_bug.cgi?id=281149). + """ + cookie[cookie_name]["httponly"] = True + cookie[cookie_name]["samesite"] = "Strict" + cookie[cookie_name]["path"] = "/" + if not (environment == "test" and sys.platform == "darwin"): + cookie[cookie_name]["secure"] = True + + def make_clear_session_cookie_header( cookie_name: str = SESSION_COOKIE_PRIMARY, environment: str = "production", @@ -210,13 +226,7 @@ def make_clear_session_cookie_header( """Create a Set-Cookie header that clears a session cookie.""" cookie = SimpleCookie() cookie[cookie_name] = "" - cookie[cookie_name]["httponly"] = True - if environment == "production": - cookie[cookie_name]["samesite"] = "None" - cookie[cookie_name]["secure"] = True - else: - cookie[cookie_name]["samesite"] = "Lax" - cookie[cookie_name]["path"] = "/" + apply_session_cookie_security(cookie, cookie_name, environment) cookie[cookie_name]["max-age"] = 0 return (b"Set-Cookie", cookie[cookie_name].OutputString().encode("utf-8"))