Skip to content

fix: harden B1/B2 auth-cache security - #29

Open
bluewandd wants to merge 2 commits into
aervxa:mainfrom
bluewandd:pr/lepse-auth-cache-b1-b2
Open

fix: harden B1/B2 auth-cache security#29
bluewandd wants to merge 2 commits into
aervxa:mainfrom
bluewandd:pr/lepse-auth-cache-b1-b2

Conversation

@bluewandd

Copy link
Copy Markdown

Summary

  • Namespace account-derived query data by the authenticated account.
  • Clear, persist, and synchronize cache state safely across account transitions and browser contexts.
  • Prevent stale requests and legacy persisted entries from repopulating another account's cache.
  • Add focused auth-cache security tests.

This is PR 2 of a three-PR sequence. Its head is commit 3922fc6579bb14c6000a4b10d50f6c54490f3838 and includes PR 1 by design.

Merge the email-verification PR first. After that merge, this PR's remaining change is the B1/B2 auth-cache hardening commit.

@bluewandd

Copy link
Copy Markdown
Author

Context for reviewers

This is the second PR in the three-PR sequence. It carries the email-verification synchronization from PR 27 and adds the B1/B2 auth-cache hardening needed to keep account data isolated across login, logout, reload, errors, and browser contexts.

The risks this PR addresses

Lachesis persists TanStack Query data for up to seven days. Before this change, account-derived queries used global keys and one shared persisted cache. That creates several concrete failure paths:

  • Account A logs in, loads tasks and goals, logs out, and account B logs in on the same client. A's cached data can remain available while B's session starts.
  • A persisted cache can be hydrated before the current account has been validated. Offline startup or a failed profile request can leave old account data visible.
  • A delayed write from before logout can finish after the account transition and put A's data back into storage.
  • A delayed mutation response from A can arrive after B is active and write A's result into the current UI.
  • Multiple mounted observers can retain the previous account's last result even after a query is removed.
  • Separate cookie refs in composables, the API plugin, and the startup plugin can observe token changes at different times. A profile request can then carry an old or missing bearer during a login transition.

These are account-isolation and session-lifecycle risks. The goal is to make the current bearer and the current account cache move together throughout the actual Lachesis call graph.

What the change does

1. Namespaces account data without storing the bearer

Account-derived query keys receive a deterministic scope derived from the bearer. The bearer itself never enters a query key, persisted value, localStorage event, or BroadcastChannel message. The scope is only a cache namespace; a successful profile response remains the authority that validates the current identity.

The account query roots include profile, day/session, tasks, goals, and the other user-owned data paths used by Lachesis. Public queries keep their existing shared behavior.

For persistence, only public data and data belonging to the current non-anonymous scope are retained. Legacy unscoped account entries are discarded because their owner cannot be proven. Mutation records are not persisted. Writes are serialized and invalidated across account transitions so an older queued write cannot repopulate the cache after logout.

2. Clears active account state at the transition boundary

When the token is cleared or an account transition begins, the lifecycle first resets account queries so mounted observers receive empty results, then removes those queries from the QueryClient and persists the cleaned cache. This covers profile, task, goal, and session observers rather than relying on one profile observer.

Mutation callbacks capture the account scope at onMutate. A response is allowed to update the cache only while that scope is still current. For example, if A edits a task, logs out, and B logs in before the response returns, A's response cannot update B's task list.

3. Gives every caller one auth source

The plugin creates one shared authToken ref and provides it to the API client, composables, and startup auth plugin. The request hook reads that same ref when it sets the Authorization header.

Login and signup publish the new token before the reactive profile query can run. The first profile request therefore reads the new bearer. Logout clears visible account state while retaining the old bearer long enough for the logout request, then clears the token on settlement even when the server is offline or has already revoked it.

Unauthorized responses are tied to the bearer that was actually sent. A late 401 from an old request cannot clear a newer login. Login, signup, and password-reset-request routes are excluded from the session-revocation path.

4. Synchronizes supported browser contexts without broadcasting credentials

A token-set or token-cleared event is sent through BroadcastChannel and the storage event path. The event contains an event ID, event type, and scope only. A sibling context clears its account state and refreshes the one shared cookie source; it never receives the bearer through browser messaging.

This handles cases such as:

  • logging out in one tab while another tab is showing the account;
  • logging into one tab while a sibling tab still has the previous account state;
  • preventing a sibling context from continuing to render an account whose cookie transition it cannot yet validate.

Review examples

The most important end-to-end checks are:

  1. A to logout to B in one tab

    • Load account A's profile, tasks, and goals.
    • Log out.
    • Log in as account B.
    • Confirm A's data disappears immediately and B's data arrives only under B's validated scope.
  2. Persisted cache on reload

    • Let account A populate the cache.
    • End the session and start account B, including an offline or failed-profile case.
    • Confirm old account data is not rendered while B's identity is unvalidated.
  3. Multiple observers

    • Mount more than one account-aware observer.
    • Clear the token.
    • Confirm every observer receives an empty result and no observer can retain the prior profile on screen.
  4. First request after login

    • Complete login.
    • Inspect the first profile request.
    • Confirm its Authorization header carries the new bearer, with no request racing ahead under the old value.
  5. Late response from the previous account

    • Start an account-A mutation.
    • Transition to account B before it settles.
    • Confirm the late callback is ignored for B's cache.
  6. Two browser contexts

    • Open the same account in two tabs.
    • Log out or log in in one tab.
    • Confirm the other tab clears stale account state and never receives the bearer through the cross-context event.

The focused Lachesis test command is:

npm test --workspace=@lepse/lachesis

The tests cover scoped keys, persisted-cache filtering, queued-write invalidation, A-to-B transitions, token-aware request headers, mounted observers, unauthorized responses, and cross-context events. The release workflow currently builds Lachesis but does not run the frontend tests, so the focused suite remains a local/manual check.

Scope and sequence

This PR changes the Lachesis frontend auth/cache lifecycle only. Clotho routes, the database, migrations, mail delivery, bearer format, and server authorization remain unchanged. No dependency or lockfile change is required.

PR 29 depends on PR 27. PR 28 contains both earlier PRs plus the independent timer correction, so the intended landing order is:

  1. feat: add email verification #27
  2. fix: harden B1/B2 auth-cache security #29
  3. fix: use colon separator in Slice 2 timer #28

The key review question is whether every account-owned read, write, persistence path, and auth transition follows the same scope and bearer lifecycle, including the failure paths.

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.

2 participants