Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions backend/chainlit/auth/cookie.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hmac
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Cookie path mismatch: cookies are set at / but deleted at _cookie_path (CHAINLIT_ROOT_PATH), so auth cookies/chunks cannot be cleaned up under a sub-path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/chainlit/auth/cookie.py, line 28:

<comment>Cookie path mismatch: cookies are set at `/` but deleted at `_cookie_path` (`CHAINLIT_ROOT_PATH`), so auth cookies/chunks cannot be cleaned up under a sub-path.</comment>

<file context>
@@ -24,7 +25,7 @@
 _cookie_secure = _cookie_samesite == "none"
 if _cookie_root_path := os.environ.get("CHAINLIT_ROOT_PATH", None):
-    _cookie_path = os.environ.get(_cookie_root_path, "/")
+    _cookie_path = _cookie_root_path
 else:
     _cookie_path = os.environ.get("CHAINLIT_AUTH_COOKIE_PATH", "/")
</file context>

import os
from typing import Literal, Optional, cast

Expand All @@ -24,7 +25,7 @@
)
_cookie_secure = _cookie_samesite == "none"
if _cookie_root_path := os.environ.get("CHAINLIT_ROOT_PATH", None):
_cookie_path = os.environ.get(_cookie_root_path, "/")
_cookie_path = _cookie_root_path
else:
_cookie_path = os.environ.get("CHAINLIT_AUTH_COOKIE_PATH", "/")
Comment on lines 26 to 30
_state_cookie_lifetime = int(
Expand Down Expand Up @@ -186,11 +187,15 @@ def set_oauth_state_cookie(response: Response, token: str):


def validate_oauth_state_cookie(request: Request, state: str):
"""Check the state from the oauth provider against the browser cookie."""
"""Check the state from the oauth provider against the browser cookie.

Uses ``hmac.compare_digest`` for constant-time comparison to prevent
timing-based side-channel leakage of the expected state value.
"""

oauth_state = request.cookies.get(_state_cookie_name)

if oauth_state != state:
if oauth_state is None or not hmac.compare_digest(oauth_state, state):
raise Exception("oauth state does not correspond")


Expand Down
62 changes: 62 additions & 0 deletions backend/tests/auth/test_cookie.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,68 @@ def test_overwrite_shorter_token_unchunked(client):
assert len(chunk_cookies) == 0, f"Found {len(chunk_cookies)} residual cookies"


def test_cookie_path_uses_chainlit_root_path_value(monkeypatch):
"""CHAINLIT_ROOT_PATH must be used as the cookie path directly.

Before the fix, the code read::

_cookie_path = os.environ.get(_cookie_root_path, "/")

where ``_cookie_root_path`` was the *value* of ``CHAINLIT_ROOT_PATH``,
not a key. This caused ``_cookie_path`` to always be ``"/"`` (the
default) whenever ``CHAINLIT_ROOT_PATH`` was set, because the value
(e.g. ``"/app"``) is not a valid environment variable name. Cookie
deletion then used the wrong path, leaving stale cookies on the client.
"""
monkeypatch.setenv("CHAINLIT_ROOT_PATH", "/myapp")
monkeypatch.delenv("CHAINLIT_AUTH_COOKIE_PATH", raising=False)
importlib.reload(cookie_module)
assert cookie_module._cookie_path == "/myapp", (
f"Expected _cookie_path to be '/myapp' but got '{cookie_module._cookie_path}'. "
"CHAINLIT_ROOT_PATH value should be used directly as the cookie path."
)
Comment on lines +149 to +155


def test_validate_oauth_state_cookie_rejects_missing_state(monkeypatch):
"""validate_oauth_state_cookie must raise when the state cookie is absent."""
from chainlit.auth.cookie import validate_oauth_state_cookie
from starlette.requests import Request as StarletteRequest

scope = {
"type": "http",
"headers": [],
"query_string": b"",
"method": "GET",
"path": "/",
}
request = StarletteRequest(scope)
# Cookies dict is empty — oauth_state cookie is absent
with pytest.raises(Exception, match="oauth state does not correspond"):
validate_oauth_state_cookie(request, "expected_state")


def test_validate_oauth_state_cookie_accepts_correct_state():
"""validate_oauth_state_cookie must not raise when states match."""
from chainlit.auth.cookie import validate_oauth_state_cookie
from starlette.requests import Request as StarletteRequest

state_value = "secret_state_token"

# Build a minimal request with the oauth_state cookie set
scope = {
"type": "http",
"headers": [
(b"cookie", f"oauth_state={state_value}".encode()),
],
"query_string": b"",
"method": "GET",
"path": "/",
}
request = StarletteRequest(scope)
# Should not raise
validate_oauth_state_cookie(request, state_value)


def test_state_cookie_lifetime_default(monkeypatch):
"""Test that _state_cookie_lifetime defaults to 180 seconds (3 minutes)."""
monkeypatch.delenv("CHAINLIT_STATE_COOKIE_LIFETIME", raising=False)
Expand Down
Loading