Fix cookie not accepted in safari

This commit is contained in:
2026-03-17 16:57:51 -03:00
parent 72088dba9a
commit d6d0735ff8
6 changed files with 218 additions and 46 deletions

View File

@@ -107,12 +107,16 @@ def get_request_auth_context(
token = credentials.credentials.strip() if credentials is not None and credentials.credentials else ""
using_cookie_session = False
session_candidates: list[str] = []
if not token:
token = (session_cookie or "").strip()
using_cookie_session = True
if not token:
_raise_unauthorized()
session_candidates = [candidate for candidate in _extract_cookie_values(request, SESSION_COOKIE_NAME) if candidate]
normalized_session_cookie = (session_cookie or "").strip()
if normalized_session_cookie and normalized_session_cookie not in session_candidates:
session_candidates.append(normalized_session_cookie)
if not session_candidates:
_raise_unauthorized()
if _requires_csrf_validation(request.method) and using_cookie_session:
normalized_csrf_header = (csrf_header or "").strip()
@@ -127,7 +131,15 @@ def get_request_auth_context(
):
_raise_csrf_rejected()
resolved_session = resolve_auth_session(session, token=token)
resolved_session = None
if token:
resolved_session = resolve_auth_session(session, token=token)
else:
for candidate in session_candidates:
resolved_session = resolve_auth_session(session, token=candidate)
if resolved_session is not None and resolved_session.user is not None:
break
if resolved_session is None or resolved_session.user is None:
_raise_unauthorized()

View File

@@ -90,12 +90,49 @@ def _resolve_cookie_domain() -> str | None:
return configured_domain
def _resolve_cookie_samesite(secure_cookie: bool) -> str:
"""Returns cookie SameSite mode with secure-aware defaults for browser compatibility."""
def _resolve_cookie_domains() -> tuple[str | None, ...]:
"""Returns cookie domain variants with a host-only cookie first for browser compatibility."""
configured_domain = _resolve_cookie_domain()
if configured_domain is None:
return (None,)
return (None, configured_domain)
def _request_matches_cookie_domain(request: Request) -> bool:
"""Returns whether request and origin hosts both sit under the configured cookie domain."""
configured_domain = _resolve_cookie_domain()
if configured_domain is None:
return False
origin_header = request.headers.get("origin", "").strip()
origin_host = urlparse(origin_header).hostname.strip().lower() if origin_header else ""
if not origin_host:
return False
request_url = getattr(request, "url", None)
request_host = str(getattr(request_url, "hostname", "")).strip().lower() if request_url is not None else ""
if not request_host:
parsed_public_base_url = urlparse(get_settings().public_base_url.strip())
request_host = parsed_public_base_url.hostname.strip().lower() if parsed_public_base_url.hostname else ""
if not request_host:
return False
def _matches(candidate: str) -> bool:
return candidate == configured_domain or candidate.endswith(f".{configured_domain}")
return _matches(origin_host) and _matches(request_host)
def _resolve_cookie_samesite(request: Request, secure_cookie: bool) -> str:
"""Returns cookie SameSite mode with same-site subdomain compatibility defaults."""
configured_mode = get_settings().auth_cookie_samesite.strip().lower()
if configured_mode in {"strict", "lax", "none"}:
if configured_mode in {"strict", "lax"}:
return configured_mode
if configured_mode == "none":
return "lax" if _request_matches_cookie_domain(request) else "none"
return "none" if secure_cookie else "lax"
@@ -107,30 +144,39 @@ def _session_cookie_ttl_seconds(expires_at: datetime) -> int:
return max(1, ttl)
def _set_session_cookie(response: Response, session_token: str, *, expires_at: datetime, secure: bool) -> None:
def _set_session_cookie(
response: Response,
session_token: str,
*,
request: Request,
expires_at: datetime,
secure: bool,
) -> None:
"""Stores the issued session token in a browser HttpOnly auth cookie."""
if response is None or not hasattr(response, "set_cookie"):
return
expires_seconds = _session_cookie_ttl_seconds(expires_at)
cookie_domain = _resolve_cookie_domain()
same_site_mode = _resolve_cookie_samesite(secure)
response.set_cookie(
SESSION_COOKIE_NAME,
value=session_token,
max_age=expires_seconds,
httponly=True,
secure=secure,
samesite=same_site_mode,
path="/",
domain=cookie_domain,
)
same_site_mode = _resolve_cookie_samesite(request, secure)
for cookie_domain in _resolve_cookie_domains():
cookie_kwargs = {
"value": session_token,
"max_age": expires_seconds,
"httponly": True,
"secure": secure,
"samesite": same_site_mode,
"path": "/",
}
if cookie_domain is not None:
cookie_kwargs["domain"] = cookie_domain
response.set_cookie(SESSION_COOKIE_NAME, **cookie_kwargs)
def _set_csrf_cookie(
response: Response,
csrf_token: str,
*,
request: Request,
expires_at: datetime,
secure: bool,
) -> None:
@@ -138,18 +184,19 @@ def _set_csrf_cookie(
if response is None or not hasattr(response, "set_cookie"):
return
cookie_domain = _resolve_cookie_domain()
same_site_mode = _resolve_cookie_samesite(secure)
response.set_cookie(
CSRF_COOKIE_NAME,
value=csrf_token,
max_age=_session_cookie_ttl_seconds(expires_at),
httponly=False,
secure=secure,
samesite=same_site_mode,
path="/",
domain=cookie_domain,
)
same_site_mode = _resolve_cookie_samesite(request, secure)
for cookie_domain in _resolve_cookie_domains():
cookie_kwargs = {
"value": csrf_token,
"max_age": _session_cookie_ttl_seconds(expires_at),
"httponly": False,
"secure": secure,
"samesite": same_site_mode,
"path": "/",
}
if cookie_domain is not None:
cookie_kwargs["domain"] = cookie_domain
response.set_cookie(CSRF_COOKIE_NAME, **cookie_kwargs)
def _clear_session_cookies(response: Response) -> None:
@@ -157,9 +204,12 @@ def _clear_session_cookies(response: Response) -> None:
if response is None or not hasattr(response, "delete_cookie"):
return
cookie_domain = _resolve_cookie_domain()
response.delete_cookie(SESSION_COOKIE_NAME, path="/", domain=cookie_domain)
response.delete_cookie(CSRF_COOKIE_NAME, path="/", domain=cookie_domain)
for cookie_domain in _resolve_cookie_domains():
delete_kwargs = {"path": "/"}
if cookie_domain is not None:
delete_kwargs["domain"] = cookie_domain
response.delete_cookie(SESSION_COOKIE_NAME, **delete_kwargs)
response.delete_cookie(CSRF_COOKIE_NAME, **delete_kwargs)
@router.post("/login", response_model=AuthLoginResponse)
@@ -241,12 +291,14 @@ def login(
_set_session_cookie(
response,
issued_session.token,
request=request,
expires_at=issued_session.expires_at,
secure=secure_cookie,
)
_set_csrf_cookie(
response,
csrf_token,
request=request,
expires_at=issued_session.expires_at,
secure=secure_cookie,
)