Skip to content

Opt-in "Remember this browser for 30 days" (#205) - #226

Merged
aleksejs1 merged 1 commit into
mainfrom
feat/remember-browser-205
Oct 4, 2026
Merged

aleksejs1 merged 1 commit into
mainfrom
feat/remember-browser-205

Conversation

@aleksejs1

Copy link
Copy Markdown
Owner

Closes #205, part of #213.

What it does

A checkbox on the login form, off by default (6 locales; the translations are mine and not native-speaker-reviewed). With it:

  • the session ends 30 days after the login — fixed, not extended by activity, and the 12-hour idle timeout doesn't apply to it;
  • the master key is kept in IndexedDB under a non-extractable AES-GCM key, with the session's end date, so a new tab or a restarted browser opens a meeting without the password;
  • the demo account is always refused by the server.

Without the checkbox nothing changes.

Where it differs from the issue

  • The "protects against copying the key off the disk" claim is dropped. The wrapping key is in the same browser profile, so a copy of the profile works in another browser. docs/encryption.md says plainly that anyone who can use the browser, or read its profile, can read the 1:1s for up to 30 days.
  • A password change or reset ends every other session (maintainer decision): a stamp of authHash in the session, no token table. A password change elsewhere leads to the login screen, not "Unlock this tab".
  • The remembered key carries an end date and is deleted once it passes, on any page load (maintainer decision).
  • 30 days are fixed from the login, not an idle threshold: the cookie is only set at login.
  • A remembered key that fails to unwrap is not deleted, and only a login with the checkbox creates one. IndexedDB is shared by all tabs and a tab's view may be stale; a password change and "Unlock this tab" replace the key in place.
  • The BroadcastChannel alternative wasn't built.

Found along the way

  • PHP re-sent the session cookie without an expiry on every request closeForReading() handles, turning the 30-day cookie back into a browser-session one. closeForReading() now removes that header, through Symfony's @internal SessionUtils; the e2e restart test is what would catch that changing.
  • With a cookie lifetime to change, migrate() saves the session under its old id first, which would have left a working login there. The login is now written after the id changes.

Accepted limits

Listed in the decision record. The main ones:

  • After a block or a password change on another device, the old key stays in that browser's profile until its end date ("401 → keep" from the issue).
  • With IndexedDB unavailable the checkbox still gives a 30-day session with no idle timeout.
  • gc_maxlifetime is 31 days for every session file.

Verification

  • Backend 836 tests, frontend 888, full e2e 63/63, linters, clean npm ci.
  • frontend/e2e/remember-browser.spec.ts: real crypto against the e2e stack, a persistent Chromium profile closed and launched again as the browser restart.
  • With curl on the e2e stack: Max-Age=2592000 on a remembered login, no re-sent cookie on /api/me, 401 for the pre-login session id.
  • Six code-review rounds; the last returned only points already under "Accepted limits".

Notes for review

  • New dev dependency: fake-indexeddb (Apache-2.0).
  • Two existing tests changed to match "a password change ends other sessions": AnketaControllerTest and password-change-drafts.spec.ts.

See docs/decisions/2026-10-04-remember-this-browser.md.

🤖 Generated with Claude Code

A checkbox on the login form, off by default. With it the session ends 30
days after the login (fixed, not an idle timeout) and the master key is kept
in IndexedDB under a non-extractable AES-GCM key, so a new tab or a
restarted browser opens a meeting without the password.

- AuthSession: remembered_until, a 30-day cookie, and a credential stamp
  that ends every other session of a user on a password change or reset.
  The login is written after the session id changes, and closeForReading()
  drops PHP's re-sent session cookie, which had no expiry.
- crypto/rememberedKey.ts: created only by a remembered login, replaced in
  place by a password change or "Unlock this tab", deleted by logout, by a
  login that isn't remembered, and when its end date passes.
- ensureUnlocked() tries the tab's key, then the remembered one, which it
  only reads.
- docs/encryption.md states what the stored key does and doesn't protect.

See docs/decisions/2026-10-04-remember-this-browser.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aleksejs1
aleksejs1 merged commit 43392f6 into main Oct 4, 2026
6 checks passed
@aleksejs1
aleksejs1 deleted the feat/remember-browser-205 branch October 4, 2026 04:08
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.

Opt-in "Remember this browser" (decision doc first)

1 participant