fix: set cookie domain for cross-subdomain authentication
In production, the session cookie needs to be shared across subdomains (e.g., api.example.com and app.example.com). - Add cookie_domain property to config (extracts parent domain) - Set SameSite=None for cross-origin requests in production - Update auth callback and logout to use cookie domain - This fixes the login loop where session cookie wasn't sent
This commit is contained in:
@@ -114,9 +114,18 @@ async def callback(
|
|||||||
cookie_options = build_cookie_options(settings)
|
cookie_options = build_cookie_options(settings)
|
||||||
cookie_samesite = cast(Literal["lax", "strict", "none"], cookie_options["samesite"])
|
cookie_samesite = cast(Literal["lax", "strict", "none"], cookie_options["samesite"])
|
||||||
cookie_secure = bool(cookie_options["secure"])
|
cookie_secure = bool(cookie_options["secure"])
|
||||||
response.set_cookie("session", session_cookie, httponly=True, samesite=cookie_samesite, secure=cookie_secure)
|
cookie_domain = str(cookie_options["domain"]) if cookie_options.get("domain") else None
|
||||||
response.delete_cookie("auth_state", samesite="lax")
|
|
||||||
response.delete_cookie("auth_next", samesite="lax")
|
response.set_cookie(
|
||||||
|
"session",
|
||||||
|
session_cookie,
|
||||||
|
httponly=True,
|
||||||
|
samesite=cookie_samesite,
|
||||||
|
secure=cookie_secure,
|
||||||
|
domain=cookie_domain,
|
||||||
|
)
|
||||||
|
response.delete_cookie("auth_state", samesite="lax", domain=cookie_domain)
|
||||||
|
response.delete_cookie("auth_next", samesite="lax", domain=cookie_domain)
|
||||||
|
|
||||||
logger.info("Auth callback complete for user id=%s, redirecting to %s", user.id, auth_next)
|
logger.info("Auth callback complete for user id=%s, redirecting to %s", user.id, auth_next)
|
||||||
|
|
||||||
@@ -131,8 +140,9 @@ async def logout(response: Response) -> dict[str, str]:
|
|||||||
cookie_options = build_cookie_options(settings)
|
cookie_options = build_cookie_options(settings)
|
||||||
cookie_samesite = cast(Literal["lax", "strict", "none"], cookie_options["samesite"])
|
cookie_samesite = cast(Literal["lax", "strict", "none"], cookie_options["samesite"])
|
||||||
cookie_secure = bool(cookie_options["secure"])
|
cookie_secure = bool(cookie_options["secure"])
|
||||||
|
cookie_domain = str(cookie_options["domain"]) if cookie_options.get("domain") else None
|
||||||
|
|
||||||
response.delete_cookie("session", samesite=cookie_samesite, secure=cookie_secure)
|
response.delete_cookie("session", samesite=cookie_samesite, secure=cookie_secure, domain=cookie_domain)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from src.config import Settings
|
from src.config import Settings
|
||||||
|
|
||||||
|
|
||||||
def build_cookie_options(settings: Settings) -> dict[str, str | bool]:
|
def build_cookie_options(settings: Settings) -> dict[str, str | bool | None]:
|
||||||
return {
|
return {
|
||||||
"httponly": True,
|
"httponly": True,
|
||||||
"secure": settings.cookie_secure,
|
"secure": settings.cookie_secure,
|
||||||
"samesite": settings.cookie_samesite,
|
"samesite": settings.cookie_samesite,
|
||||||
|
"domain": settings.cookie_domain,
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -117,6 +117,22 @@ class Settings(BaseSettings):
|
|||||||
@property
|
@property
|
||||||
def cookie_samesite(self) -> str:
|
def cookie_samesite(self) -> str:
|
||||||
if self.app_env == "production":
|
if self.app_env == "production":
|
||||||
return "strict"
|
return "none"
|
||||||
|
|
||||||
return "lax"
|
return "lax"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cookie_domain(self) -> str | None:
|
||||||
|
"""Return the parent domain for cross-subdomain cookies.
|
||||||
|
|
||||||
|
E.g., api.example.com and app.example.com both share .example.com
|
||||||
|
"""
|
||||||
|
if self.app_env != "production":
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extract parent domain from api_domain
|
||||||
|
# e.g., "api.headquarter.commumedia.org" -> ".headquarter.commumedia.org"
|
||||||
|
parts = self.api_domain.split(".")
|
||||||
|
if len(parts) >= 3:
|
||||||
|
return "." + ".".join(parts[1:])
|
||||||
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user