Skip to content

Set SESSION_COOKIE_AGE to 399 days so logins survive longer than two weeks - #3587

Open
mjradwin wants to merge 1 commit into
Sefaria:masterfrom
mjradwin:claude/session-cookie-age-399-days
Open

Set SESSION_COOKIE_AGE to 399 days so logins survive longer than two weeks#3587
mjradwin wants to merge 1 commit into
Sefaria:masterfrom
mjradwin:claude/session-cookie-age-399-days

Conversation

@mjradwin

@mjradwin mjradwin commented Aug 5, 2026

Copy link
Copy Markdown

Description

SESSION_COOKIE_AGE is not set anywhere in the codebase, so Django's default of
1209600 seconds (2 weeks) has been governing how long a login lasts. Users get
signed out roughly a fortnight after their last visit, far short of the ~400-day
cookie lifetime browsers permit.

Because SESSION_SAVE_EVERY_REQUEST is already True (settings.py), the window
is rolling: the logout lands two weeks after a user's last visit, not two
weeks after login. That's why the symptom tends to get reported as "about a
month" rather than "exactly 14 days" — the interval a user perceives is always
longer than the setting itself.

This sets the session lifetime to 399 days, leaving a day of headroom under the
400-day cap Chrome, Firefox and Safari apply to cookie lifetimes. A value at or
above the cap gets silently truncated by the browser, which would put the cookie
back out of sync with the server-side record.

One setting covers both halves of the problem: Django feeds SESSION_COOKIE_AGE
to the sessionid cookie's Max-Age and to django_session.expire_date, so
the cookie cannot outlive the session record it points at. A long-lived cookie
riding on a short-lived server record would log the user out anyway.

Code Changes

  • sefaria/settings.py — adds SESSION_COOKIE_AGE = 399 * 24 * 60 * 60
    (34,473,600 seconds) alongside the existing SESSION_SAVE_EVERY_REQUEST.
  • sefaria/local_settings_example.py — same value, documented for local dev.
  • sefaria/system/tests/session_settings_test.py — new. Pins the value, pins
    that it stays under the 400-day browser cap, pins SESSION_SAVE_EVERY_REQUEST,
    and pins the invariant that matters: the cookie's Max-Age and
    SessionStore.get_expiry_age() (what becomes django_session.expire_date)
    both derive from the same setting. The cookie test uses the signed_cookies
    engine via override_settings, so no database is required.

Notes

  • Existing sessions migrate themselves. SESSION_SAVE_EVERY_REQUEST = True
    means each logged-in user's next request rewrites both the cookie and the DB
    row to +399 days. No data migration needed. Users already past their two weeks
    are gone regardless.
  • django_session will grow. Rows now live 399 days instead of 14, and there
    is no clearsessions job today, so expired rows are never purged. A companion
    PR adds that cronjob to the helm chart. The two PRs are independent and can be
    accepted or rejected separately — this one is correct on its own, and the helm
    one is correct on its own.
  • Redis. In production SESSION_ENGINE is cached_db, so sessions are also
    written to Redis DB 0, now with a 399-day TTL instead of 14 days. Eviction is
    safe (cached_db falls back to the database), but memory use is worth
    watching.
  • Security trade-off, flagged deliberately for review: 399-day sessions live
    a long time on shared or public computers, and a stolen sessionid stays valid
    much longer. Django rotates the session key on login and on password change, so
    those paths are covered, but the longer window is a real policy decision, not
    just a config tweak.
  • Not the cause, in case it comes up: the default Redis cache in the helm
    chart has "TIMEOUT": 60 * 60 * 24 * 30. That is not the session expiry —
    cached_db.save() passes an explicit per-key timeout that overrides the cache
    default, and on a cache miss load() falls back to the django_session row.
    It cannot log anyone out.
  • Test filename: session_settings_test.py, not test_session_settings.py.
    pytest.ini collects sefaria/system/tests/*_test.py, so a test_*.py name in
    that directory is never collected. Worth a separate look: this means
    test_middleware.py, test_database.py, test_decorators.py and
    test_language_module_switching.py in that same directory are currently not
    running in CI. Out of scope here, left untouched.

🤖 Co-authored with Claude Code

SESSION_COOKIE_AGE was never set anywhere in the codebase, so Django's
default of 1209600 seconds (2 weeks) governed how long a login lasted.
Users were signed out roughly a fortnight after their last visit, well
short of the ~400-day cookie lifetime browsers permit.

Because SESSION_SAVE_EVERY_REQUEST is already True the window is rolling,
so the logout lands two weeks after a user's *last* visit rather than two
weeks after login -- which is why the symptom gets reported as "about a
month" rather than "exactly 14 days".

399 days leaves a day of headroom under the 400-day cap that Chrome,
Firefox and Safari apply to cookie lifetimes; a value at or above the cap
would be silently truncated by the browser and put the cookie back out of
sync with the server-side record.

One setting covers both halves of the problem: Django feeds
SESSION_COOKIE_AGE to the sessionid cookie's Max-Age *and* to
django_session.expire_date, so the cookie cannot outlive the session
record it points at.

Tests are named session_settings_test.py, not test_session_settings.py:
pytest.ini collects sefaria/system/tests/*_test.py, so a test_*.py name
in this directory is silently never run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt3gvV73jEpFCnuvU2hm1c

Copilot AI left a comment

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.

Pull request overview

This PR extends Django session lifetime by explicitly setting SESSION_COOKIE_AGE to 399 days (a rolling window due to SESSION_SAVE_EVERY_REQUEST = True) so users remain logged in longer, and adds tests to pin the intended behavior.

Changes:

  • Set SESSION_COOKIE_AGE = 399 * 24 * 60 * 60 in the main Django settings to extend session lifetime.
  • Document the same setting in local_settings_example.py for local/dev configuration parity.
  • Add a new pytest module asserting the configured session age, browser-cap constraint, rolling-session behavior, and cookie/server expiry derivation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
sefaria/settings.py Sets SESSION_COOKIE_AGE to 399 days and documents rationale/rolling behavior.
sefaria/local_settings_example.py Adds the same SESSION_COOKIE_AGE example value and explanatory comments.
sefaria/system/tests/session_settings_test.py Introduces tests that pin the session lifetime and its coupling to cookie/server-side expiry behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +75 to +81
from django.contrib.sessions.backends.db import SessionStore

assert SessionStore().get_expiry_age() == settings.SESSION_COOKIE_AGE

expected = timezone.now() + timedelta(seconds=settings.SESSION_COOKIE_AGE)
drift = abs((SessionStore().get_expiry_date() - expected).total_seconds())
assert drift < 60
Comment thread sefaria/settings.py
Comment on lines +216 to +220
# the window rolls: every request pushes both out another 399 days.
#
# Django's own default is 1209600 (2 weeks), which is what we were silently
# running on before.
SESSION_COOKIE_AGE = 399 * 24 * 60 * 60 # 34,473,600 seconds
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants