fix cross site tokens
This commit is contained in:
parent
35693a61e3
commit
ca6d8b9635
3 changed files with 46 additions and 28 deletions
|
|
@ -4,6 +4,8 @@
|
|||
APP_ENV_TYPE = int
|
||||
APP_ENV_LABEL = Integration Instance
|
||||
APP_API_URL = https://gateway-int.poweron.swiss
|
||||
# Force SameSite=None+Secure for auth cookies (cross-site UI on poweron-center.net). Optional if APP_API_URL is https://
|
||||
APP_COOKIE_SECURE = true
|
||||
APP_KEY_SYSVAR = CONFIG_KEY
|
||||
APP_INIT_PASS_ADMIN_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjWm41MWZ4TUZGaVlrX3pWZWNwakJsY3Facm0wLVZDd1VKeTFoZEVZQnItcEdUUnVJS1NXeDBpM2xKbGRsYmxOSmRhc29PZjJSU2txQjdLbUVrTTE1NEJjUXBHbV9NOVJWZUR3QlJkQnJvTEU9
|
||||
APP_INIT_PASS_EVENT_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjdmtrakgxa0djekZVNGtTZV8wM2I5UUpCZllveVBMWXROYk5yS3BiV3JEelJSM09VYTRONHpnY3VtMGxDRk5JTEZSRFhtcDZ0RVRmZ1RicTFhb3c5dVZRQ1o4SmlkLVpPTW5MMTU2eTQ0Vkk9
|
||||
|
|
@ -19,7 +21,7 @@ APP_JWT_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNUctb2RwU25iR3ZnanBOdHZhWUtIajZ1RnZ
|
|||
APP_TOKEN_EXPIRY=300
|
||||
|
||||
# CORS Configuration
|
||||
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://playground.poweron.swiss,https://playground-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss
|
||||
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://playground.poweron.swiss,https://playground-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net
|
||||
|
||||
# Logging configuration
|
||||
APP_LOGGING_LOG_LEVEL = DEBUG
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ APP_KEY_SYSVAR = CONFIG_KEY
|
|||
APP_INIT_PASS_ADMIN_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3UnJRV0sySFlDblpXUlREclREaW1WbUt6bGtQYkdrNkZDOXNOLXFua1hqeFF2RHJnRXJ5VlVGV3hOZm41QjZOMlNTb0duYXNxZi05dXVTc2xDVkx0SVBFLUhncVo5T0VUZHE0UTZLWWw3ck09
|
||||
APP_INIT_PASS_EVENT_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3QVpIY19DQVZSSzJmc2F0VEZvQlU1cHBhTEgxdHdnR3g4eW01aTEzYTUxc1gxTDR1RVVpSHRXYjV6N1BLZUdCUGlfOW1qdy0xSHFVRkNBcGZvaGlSSkZycXRuUllaWnpyVGRoeFg1dGEyNUk9
|
||||
APP_API_URL = https://gateway-prod.poweron.swiss
|
||||
APP_COOKIE_SECURE = true
|
||||
|
||||
# PostgreSQL DB Host
|
||||
DB_HOST=gateway-prod-server.postgres.database.azure.com
|
||||
|
|
@ -19,7 +20,7 @@ APP_JWT_KEY_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3elhfV0Rnd2pQRjlMdkVwX1FnSmRhSzNZUl
|
|||
APP_TOKEN_EXPIRY=300
|
||||
|
||||
# CORS Configuration
|
||||
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://playground.poweron.swiss,https://playground-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss
|
||||
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://playground.poweron.swiss,https://playground-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net
|
||||
|
||||
# Logging configuration
|
||||
APP_LOGGING_LOG_LEVEL = DEBUG
|
||||
|
|
|
|||
|
|
@ -19,15 +19,28 @@ ALGORITHM = APP_CONFIG.get("Auth_ALGORITHM")
|
|||
ACCESS_TOKEN_EXPIRE_MINUTES = int(APP_CONFIG.get("APP_TOKEN_EXPIRY"))
|
||||
REFRESH_TOKEN_EXPIRE_DAYS = int(APP_CONFIG.get("APP_REFRESH_TOKEN_EXPIRY", "7"))
|
||||
|
||||
# Cookie security settings - use secure cookies based on whether API uses HTTPS
|
||||
# Cookies must have secure=True on HTTPS sites, secure=False on HTTP sites
|
||||
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
|
||||
def _cookiePolicy() -> Tuple[bool, str, str]:
|
||||
"""
|
||||
Return (useSecure, samesiteStarlette, samesiteSetCookieHeader).
|
||||
|
||||
# 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"
|
||||
Evaluated on each Set-Cookie so policy is not frozen at module import (config refresh / load order).
|
||||
|
||||
Cross-origin SPA + API: SameSite=None and Secure=True so credentialed fetch sends cookies.
|
||||
HTTP dev: Lax + Secure=False.
|
||||
|
||||
APP_COOKIE_SECURE: explicit true/false (1/0, yes/no) overrides the APP_API_URL heuristic.
|
||||
"""
|
||||
explicit = (APP_CONFIG.get("APP_COOKIE_SECURE") or "").strip().lower()
|
||||
if explicit in ("1", "true", "yes"):
|
||||
useSecure = True
|
||||
elif explicit in ("0", "false", "no"):
|
||||
useSecure = False
|
||||
else:
|
||||
apiUrl = (APP_CONFIG.get("APP_API_URL") or "").strip()
|
||||
useSecure = apiUrl.startswith("https://")
|
||||
samesite = "none" if useSecure else "lax"
|
||||
samesiteHeader = "None" if useSecure else "Lax"
|
||||
return useSecure, samesite, samesiteHeader
|
||||
|
||||
|
||||
def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> Tuple[str, "datetime"]:
|
||||
|
|
@ -59,13 +72,14 @@ def createRefreshToken(data: dict) -> Tuple[str, "datetime"]:
|
|||
|
||||
def setAccessTokenCookie(response: Response, token: str, expiresDelta: Optional[timedelta] = None) -> None:
|
||||
"""Set access token as httpOnly cookie."""
|
||||
useSecure, samesite, _ = _cookiePolicy()
|
||||
maxAge = int(expiresDelta.total_seconds()) if expiresDelta else ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
response.set_cookie(
|
||||
key="auth_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=USE_SECURE_COOKIES, # Only secure in production (HTTPS)
|
||||
samesite=COOKIE_SAMESITE,
|
||||
secure=useSecure,
|
||||
samesite=samesite,
|
||||
path="/",
|
||||
max_age=maxAge
|
||||
)
|
||||
|
|
@ -73,12 +87,13 @@ def setAccessTokenCookie(response: Response, token: str, expiresDelta: Optional[
|
|||
|
||||
def setRefreshTokenCookie(response: Response, token: str) -> None:
|
||||
"""Set refresh token as httpOnly cookie."""
|
||||
useSecure, samesite, _ = _cookiePolicy()
|
||||
response.set_cookie(
|
||||
key="refresh_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=USE_SECURE_COOKIES, # Only secure in production (HTTPS)
|
||||
samesite=COOKIE_SAMESITE,
|
||||
secure=useSecure,
|
||||
samesite=samesite,
|
||||
path="/",
|
||||
max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
|
||||
)
|
||||
|
|
@ -89,22 +104,22 @@ def clearAccessTokenCookie(response: Response) -> None:
|
|||
Clear access token cookie by setting it to expire immediately.
|
||||
Uses both raw header manipulation and FastAPI's delete_cookie for maximum browser compatibility.
|
||||
"""
|
||||
# Build secure flag based on environment
|
||||
secure_flag = "; Secure" if USE_SECURE_COOKIES else ""
|
||||
|
||||
useSecure, samesite, samesiteHeader = _cookiePolicy()
|
||||
secure_flag = "; Secure" if useSecure else ""
|
||||
|
||||
# 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={COOKIE_SAMESITE_SET_COOKIE}"
|
||||
f"auth_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite={samesiteHeader}"
|
||||
)
|
||||
|
||||
|
||||
# Fallback: Also use FastAPI's built-in method (match SameSite/Secure for invalidation)
|
||||
response.delete_cookie(
|
||||
key="auth_token",
|
||||
path="/",
|
||||
secure=USE_SECURE_COOKIES,
|
||||
secure=useSecure,
|
||||
httponly=True,
|
||||
samesite=COOKIE_SAMESITE,
|
||||
samesite=samesite,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -113,22 +128,22 @@ def clearRefreshTokenCookie(response: Response) -> None:
|
|||
Clear refresh token cookie by setting it to expire immediately.
|
||||
Uses both raw header manipulation and FastAPI's delete_cookie for maximum browser compatibility.
|
||||
"""
|
||||
# Build secure flag based on environment
|
||||
secure_flag = "; Secure" if USE_SECURE_COOKIES else ""
|
||||
|
||||
useSecure, samesite, samesiteHeader = _cookiePolicy()
|
||||
secure_flag = "; Secure" if useSecure else ""
|
||||
|
||||
# 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={COOKIE_SAMESITE_SET_COOKIE}"
|
||||
f"refresh_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly{secure_flag}; SameSite={samesiteHeader}"
|
||||
)
|
||||
|
||||
|
||||
# Fallback: Also use FastAPI's built-in method (match SameSite/Secure for invalidation)
|
||||
response.delete_cookie(
|
||||
key="refresh_token",
|
||||
path="/",
|
||||
secure=USE_SECURE_COOKIES,
|
||||
secure=useSecure,
|
||||
httponly=True,
|
||||
samesite=COOKIE_SAMESITE,
|
||||
samesite=samesite,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue