fix(auth): tie remember-me cookie lifetime to TOKEN_TTL (#4472)

The persistent login cookie's max_age hardcoded 60 * 60 * 24 * 7, an
independent copy of the session token lifetime that core/auth.py already
defines once as TOKEN_TTL (and reports to the frontend via /api/auth/policy
as session_days). If TOKEN_TTL changes, the cookie silently drifts: the
browser keeps a cookie for a token whose lifetime no longer matches.

Import TOKEN_TTL and use it for the cookie max_age so the session lifetime
has a single source of truth. No behaviour change at the current value.

Fixes #4471
This commit is contained in:
Karl Jussila
2026-06-18 14:15:48 -05:00
committed by GitHub
parent 0bfc7750a2
commit 396e26b4bf
2 changed files with 57 additions and 2 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ import re
from pathlib import Path
from core.atomic_io import atomic_write_json, atomic_write_text
from core.auth import AuthManager, RESERVED_USERNAMES, SetAdminResult
from core.auth import AuthManager, RESERVED_USERNAMES, SetAdminResult, TOKEN_TTL
from src.constants import DEEP_RESEARCH_DIR, MEMORY_FILE, PASSWORD_MIN_LENGTH, SKILLS_DIR
from src.rate_limiter import RateLimiter
from src.settings_scrub import scrub_settings
@@ -161,7 +161,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
path="/",
)
if body.remember:
cookie_kwargs["max_age"] = 60 * 60 * 24 * 7 # 7 days
cookie_kwargs["max_age"] = TOKEN_TTL
response.set_cookie(**cookie_kwargs)
return {"ok": True, "username": username}
+55
View File
@@ -215,3 +215,58 @@ def test_setup_rejects_seven_char_password(tmp_path):
asyncio.run(endpoint(body=body, request=request))
assert exc.value.status_code == 400
# ── Login "remember me" cookie lifetime ────────────────────────────────
class _CapturingResponse:
"""Stand-in for fastapi.Response that records set_cookie kwargs."""
def __init__(self):
self.cookie_kwargs = None
def set_cookie(self, **kwargs):
self.cookie_kwargs = kwargs
def _login_endpoint(auth_manager):
sys.modules.pop("routes.auth_routes", None)
_real_core_package()
from routes.auth_routes import LoginRequest, setup_auth_routes
router = setup_auth_routes(auth_manager)
for route in router.routes:
if getattr(route, "path", None) == "/api/auth/login":
return route.endpoint, LoginRequest
raise AssertionError("login route not found")
def test_remember_cookie_max_age_matches_token_ttl(tmp_path):
auth_mod = _auth_module()
mgr = _make_manager(tmp_path)
mgr.create_user("alice", "alice-password", is_admin=False)
endpoint, LoginRequest = _login_endpoint(mgr)
request = SimpleNamespace(client=SimpleNamespace(host="127.0.0.1"))
response = _CapturingResponse()
body = LoginRequest(username="alice", password="alice-password", remember=True)
result = asyncio.run(endpoint(body=body, request=request, response=response))
assert result == {"ok": True, "username": "alice"}
# The persistent cookie must outlive neither more nor less than the token.
assert response.cookie_kwargs["max_age"] == auth_mod.TOKEN_TTL
def test_no_remember_omits_cookie_max_age(tmp_path):
mgr = _make_manager(tmp_path)
mgr.create_user("bob", "bob-password", is_admin=False)
endpoint, LoginRequest = _login_endpoint(mgr)
request = SimpleNamespace(client=SimpleNamespace(host="127.0.0.1"))
response = _CapturingResponse()
body = LoginRequest(username="bob", password="bob-password", remember=False)
asyncio.run(endpoint(body=body, request=request, response=response))
# Without "remember", the cookie is a session cookie (no max_age).
assert "max_age" not in response.cookie_kwargs