Scope session cookies by grid path

This commit is contained in:
Jage9
2026-03-08 22:59:59 -04:00
parent 54a7a3085b
commit 19b593b1aa
5 changed files with 33 additions and 14 deletions

View File

@@ -193,6 +193,7 @@ class SignalingServer:
self.server_version = self._resolve_server_version()
self.host_origin = normalize_origin(host_origin, field_name="host origin") if host_origin else None
self.base_path = self._normalize_base_path(base_path)
self.auth_session_cookie_name = self._session_cookie_name_for_base_path(self.base_path)
self.websocket_path = self._base_path_join(WEBSOCKET_PATH)
self.auth_session_cookie_set_path = self._base_path_join(AUTH_SESSION_COOKIE_SET_PATH)
self.auth_session_cookie_clear_path = self._base_path_join(AUTH_SESSION_COOKIE_CLEAR_PATH)
@@ -287,6 +288,17 @@ class SignalingServer:
return f"/{token}"
return f"{self.base_path}{token}"
@staticmethod
def _session_cookie_name_for_base_path(base_path: str) -> str:
"""Return one deterministic session cookie name for the configured instance path."""
if base_path == "/":
return AUTH_SESSION_COOKIE_NAME
suffix = re.sub(r"[^a-z0-9]+", "_", base_path.strip("/").casefold()).strip("_")
if not suffix:
return AUTH_SESSION_COOKIE_NAME
return f"chgrid_session_{suffix}"
def _session_cookie_secure(self, request: HttpRequest | None = None) -> bool:
"""Return True when session cookies should be marked Secure."""
@@ -302,7 +314,7 @@ class SignalingServer:
secure = "; Secure" if self._session_cookie_secure(request) else ""
return (
f"{AUTH_SESSION_COOKIE_NAME}={token}; Path={self.base_path}; HttpOnly; SameSite=Lax; "
f"{self.auth_session_cookie_name}={token}; Path={self.base_path}; HttpOnly; SameSite=Lax; "
f"Max-Age={AUTH_SESSION_COOKIE_MAX_AGE_SECONDS}{secure}"
)
@@ -310,7 +322,7 @@ class SignalingServer:
"""Build Set-Cookie header value that expires the session cookie."""
secure = "; Secure" if self._session_cookie_secure(request) else ""
return f"{AUTH_SESSION_COOKIE_NAME}=; Path={self.base_path}; HttpOnly; SameSite=Lax; Max-Age=0{secure}"
return f"{self.auth_session_cookie_name}=; Path={self.base_path}; HttpOnly; SameSite=Lax; Max-Age=0{secure}"
def _origin_allowed(self, request: HttpRequest) -> bool:
"""Return whether one auth helper HTTP request comes from the configured app origin."""
@@ -364,7 +376,7 @@ class SignalingServer:
if path == self.auth_session_cookie_check_path:
cookie_header = str(request.headers.get("Cookie", "")).strip()
token = self._cookie_value(cookie_header, AUTH_SESSION_COOKIE_NAME)
token = self._cookie_value(cookie_header, self.auth_session_cookie_name)
if not token:
return HttpResponse(401, "Unauthorized", headers, b"missing session")
try:
@@ -400,7 +412,7 @@ class SignalingServer:
cookie_header = str(headers.get("Cookie", "")).strip()
if not cookie_header:
return ""
return self._cookie_value(cookie_header, AUTH_SESSION_COOKIE_NAME)
return self._cookie_value(cookie_header, self.auth_session_cookie_name)
def _build_admin_menu_actions_for_client(self, client: ClientConnection | None) -> list[dict[str, str]]:
"""Build server-authored admin menu actions allowed for one client."""

View File

@@ -11,7 +11,6 @@ from app.server import (
AUTH_SESSION_COOKIE_CLIENT_HEADER,
AUTH_SESSION_COOKIE_CHECK_PATH,
AUTH_SESSION_COOKIE_CLEAR_PATH,
AUTH_SESSION_COOKIE_NAME,
AUTH_SESSION_COOKIE_SET_PATH,
SignalingServer,
)
@@ -47,7 +46,7 @@ async def test_session_cookie_set_endpoint_sets_httponly_cookie() -> None:
assert response is not None
assert response.status_code == 200
set_cookie = response.headers.get("Set-Cookie", "")
assert f"{AUTH_SESSION_COOKIE_NAME}=" in set_cookie
assert f"{server.auth_session_cookie_name}=" in set_cookie
assert "Path=/chgrid/" in set_cookie
assert "HttpOnly" in set_cookie
assert "SameSite=Lax" in set_cookie
@@ -66,7 +65,7 @@ async def test_session_cookie_clear_endpoint_expires_cookie() -> None:
assert response is not None
assert response.status_code == 200
set_cookie = response.headers.get("Set-Cookie", "")
assert f"{AUTH_SESSION_COOKIE_NAME}=" in set_cookie
assert f"{server.auth_session_cookie_name}=" in set_cookie
assert "Max-Age=0" in set_cookie
assert "HttpOnly" in set_cookie
@@ -80,7 +79,7 @@ async def test_session_cookie_check_endpoint_accepts_valid_cookie() -> None:
server.auth_session_cookie_check_path,
headers={
AUTH_SESSION_COOKIE_CLIENT_HEADER: "1",
"Cookie": f"{AUTH_SESSION_COOKIE_NAME}={session.token}",
"Cookie": f"{server.auth_session_cookie_name}={session.token}",
"Origin": "https://example.com",
},
)
@@ -120,13 +119,21 @@ async def test_session_cookie_helpers_reject_wrong_origin() -> None:
def test_session_token_from_websocket_cookie_reads_named_cookie() -> None:
server = SignalingServer("127.0.0.1", 8765, None, None)
server = SignalingServer("127.0.0.1", 8765, None, None, base_path="/chgrid/")
websocket = SimpleNamespace(
request=SimpleNamespace(
headers=Headers({"Cookie": f"foo=bar; {AUTH_SESSION_COOKIE_NAME}=abc123; hello=world"})
headers=Headers({"Cookie": f"foo=bar; {server.auth_session_cookie_name}=abc123; hello=world"})
)
)
token = server._session_token_from_websocket_cookie(websocket)
assert token == "abc123"
def test_session_cookie_name_scopes_to_base_path() -> None:
root_server = SignalingServer("127.0.0.1", 8765, None, None, base_path="/")
nested_server = SignalingServer("127.0.0.1", 8765, None, None, base_path="/ttgrid/")
assert root_server.auth_session_cookie_name == "chgrid_session_token"
assert nested_server.auth_session_cookie_name == "chgrid_session_ttgrid"