Skip to content

feat: Share one session across tabs (M2-11052,M2-11002) - #2251

Open
sricharan-varanasi wants to merge 41 commits into
developfrom
session-sync-local
Open

feat: Share one session across tabs (M2-11052,M2-11002)#2251
sricharan-varanasi wants to merge 41 commits into
developfrom
session-sync-local

Conversation

@sricharan-varanasi

@sricharan-varanasi sricharan-varanasi commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

📝 Description

🔗 Jira Ticket M2-11052
🔗 Jira Ticket M2-11002

#2249 moved tokens into one shared, encrypted slot. But react-secure-storage reads from a snapshot taken when the page loads, so a tab still can't see what another tab wrote.

That leaves three problems:

  • Tab A rotates the token, tab B keeps using the old one, and the retry revokes the family for both
  • Tab A logs out, tab B keeps showing a signed-in UI over a dead session
  • A tab sitting on the login page doesn't notice a sign-in elsewhere

Tabs now talk over a BroadcastChannel.

Changes include:

  • Rotated tokens, logout, and session hand-off sync across tabs
  • A tab on the login page joins a session started elsewhere, or reloads into one if no other tab is left
  • A frozen tab catches up on focus, or tears down if the session ended while it slept
  • An idle logout no longer gets caught by the builder's unsaved-changes prompt

Changes include:

  • A modal at 25 minutes idle, counting down the last 5 before logout
  • Stay logged in keeps the session; Log out ends it immediately. Closing the modal keeps the session
  • Activity pauses while the modal is open, so moving the mouse toward the buttons cannot answer it for you
  • The countdown length is configurable via REACT_APP_IDLE_WARNING_MIN, capped at half the idle timeout
  • Answering in one tab closes the modal in the others

The main bug - a background tab logging out the tab you're actually using - is fixed by one line. The idle clock is shared now, so the timer just re-reads it instead of firing blind.

🪤 Peer Testing

Needs enableSessionKeepAlive on locally, REACT_APP_IDLE_TIMEOUT_MIN=3, REACT_APP_REFRESH_LEAD_SEC=20.

Use chrome://discards (or edge://discards) → Freeze for the frozen-tab steps.

  • Two tabs. Work in tab A past the idle timeout, leave tab B in the background

    Expected outcome: neither logs out

  • Two tabs. Wait for a rotation, then click something in tab B

    Expected outcome: one refresh call across the browser, and tab B doesn't 401

  • Log out in tab A

    Expected outcome: tab B goes to the login page straight away

  • Edit something in the builder, then idle out

    Expected outcome: soft-lock login, no unsaved-changes modal

  • Edit something in the builder, then log out from the account menu

    Expected outcome: the save/discard prompt still appears

  • Freeze a tab, rotate a few times elsewhere, then focus it

    Expected outcome: it catches up, no 401

  • Freeze tab B, log out in tab A, focus tab B

    Expected outcome: tab B tears down on focus

  • Leave tab A on the login page, sign in on tab B

    Expected outcome: tab A moves to the dashboard. Close tab B first and tab A reloads once instead

  • Flag off

    Expected outcome: no channel traffic, no idle logout, same as feat: Move tokens to local storage (M2-11055) #2249 alone

✏️ Notes

✅ Checklist

Functionality

  • The feature behaves correctly in practice and fulfills the intended business purpose
  • The implementation accounts for edge cases, avoids subtle logical errors, and handles somewhat rare failure states (e.g. offline mode for mobile, 3rd party being down, etc)

Testing

  • Verify there are automated tests added that meaningfully cover critical behavior and failure cases
  • Code coverage does not go down as result of this change
  • Test suite passes

Security & Data Privacy

  • Verify there is no chance we would accidentally log PII to application logs
  • Verify this addition does not materially affect our security attack surface, and if so it has undergone security review
  • All inputs are sanitized
  • New dependencies are well maintained, have significant justification for being added to the project, and are documented in the Curious open source credit page - none added

Logging/Monitoring

  • Logging is implemented for this change such that you could troubleshoot this feature in production — none added
  • The change/feature is able to be monitored in production — none added

Performance

  • This change does not introduce n+1 queries or other performance issues within our expected scale (e.g. missing indexes on frequently queried columns, frequently updating tables that are accessed often)

Readability

  • All commented out code is removed
  • Debugging code including extraneous log lines are removed
  • Code is easy to understand through naming and structure; comments explain intent or non‑obvious decisions

Change Safety

  • Backend changes are backwards compatible with old clients, or it is well known they are not and a deployment/rollout plan is in place. This include backend changes being compatible with old mobile app versions, as well as applet versioning within Curious.
  • Destructive database migrations are rolled out in stages. For example, renaming a column means adding a new column and migrating the existing data to that columns in one deployment. Then monitoring to ensure that field isn't used, and finally removing that old column in a separate deployment.

@sricharan-varanasi
sricharan-varanasi changed the base branch from session-storage-local to develop August 18, 2026 03:21
@aws-amplify-us-east-1

Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-2251.d2ccder08v9rmu.amplifyapp.com

@divbzero divbzero 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.

@sricharan-varanasi This is big set of changes and looks really good overall. Just a few comments: 3 that I think could tighten the security, and 2 ACTIVITY_EVENTS comments that could slightly reduce how often the event handler fires.

if (Date.now() - lastActivityAt < idleTimeoutMs) return;

authStorage.clear();
clearSessionState();

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.

[Important] Also clear sessionStorage like we do in Auth.reducer.ts?

sessionStorage.clear();

@sricharan-varanasi sricharan-varanasi Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you, @divbzero . Fixed in 7c31d79 . Added sessionStorage.clear() after the token clear, matching the teardown in Auth.reducer.ts.


const { idleTimeoutMs } = resolveSessionConfig();
if (Date.now() - lastActivityAt < idleTimeoutMs) return;

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.

[Important] Also invalidate tokens on the server like we do in useLogout.ts?

await deleteAccessTokenApi();
await deleteRefreshTokenApi();

@sricharan-varanasi sricharan-varanasi Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 46a02ca . It now calls deleteRefreshTokenApi() before clearing, guarded on the
refresh token's own exp so it only fires while the server would still accept it.

clearStaleSession now calls deleteRefreshTokenApi() before clearing local storage, so the
server ends the session too. logout2 revokes the whole token family, so the access token goes
with it - one call covers both.

The call is guarded on the refresh token's exp, so we only ask the server when the token is
actually still valid. If it has already expired there's nothing to revoke and we skip it.

export const ACTIVITY_THROTTLE_MS = 5 * MS_IN_SEC;

export const ACTIVITY_EVENTS = [
'mousemove',

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.

s/mousemove/pointermove/? pointermove should capture movement from mouse, touch, or pen.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 46a6f63 . Switched to pointermove, which covers mouse, touch and pen in one.

'keydown',
'scroll',
'wheel',
'touchstart',

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.

Remove touchstart? touchstart should be captured by pointerdown.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 46a6f63 . Dropped both touchstart and mousedown for pointerdown.

if (!accessToken || !refreshToken) {
throw new Error('Access token refresh failed.');
}

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.

[Important] Do we need a check that the user hasn’t logged out while awaiting the refreshed token?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you @divbzero . Fixed this gap in 3713f85 . The refresh now checks the store still
holds a refresh token before saving the new pair, and throws if it doesn't.

@divbzero divbzero 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.

The latest 9 commits look solid 👍 just one typo from the design. Thank you @sricharan-varanasi!

Comment thread src/resources/app-en.json Outdated
"sept": "September",
"serverConfiguration": "Server Configuration",
"serverStatusConfigured": "Server Status: Connected.",
"sessionTimeoutDescription": "You haven't done anything in this window for awhile. If you don't make a choice, we'll log you out in {{countdown}}.",

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.

s/awhile/a while/ — I think this was a typo in the design.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cbef683 . Corrected in both app-en.json .

Comment thread src/shared/hooks/useSessionKeepAlive/SessionTimeoutModal.tsx
Comment thread .env.example
@sricharan-varanasi sricharan-varanasi changed the title feat: Share one session across tabs (M2-11052) feat: Share one session across tabs (M2-11052,M2-11002) Aug 22, 2026
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