Skip to content

fix(console): end console sessions 8 hours after login - #875

Merged
hitesh96db merged 2 commits into
mainfrom
fix/console-session-absolute-lifetime
Nov 6, 2026
Merged

hitesh96db merged 2 commits into
mainfrom
fix/console-session-absolute-lifetime

Conversation

@hitesh96db

@hitesh96db hitesh96db commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Each console request that passes its session check resets the 8-hour idle timeout. A session token used at least every 8 hours therefore stayed valid until the server process restarted. The cookie's Max-Age=28800 does not bound this on the server side: the server never sees that attribute, and a stolen token can be sent without it.

This change records when each session was created and ends the session 8 hours after login, however active it is. The login cookie's Max-Age is built from the same constant, so the browser drops the cookie at the moment the server ends the session.

Follow-up to #704 (sessions end when the principal is deleted or its password changes) and #773 (Secure cookie flag).

Changes

  • crates/server/src/console/session.rs
    • New SESSION_MAX_LIFETIME_SECS (8 h, pub(crate)) and SESSION_MAX_LIFETIME, plus SessionEntry.created_at.
    • SessionEntry::is_expired() checks both limits. Lookup uses it, and so does the prune that runs on insert when the store is full.
    • Activity refreshes last_active only, never created_at.
    • The 8-hour idle check stays. With the two limits equal, the lifetime ends every session first; the idle check takes effect only if a shorter idle timeout is set later.
    • The test-only constructor with_ttl is replaced by with_limits(ttl, max_lifetime).
  • crates/server/src/console/pages/auth_pages.rs: the login cookie's Max-Age is SESSION_MAX_LIFETIME_SECS instead of the literal 28800.
  • crates/server/src/console/session_tests.rs adds four tests:
    • the lifetime constants (8 h, and the idle timeout no longer than the lifetime);
    • a session that was just active but logged in 121 s ago, under a 120 s cap, is refused, removed, and stays refused;
    • a request that passes the check leaves created_at unchanged;
    • the insert-time prune drops both a session past its lifetime and a session past its idle timeout.
  • Docs:
    • getting-started and the admin guide: "Sessions end 8 hours after login."
    • Architecture guide and security model: the 8-hour lifetime, and that the cookie expires at the same time.
    • Troubleshooting, "Console redirects to login page on every request": the cause now names the 8-hour lifetime.

Behaviour

  • Browser users see no change. The cookie already expired 8 hours after login, and the server now ends the session at that same time.
  • A token presented without the cookie's expiry, such as a stolen token, is now refused 8 hours after login, instead of staying valid for as long as it is used at least every 8 hours.

Testing

  • cargo test -p extenddb-server --lib console: 38/38 pass.
  • Each of these temporary breakages fails its test:
    • lifetime check removed from is_expired: the two lifetime tests;
    • prune checking only created_at: the prune test;
    • lifetime set back to 24 h: the constant test.
  • cargo +1.97.0 fmt --all -- --check and cargo +1.97.0 clippy -p extenddb-server --all-targets -- -D warnings are clean.
  • Python integration tests were not run.

A console session's idle timeout is refreshed by every request that
passes its check, so a session token used at least every 8 hours stayed
valid until the process restarted. The cookie's Max-Age does not bound
it: the server never sees that attribute, and a stolen token can be
sent without it.

Record when each session was created and end it 24 hours after login,
however active it is, in addition to the 8-hour idle timeout. Lookup
and the insert-time prune apply both limits. Activity refreshes only
the idle timer, never the login time.
@hitesh96db hitesh96db added security v1 Needed for the 1.0 release release-blocker Must ship for its milestone's release labels Nov 5, 2026

@LeeroyHannigan LeeroyHannigan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The cap works as described, but at 24 hours a stolen token stays valid for 16 hours after the browser drops the cookie.

This PR stores the login time of each console session. The server now refuses a session 24 hours after login, and the 8 hour idle timeout still applies. Only create writes created_at, at login, and touch writes only last_active, so no request can reset the login time. lookup and the insert-time prune both call is_expired. Logout removes the entry, and a restart empties the in-memory store.

Both limits read Instant, so setting the system clock does not move them. I tested the edge cases of is_expired in a scratch test. A session exactly 24 hours old is still valid, and the check refuses a session one nanosecond older. The idle check uses the same > comparison. With a now earlier than the stored instants, or with Duration::MAX limits, is_expired returns false and does not panic.

I ran the server on 127.0.0.1 under libfaketime, which also moves the monotonic clock that Instant reads. On this branch, a token sent at 7, 14 and 21 hours and at 23 h 59 min after login got 200 each time. At 24 h 1 min, with 2 minutes idle, it got the redirect to /console/login, and so did the next request. On main, the same requests got 200 at 24 h 1 min and at 31 hours. That session ended only after 8 h 1 min idle. After a restart, the token from the earlier process got the login redirect.

On Rust 1.88.0, cargo test -p extenddb-server --lib console passes 38 of 38. With session.rs reverted to main, the test module does not compile (8 errors). I broke each part of the change in turn, and each new test fails against its part:

  • is_expired without the lifetime clause: the two lifetime tests, as the description says
  • lookup with the old idle-only check: an_active_session_ends_at_its_maximum_lifetime_and_stays_ended
  • the prune with the old idle-only retain: creating_a_session_prunes_sessions_past_their_maximum_lifetime
  • a touch that also resets created_at: activity_does_not_extend_the_maximum_lifetime
  • SESSION_MAX_LIFETIME at 25 hours: sessions_last_at_most_twenty_four_hours_after_login

cargo +1.97.0 fmt --all -- --check and cargo +1.97.0 clippy -p extenddb-server --all-targets -- -D warnings are clean. Every CI check passes except Analyze (rust).

Findings

Major, crates/server/src/console/session.rs:38. The login response sets Max-Age=28800 (crates/server/src/console/pages/auth_pages.rs:61), and on the running server no later response set the cookie again. A browser therefore drops the cookie 8 hours after login, however active the session is. After that, only a client that ignores Max-Age can use the session, for example one that holds a stolen token. The 24 hour cap gives that client 16 more hours. Set SESSION_MAX_LIFETIME to 8 hours, the time at which the browser drops the cookie, so browser users see no change. Build the cookie's Max-Age from the same constant. With an 8 hour cap, the idle check never ends a session first. A session idle for 8 hours is also 8 hours past login.

Minor, docs/getting-started.md:1284 and docs/manuals/05-admin-guide.md:832. These guides describe the console in a browser, and they now say that an active session lasts up to 24 hours. In a browser the session ends 8 hours after login, when the cookie expires. With an 8 hour cap, write "Sessions end 8 hours after login." If the cap stays at 24 hours, add that the browser drops the cookie 8 hours after login. The architecture guide and the security model describe the server, and their lines match the code.

Minor, docs/troubleshooting.md:857. The cause given for "Console redirects to login page on every request" is still "sessions last 8 hours of inactivity". A user reads this entry after the cap ends a session. Add the cap and the cookie's 8 hour lifetime to that cause.

Optional, crates/server/src/console/session_tests.rs:315. The prune test covers only the lifetime half of is_expired. A prune that checks only created_at passes all 13 session tests. Add a session past the idle timeout to this test. Assert that the prune removes it.

Not checked

  • Why Analyze (rust) (CodeQL) failed. The job stopped in its upload step with no error message, and there is no open alert for this PR. The same job passed on main and on nine other PRs on 5 November. A re-run will show whether it fails again.

The login cookie's Max-Age is 8 hours and is never refreshed, so a
browser drops it 8 hours after login however active the session is. A
24-hour server cap only gave a client that ignores Max-Age, such as one
holding a stolen token, 16 more hours.

Cap sessions at 8 hours after login and build the cookie's Max-Age from
the same constant, so the browser and the server end a session at the
same time. The 8-hour idle check stays; with the two equal, the
lifetime ends every session first.

Describe the 8-hour lifetime in the user guides and the troubleshooting
entry for login redirects, and extend the insert-time prune test to
cover an idle session as well as an outlived one.
@hitesh96db hitesh96db changed the title fix(console): cap console sessions at 24 hours after login fix(console): end console sessions 8 hours after login Nov 6, 2026

@LeeroyHannigan LeeroyHannigan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

b449df0 fixes the four findings from my last review, so I approve.

  • The cap is now 8 hours. The login cookie's Max-Age comes from the same constant, SESSION_MAX_LIFETIME_SECS (crates/server/src/console/session.rs:40, used at crates/server/src/console/pages/auth_pages.rs:61).
  • docs/getting-started.md:1284 and docs/manuals/05-admin-guide.md:832 now say that sessions end 8 hours after login. The entry at docs/troubleshooting.md:857 gives the lifetime and the cookie as the cause.
  • creating_a_session_prunes_idle_and_outlived_sessions now holds one session past the lifetime and one past the idle timeout. When the prune checks only the login time, the test fails at line 354. When the prune checks only the idle time, it fails at line 353.

On Rust 1.88.0, the 13 session tests pass at this head.

@hitesh96db
hitesh96db added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit 71bcf55 Nov 6, 2026
39 checks passed
@hitesh96db
hitesh96db deleted the fix/console-session-absolute-lifetime branch November 6, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-blocker Must ship for its milestone's release security v1 Needed for the 1.0 release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants