Skip to content

getUser() and refresh-token silent renew lose custom state and url_state #2457

Description

@MN755

Summary

src/User.ts drops state and url_state when the authenticated User is persisted, so the first storeUser() / _loadUser() round-trip strips those fields from later getUser() results and from refresh-token silent renew.

Repro

  1. Sign in with custom state, for example:
    await mgr.signinRedirect({
      state: { returnTo: /billing },
      url_state: tab=security,
    })
  2. Complete the callback and confirm the first User still has those values:
    const user = await mgr.signinCallback()
    user.state      // { returnTo: /billing }
    user.url_state  // tab=security
  3. Call await mgr.getUser() or let signinSilent() take the refresh-token path.
  4. The loaded user now has state === undefined and url_state === undefined.

Expected

Once a User has been stored, later getUser() calls and silent-renew updates should preserve the original state / url_state just like the initial callback user.

Actual

UserManager.storeUser() persists a JSON blob from User.toStorageString(), but that blob only contains tokens, profile, scope, and expiry. state and url_state are never written, so User.fromStorageString() reconstructs a user without them.

Current path:

  • src/User.ts:52-78
  • src/User.ts:109-120
  • src/UserManager.ts:145-155
  • src/UserManager.ts:319-330
  • src/UserManager.ts:787-804
  • src/RefreshState.ts:12-36

Likely Cause

User treats the callback payload as userState internally, but toStorageString() omits both state and url_state entirely.

That means:

  • getUser() always reloads a stripped user from storage, and
  • the refresh-token path builds RefreshState from that stripped user, so validateRefreshResponse() has no custom state left to copy back into the renewed response.

Source / Trigger

Any app that uses signinRedirect / signinPopup / signinSilent with custom state or url_state, then later calls getUser() or relies on the stored user for refresh-token silent renew.

Control Gap

The repo's own sample still demonstrates reading user.state after login in samples/Parcel/src/code-flow-duendesoftware/sample.js, but there is no regression coverage for the persisted-user path.

Impact

Post-login routing context, tenant/workspace hints, and other caller-owned state disappear after the first persistence round-trip. Apps that read the current user from storage instead of holding the callback return value in memory get inconsistent behavior, and silent renew can erase that context without an obvious error.

Fix Direction

Persist state and url_state in User.toStorageString() and restore them in User.fromStorageString() / new User(...).

A focused regression test around storeUser() + getUser() and another one for refresh-token silent renew would make this hard to re-break.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions