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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions