Skip to content

_updateStore() can persist session without authenticator key, causing silent logout on next restore #3083

@lifeart

Description

@lifeart

Bug

_updateStore() in internal-session.js only includes the authenticator key in persisted data when this.authenticator is non-empty:

_updateStore() {
    let data = this.content;
    if (!isEmpty(this.authenticator)) {
      set(data, 'authenticated', Object.assign(
        { authenticator: this.authenticator },
        data.authenticated || {}
      ));
    }
    return this.store.persist(data);
}

If this.authenticator is temporarily null due to a race condition (e.g., _clear() runs concurrently with a token refresh that triggers _setup()_updateStore()), the session is persisted without the authenticator key:

{"authenticated": {"access_token": "...", "refresh_token": "..."}}

On the next page load, restore() reads this data, finds no authenticator key, and immediately clears the session — silently logging the user out with zero network requests.

Reproduction

  1. Have an authenticated session with a valid refresh token
  2. Trigger a race condition where _clear() nulls this.authenticator while _updateStore() is about to persist (e.g., cross-tab logout during a scheduled token refresh)
  3. Close the tab
  4. Reopen the app — the user is immediately logged out without any token refresh attempt

Root cause

restore() treats missing authenticator key as "no session":

let { authenticator: authenticatorFactory } = restoredContent.authenticated || {};
if (authenticatorFactory) {
    // ... restore session
} else {
    // clear session ← THIS CLEARS VALID TOKENS
}

Suggested fix

Either:

  1. Prevent _updateStore() from persisting when isAuthenticated is true but authenticator is null — refuse to write corrupt state:

    _updateStore() {
        if (this.isAuthenticated && isEmpty(this.authenticator)) {
            return Promise.resolve(); // skip persist
        }
        // ... existing logic
    }
  2. Or preserve the last-known authenticator name so it's always available for persist, even if this.authenticator is transiently null.

Versions

  • Verified the bug exists in v1.9.2 and the current master (v8.3.0) — the _updateStore() logic is identical.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions