diff --git a/README.md b/README.md index 012fbd5..d6e21e2 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Summary: 1. Copy repo to your server. 2. Build client and publish `client/dist/` to your web root/subdirectory. 3. Configure server `config.toml` and run it via `systemd`. -4. Add Apache `/ws` websocket proxy from `deploy/apache/chgrid-vhost-snippet.conf`. +4. Add base-path-scoped websocket/auth proxy routes from `deploy/apache/chgrid-vhost-snippet.conf`. ## Key Paths diff --git a/client/public/version.js b/client/public/version.js index a6ba1d7..9fd78f2 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,6 +1,6 @@ // Maintainer-controlled web client version metadata. window.CHGRID_RELEASE_VERSION = "0.1.0"; -window.CHGRID_BUILD_REVISION = "R341"; +window.CHGRID_BUILD_REVISION = "R342"; window.CHGRID_WEB_VERSION = `${window.CHGRID_RELEASE_VERSION} ${window.CHGRID_BUILD_REVISION}`; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/main.ts b/client/src/main.ts index 7c9a217..6fcacdd 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -236,8 +236,8 @@ const SYSTEM_SOUND_URLS = { logout: withBase('sounds/logout.ogg'), notify: withBase('sounds/notify.ogg'), } as const; -const AUTH_SESSION_COOKIE_SET_URL = '/auth/session/set'; -const AUTH_SESSION_COOKIE_CLEAR_URL = '/auth/session/clear'; +const AUTH_SESSION_COOKIE_SET_URL = withBase('auth/session/set'); +const AUTH_SESSION_COOKIE_CLEAR_URL = withBase('auth/session/clear'); const AUTH_SESSION_COOKIE_CLIENT_HEADER = 'X-Chgrid-Auth-Client'; const ACTION_SOUND_URL = withBase('sounds/action.ogg'); const FOOTSTEP_SOUND_URLS = Array.from({ length: 11 }, (_, index) => withBase(`sounds/step-${index + 1}.ogg`)); @@ -313,7 +313,7 @@ let activeTeleport: | null = null; const signalingProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; -const signalingUrl = `${signalingProtocol}://${window.location.host}/ws`; +const signalingUrl = `${signalingProtocol}://${window.location.host}${withBase('ws')}`; const signaling = new SignalingClient(signalingUrl, handleSignalingStatus); const peerManager = new PeerManager( diff --git a/deploy/README.md b/deploy/README.md index 0d8f7c2..9d33245 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -79,21 +79,19 @@ cd "$REPO_ROOT" Expected proxy endpoints: ```apache -ProxyPass /ws ws://127.0.0.1:8765 -ProxyPassReverse /ws ws://127.0.0.1:8765 -ProxyPass /auth/session/ http://127.0.0.1:8765/auth/session/ -ProxyPassReverse /auth/session/ http://127.0.0.1:8765/auth/session/ +ProxyPass /chgrid/ws ws://127.0.0.1:8765/chgrid/ws +ProxyPassReverse /chgrid/ws ws://127.0.0.1:8765/chgrid/ws +ProxyPass /chgrid/auth/session/ http://127.0.0.1:8765/chgrid/auth/session/ +ProxyPassReverse /chgrid/auth/session/ http://127.0.0.1:8765/chgrid/auth/session/ ``` The websocket server enforces browser origin matching against `CHGRID_HOST_ORIGIN`, so the public site origin must match that env var exactly. +The `server.base_path` value in `config.toml` must match the published client path and the proxy paths above. What each route does: -- `/ws`: websocket signaling (presence, movement, item actions, chat, voice signaling). -- `/auth/session/set`: called by client after successful login to set `HttpOnly` session cookie. -- `/auth/session/clear`: called by client on logout/session-reset to clear `HttpOnly` session cookie. - -Important: -- Keep `/auth/session/*` at domain root even when the app is served from a subpath like `/chgrid`. +- `ws`: websocket signaling (presence, movement, item actions, chat, voice signaling). +- `auth/session/set`: called by client after successful login to set the instance-scoped `HttpOnly` session cookie. +- `auth/session/clear`: called by client on logout/session-reset to clear the instance-scoped `HttpOnly` session cookie. After Apache changes, reload Apache using your host's command. diff --git a/deploy/apache/chgrid-vhost-snippet.conf b/deploy/apache/chgrid-vhost-snippet.conf index 847e1fa..e9bdbcd 100644 --- a/deploy/apache/chgrid-vhost-snippet.conf +++ b/deploy/apache/chgrid-vhost-snippet.conf @@ -7,14 +7,13 @@ # SetEnv CHGRID_HOST_ORIGIN https://example.com # Proxy websocket signaling endpoint to local Python service. -# `/ws` is used by the browser signaling client for realtime packets. -ProxyPass /ws ws://127.0.0.1:8765 -ProxyPassReverse /ws ws://127.0.0.1:8765 +# Replace `/chgrid/` with the same value configured in `server.base_path`. +ProxyPass /chgrid/ws ws://127.0.0.1:8765/chgrid/ws +ProxyPassReverse /chgrid/ws ws://127.0.0.1:8765/chgrid/ws # Proxy auth cookie helper endpoints to local Python service. -# These root-scoped paths are required even when the app is hosted under `/chgrid`. -# The client calls `/auth/session/set` after login and `/auth/session/clear` on logout/session-reset. -ProxyPass /auth/session/ http://127.0.0.1:8765/auth/session/ -ProxyPassReverse /auth/session/ http://127.0.0.1:8765/auth/session/ +# These paths should live under the same instance base path. +ProxyPass /chgrid/auth/session/ http://127.0.0.1:8765/chgrid/auth/session/ +ProxyPassReverse /chgrid/auth/session/ http://127.0.0.1:8765/chgrid/auth/session/ # Ensure HTML entrypoint is never cached so version updates are picked up quickly. diff --git a/deploy/scripts/deploy_client.sh b/deploy/scripts/deploy_client.sh index d527cf7..b8cc316 100755 --- a/deploy/scripts/deploy_client.sh +++ b/deploy/scripts/deploy_client.sh @@ -57,6 +57,7 @@ except ModuleNotFoundError: # pragma: no cover - compatibility fallback config_path = Path(sys.argv[1]) host = "127.0.0.1" port = 8765 +base_path = "/" if config_path.exists(): with config_path.open("rb") as fp: data = tomllib.load(fp) @@ -72,7 +73,9 @@ if config_path.exists(): port = int(server.get("port", port)) except (TypeError, ValueError): port = 8765 -print(f"http://{host}:{port}/auth/session/check") + raw_base_path = str(server.get("base_path", base_path)).strip() or "/" + base_path = "/" if raw_base_path == "/" else f"/{raw_base_path.strip('/')}/" +print(f"http://{host}:{port}{base_path}auth/session/check") PY )" escaped_host_origin=${CHGRID_HOST_ORIGIN//\\/\\\\} diff --git a/docs/local.md b/docs/local.md index b183300..d4faefa 100644 --- a/docs/local.md +++ b/docs/local.md @@ -21,12 +21,13 @@ Open: `http://localhost:5173` Defaults: - Server reads `config.toml` automatically when present. - Server default bind/port is `127.0.0.1:8765`. +- Server default base path is `/` for local/dev; production subpath deploys should set `server.base_path` to match the published client path such as `/chgrid/`. - Server defaults to TLS-required unless you set `network.allow_insecure_ws=true` or pass `--allow-insecure-ws` for local/dev. - Client dev default is `localhost:5173`. - Auth requires `CHGRID_AUTH_SECRET` in environment. - Browser-origin enforcement requires `CHGRID_HOST_ORIGIN` in environment. - A starter env file is available at `server/.env.sample`. -- Saved login uses server-managed `HttpOnly` cookie (`chgrid_session_token`) via `GET /auth/session/set` and `GET /auth/session/clear` (both require `X-Chgrid-Auth-Client: 1`). +- Saved login uses instance-scoped server-managed `HttpOnly` cookie helpers under the configured base path (for example `/chgrid/auth/session/set` and `/chgrid/auth/session/clear`) and both require `X-Chgrid-Auth-Client: 1`. ## Quick Restarts diff --git a/docs/protocol-notes.md b/docs/protocol-notes.md index 8185fd5..022e3e0 100644 --- a/docs/protocol-notes.md +++ b/docs/protocol-notes.md @@ -96,7 +96,7 @@ This is a behavior guide for packet semantics beyond raw schemas. - `policy` (`usernameMinLength`, `usernameMaxLength`, `passwordMinLength`, `passwordMaxLength`) - `auth_required.authPolicy`: server auth limits advertised before login/register submit. - `auth_result.authPolicy`: server auth limits echoed on auth success/failure responses. -- `auth_result.sessionToken` is used by the client to call server HTTP endpoint `GET /auth/session/set` (`Authorization: Bearer `, `X-Chgrid-Auth-Client: 1`) so the server can issue `Set-Cookie: chgrid_session_token=...; HttpOnly`. +- `auth_result.sessionToken` is used by the client to call the instance-scoped HTTP endpoint `GET auth/session/set` (`Authorization: Bearer `, `X-Chgrid-Auth-Client: 1`) so the server can issue `Set-Cookie: chgrid_session_token=...; HttpOnly`. - `welcome.worldConfig.gridSize`: server-authoritative grid size used by clients for bounds/drawing. - `welcome.worldConfig.movementTickMs`: server movement-rate window used for client movement pacing. - `welcome.worldConfig.movementMaxStepsPerTick`: max allowed grid steps per movement window. @@ -126,9 +126,10 @@ This is a behavior guide for packet semantics beyond raw schemas. - Server persists account state (last nickname + last position) and restores spawn from that state on auth login/resume. - Server also supports websocket handshake cookie resume: - accepts browser sockets only when websocket `Origin` matches `CHGRID_HOST_ORIGIN` + - websocket and auth helper routes are scoped under the configured `server.base_path` - reads `chgrid_session_token` from websocket `Cookie` header - attempts resume before sending `auth_required` - - exposes `GET /auth/session/clear` to expire the `HttpOnly` cookie (`X-Chgrid-Auth-Client: 1` and matching `Origin` required) + - exposes `GET auth/session/clear` to expire the `HttpOnly` cookie (`X-Chgrid-Auth-Client: 1` and matching `Origin` required) - Server applies auth hardening before accepting login/register/resume: - login/register PBKDF2 work runs off the event loop in bounded worker concurrency - repeated auth failures are rate-limited by IP and IP+identity windows diff --git a/docs/runtime-flow.md b/docs/runtime-flow.md index ed26745..891e47d 100644 --- a/docs/runtime-flow.md +++ b/docs/runtime-flow.md @@ -5,14 +5,14 @@ 1. User clicks connect. 2. Client validates auth form and sets up local media. 3. Client connects signaling websocket from the configured app origin. -4. Server accepts the socket only when the browser `Origin` matches `CHGRID_HOST_ORIGIN`, then attempts cookie-based session resume from websocket handshake cookie (`chgrid_session_token`). +4. Server accepts the socket only on the configured instance websocket path and when the browser `Origin` matches `CHGRID_HOST_ORIGIN`, then attempts cookie-based session resume from websocket handshake cookie (`chgrid_session_token`). 5. If resume does not authenticate, server sends `auth_required`. - includes `authPolicy` limits for username/password. 6. Client sends `auth_login` or `auth_register` (or explicit `auth_resume` if provided by caller). 7. Server sends `auth_result`. - includes role + permissions for authenticated session. -8. Client persists authenticated session into a server-managed `HttpOnly` cookie via `GET /auth/session/set` (`Authorization: Bearer `, `X-Chgrid-Auth-Client: 1`), and clears it via `GET /auth/session/clear` (`X-Chgrid-Auth-Client: 1`) on logout/session errors. - - the optional PHP media proxy validates that same cookie through `GET /auth/session/check` before relaying media +8. Client persists authenticated session into instance-scoped server-managed `HttpOnly` cookie helpers under the active app base path via `GET auth/session/set` (`Authorization: Bearer `, `X-Chgrid-Auth-Client: 1`), and clears it via `GET auth/session/clear` on logout/session errors. + - the optional PHP media proxy validates that same cookie through `GET auth/session/check` before relaying media 9. Server sends `welcome` with users/items snapshot. 10. Client: - applies `welcome.worldConfig.gridSize` for authoritative grid bounds/rendering diff --git a/server/app/config.py b/server/app/config.py index 1e9ceb9..32402fb 100644 --- a/server/app/config.py +++ b/server/app/config.py @@ -13,6 +13,7 @@ class ServerConfigSection(BaseModel): bind_ip: str = "127.0.0.1" port: int = 8765 + base_path: str = "/" class NetworkConfigSection(BaseModel): diff --git a/server/app/server.py b/server/app/server.py index c749f7b..3cc8a69 100644 --- a/server/app/server.py +++ b/server/app/server.py @@ -133,9 +133,10 @@ RADIO_METADATA_TIMEOUT_S = 6.0 CLOCK_ANNOUNCE_POLL_INTERVAL_S = 1.0 AUTH_SESSION_COOKIE_NAME = "chgrid_session_token" AUTH_SESSION_COOKIE_MAX_AGE_SECONDS = 14 * 24 * 60 * 60 -AUTH_SESSION_COOKIE_SET_PATH = "/auth/session/set" -AUTH_SESSION_COOKIE_CLEAR_PATH = "/auth/session/clear" -AUTH_SESSION_COOKIE_CHECK_PATH = "/auth/session/check" +AUTH_SESSION_COOKIE_SET_PATH = "auth/session/set" +AUTH_SESSION_COOKIE_CLEAR_PATH = "auth/session/clear" +AUTH_SESSION_COOKIE_CHECK_PATH = "auth/session/check" +WEBSOCKET_PATH = "ws" AUTH_SESSION_COOKIE_CLIENT_HEADER = "X-Chgrid-Auth-Client" AUTH_LOGIN_FAILURE_MESSAGE = "We couldn't log you in. Check your details and try again." AUTH_RESUME_FAILURE_MESSAGE = "We couldn't restore your session. Please log in again." @@ -162,6 +163,7 @@ class SignalingServer: state_save_debounce_ms: int = 200, state_save_max_delay_ms: int = 1000, host_origin: str | None = None, + base_path: str = "/", ): """Initialize runtime state, TLS context, and item service.""" @@ -190,6 +192,11 @@ class SignalingServer: self.instance_id = str(uuid.uuid4()) 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.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) + self.auth_session_cookie_check_path = self._base_path_join(AUTH_SESSION_COOKIE_CHECK_PATH) self.state_save_debounce_ms = max(1, int(state_save_debounce_ms)) self.state_save_max_delay_ms = max(self.state_save_debounce_ms, int(state_save_max_delay_ms)) self._pending_state_save_handle: asyncio.TimerHandle | None = None @@ -263,6 +270,23 @@ class SignalingServer: "passwordMaxLength": self.auth_service.password_max_length, } + @staticmethod + def _normalize_base_path(value: str) -> str: + """Normalize one instance base path to leading/trailing slash form.""" + + text = str(value).strip() + if not text or text == "/": + return "/" + return f"/{text.strip('/')}/" + + def _base_path_join(self, suffix: str) -> str: + """Join one instance-relative route suffix to the configured base path.""" + + token = suffix.lstrip("/") + if self.base_path == "/": + return f"/{token}" + return f"{self.base_path}{token}" + def _session_cookie_secure(self, request: HttpRequest | None = None) -> bool: """Return True when session cookies should be marked Secure.""" @@ -278,7 +302,7 @@ class SignalingServer: secure = "; Secure" if self._session_cookie_secure(request) else "" return ( - f"{AUTH_SESSION_COOKIE_NAME}={token}; Path=/; HttpOnly; SameSite=Lax; " + f"{AUTH_SESSION_COOKIE_NAME}={token}; Path={self.base_path}; HttpOnly; SameSite=Lax; " f"Max-Age={AUTH_SESSION_COOKIE_MAX_AGE_SECONDS}{secure}" ) @@ -286,7 +310,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=/; HttpOnly; SameSite=Lax; Max-Age=0{secure}" + return f"{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.""" @@ -316,8 +340,18 @@ class SignalingServer: """Handle lightweight same-origin auth cookie set/clear HTTP endpoints.""" path = request.path.split("?", 1)[0] - if path not in {AUTH_SESSION_COOKIE_SET_PATH, AUTH_SESSION_COOKIE_CLEAR_PATH, AUTH_SESSION_COOKIE_CHECK_PATH}: + auth_paths = { + self.auth_session_cookie_set_path, + self.auth_session_cookie_clear_path, + self.auth_session_cookie_check_path, + } + if path == self.websocket_path: return None + if path not in auth_paths: + headers = Headers() + headers["Content-Type"] = "text/plain; charset=utf-8" + headers["Cache-Control"] = "no-store" + return HttpResponse(404, "Not Found", headers, b"not found") headers = Headers() headers["Content-Type"] = "text/plain; charset=utf-8" @@ -328,7 +362,7 @@ class SignalingServer: if not self._origin_allowed(request): return HttpResponse(403, "Forbidden", headers, b"origin not allowed") - if path == AUTH_SESSION_COOKIE_CHECK_PATH: + 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) if not token: @@ -339,7 +373,7 @@ class SignalingServer: return HttpResponse(401, "Unauthorized", headers, b"invalid session") return HttpResponse(204, "No Content", headers, b"") - if path == AUTH_SESSION_COOKIE_CLEAR_PATH: + if path == self.auth_session_cookie_clear_path: headers["Set-Cookie"] = self._clear_session_cookie_header(request=request) return HttpResponse(200, "OK", headers, b"cleared") @@ -1415,6 +1449,11 @@ class SignalingServer: LOGGER.info("websocket opened id=%s", client.id) try: + request = getattr(websocket, "request", None) + request_path = str(getattr(request, "path", "")).split("?", 1)[0] + if request_path != self.websocket_path: + await websocket.close() + return cookie_token = self._session_token_from_websocket_cookie(websocket) if cookie_token: await self._handle_auth_packet( @@ -3245,5 +3284,6 @@ def run() -> None: state_save_debounce_ms=config.storage.state_save_debounce_ms, state_save_max_delay_ms=config.storage.state_save_max_delay_ms, host_origin=host_origin, + base_path=config.server.base_path, ) asyncio.run(server.start()) diff --git a/server/config.example.toml b/server/config.example.toml index 64b02ec..aeafe06 100644 --- a/server/config.example.toml +++ b/server/config.example.toml @@ -3,6 +3,8 @@ bind_ip = "127.0.0.1" # Listen port for signaling websocket server. port = 8765 +# Public base path for this grid instance. Examples: "/", "/chgrid/", "/ttgrid/". +base_path = "/" [network] # Maximum inbound websocket message size in bytes. diff --git a/server/tests/test_config.py b/server/tests/test_config.py index a864a14..6e2c269 100644 --- a/server/tests/test_config.py +++ b/server/tests/test_config.py @@ -8,6 +8,7 @@ from app.config import load_config def test_load_config_defaults_when_path_none() -> None: cfg = load_config(None) assert cfg.server.bind_ip == "127.0.0.1" + assert cfg.server.base_path == "/" assert cfg.network.allow_insecure_ws is False assert cfg.storage.state_file == "runtime/items.json" assert cfg.storage.state_save_debounce_ms == 200 @@ -43,3 +44,18 @@ state_save_max_delay_ms = 900 cfg = load_config(config_path) assert cfg.storage.state_save_debounce_ms == 150 assert cfg.storage.state_save_max_delay_ms == 900 + + +def test_load_config_reads_server_base_path(tmp_path: Path) -> None: + config_path = tmp_path / "config.toml" + config_path.write_text( + """ +[network] +allow_insecure_ws = true + +[server] +base_path = "/ttgrid/" +""".strip() + ) + cfg = load_config(config_path) + assert cfg.server.base_path == "/ttgrid/" diff --git a/server/tests/test_http_session_cookie.py b/server/tests/test_http_session_cookie.py index 6e92d59..bbe89f4 100644 --- a/server/tests/test_http_session_cookie.py +++ b/server/tests/test_http_session_cookie.py @@ -24,13 +24,17 @@ def _request(path: str, headers: dict[str, str] | None = None) -> Request: return Request(path=path, headers=values) +def _server() -> SignalingServer: + return SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com", base_path="/chgrid/") + + @pytest.mark.asyncio async def test_session_cookie_set_endpoint_sets_httponly_cookie() -> None: - server = SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com") + server = _server() username = f"user_{uuid.uuid4().hex[:8]}" session = server.auth_service.register(username, "password99") request = _request( - AUTH_SESSION_COOKIE_SET_PATH, + server.auth_session_cookie_set_path, headers={ AUTH_SESSION_COOKIE_CLIENT_HEADER: "1", "Authorization": f"Bearer {session.token}", @@ -44,15 +48,16 @@ async def test_session_cookie_set_endpoint_sets_httponly_cookie() -> None: assert response.status_code == 200 set_cookie = response.headers.get("Set-Cookie", "") assert f"{AUTH_SESSION_COOKIE_NAME}=" in set_cookie + assert "Path=/chgrid/" in set_cookie assert "HttpOnly" in set_cookie assert "SameSite=Lax" in set_cookie @pytest.mark.asyncio async def test_session_cookie_clear_endpoint_expires_cookie() -> None: - server = SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com") + server = _server() request = _request( - AUTH_SESSION_COOKIE_CLEAR_PATH, + server.auth_session_cookie_clear_path, headers={AUTH_SESSION_COOKIE_CLIENT_HEADER: "1", "Origin": "https://example.com"}, ) @@ -68,11 +73,11 @@ async def test_session_cookie_clear_endpoint_expires_cookie() -> None: @pytest.mark.asyncio async def test_session_cookie_check_endpoint_accepts_valid_cookie() -> None: - server = SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com") + server = _server() username = f"user_{uuid.uuid4().hex[:8]}" session = server.auth_service.register(username, "password99") request = _request( - AUTH_SESSION_COOKIE_CHECK_PATH, + server.auth_session_cookie_check_path, headers={ AUTH_SESSION_COOKIE_CLIENT_HEADER: "1", "Cookie": f"{AUTH_SESSION_COOKIE_NAME}={session.token}", @@ -88,9 +93,9 @@ async def test_session_cookie_check_endpoint_accepts_valid_cookie() -> None: @pytest.mark.asyncio async def test_session_cookie_check_endpoint_rejects_missing_cookie() -> None: - server = SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com") + server = _server() request = _request( - AUTH_SESSION_COOKIE_CHECK_PATH, + server.auth_session_cookie_check_path, headers={AUTH_SESSION_COOKIE_CLIENT_HEADER: "1", "Origin": "https://example.com"}, ) @@ -102,9 +107,9 @@ async def test_session_cookie_check_endpoint_rejects_missing_cookie() -> None: @pytest.mark.asyncio async def test_session_cookie_helpers_reject_wrong_origin() -> None: - server = SignalingServer("127.0.0.1", 8765, None, None, host_origin="https://example.com") + server = _server() request = _request( - AUTH_SESSION_COOKIE_CLEAR_PATH, + server.auth_session_cookie_clear_path, headers={AUTH_SESSION_COOKIE_CLIENT_HEADER: "1", "Origin": "https://evil.example.com"}, )