From 35693a61e3f3e926f8615e90272ed9ba1df6a031 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Fri, 8 May 2026 13:58:12 +0200
Subject: [PATCH] Cross-Site cookies enabled
---
modules/auth/jwtService.py | 33 +++++++++++++++++++++++++--------
1 file changed, 25 insertions(+), 8 deletions(-)
diff --git a/modules/auth/jwtService.py b/modules/auth/jwtService.py
index 439d3282..422a4951 100644
--- a/modules/auth/jwtService.py
+++ b/modules/auth/jwtService.py
@@ -24,6 +24,11 @@ REFRESH_TOKEN_EXPIRE_DAYS = int(APP_CONFIG.get("APP_REFRESH_TOKEN_EXPIRY", "7"))
APP_API_URL = APP_CONFIG.get("APP_API_URL", "http://localhost:8000")
USE_SECURE_COOKIES = APP_API_URL.startswith("https://") if APP_API_URL else False
+# Cross-origin SPA (any host) + API (gateway host): SameSite=None + Secure is required so the
+# browser sends cookies on credentialed XHR/fetch. HTTP localhost uses Lax (None without Secure is rejected).
+COOKIE_SAMESITE = "none" if USE_SECURE_COOKIES else "lax"
+COOKIE_SAMESITE_SET_COOKIE = "None" if USE_SECURE_COOKIES else "Lax"
+
def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> Tuple[str, "datetime"]:
"""Create a JWT access token and return (token, expiresAt)."""
@@ -60,7 +65,7 @@ def setAccessTokenCookie(response: Response, token: str, expiresDelta: Optional[
value=token,
httponly=True,
secure=USE_SECURE_COOKIES, # Only secure in production (HTTPS)
- samesite="strict",
+ samesite=COOKIE_SAMESITE,
path="/",
max_age=maxAge
)
@@ -73,7 +78,7 @@ def setRefreshTokenCookie(response: Response, token: str) -> None:
value=token,
httponly=True,
secure=USE_SECURE_COOKIES, # Only secure in production (HTTPS)
- samesite="strict",
+ samesite=COOKIE_SAMESITE,
path="/",
max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
)
@@ -90,11 +95,17 @@ def clearAccessTokenCookie(response: Response) -> None:
# Primary method: Raw Set-Cookie header for guaranteed deletion
response.headers.append(
"Set-Cookie",
- f"auth_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite=Strict"
+ f"auth_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite={COOKIE_SAMESITE_SET_COOKIE}"
)
- # Fallback: Also use FastAPI's built-in method
- response.delete_cookie(key="auth_token", path="/")
+ # Fallback: Also use FastAPI's built-in method (match SameSite/Secure for invalidation)
+ response.delete_cookie(
+ key="auth_token",
+ path="/",
+ secure=USE_SECURE_COOKIES,
+ httponly=True,
+ samesite=COOKIE_SAMESITE,
+ )
def clearRefreshTokenCookie(response: Response) -> None:
@@ -108,10 +119,16 @@ def clearRefreshTokenCookie(response: Response) -> None:
# Primary method: Raw Set-Cookie header for guaranteed deletion
response.headers.append(
"Set-Cookie",
- f"refresh_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite=Strict"
+ f"refresh_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite={COOKIE_SAMESITE_SET_COOKIE}"
)
- # Fallback: Also use FastAPI's built-in method
- response.delete_cookie(key="refresh_token", path="/")
+ # Fallback: Also use FastAPI's built-in method (match SameSite/Secure for invalidation)
+ response.delete_cookie(
+ key="refresh_token",
+ path="/",
+ secure=USE_SECURE_COOKIES,
+ httponly=True,
+ samesite=COOKIE_SAMESITE,
+ )