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
- Sign in with custom state, for example:
await mgr.signinRedirect({
state: { returnTo: /billing },
url_state: tab=security,
})
- 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
- Call
await mgr.getUser() or let signinSilent() take the refresh-token path.
- 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.
Summary
src/User.tsdropsstateandurl_statewhen the authenticatedUseris persisted, so the firststoreUser()/_loadUser()round-trip strips those fields from latergetUser()results and from refresh-token silent renew.Repro
Userstill has those values:await mgr.getUser()or letsigninSilent()take the refresh-token path.state === undefinedandurl_state === undefined.Expected
Once a
Userhas been stored, latergetUser()calls and silent-renew updates should preserve the originalstate/url_statejust like the initial callback user.Actual
UserManager.storeUser()persists a JSON blob fromUser.toStorageString(), but that blob only contains tokens, profile, scope, and expiry.stateandurl_stateare never written, soUser.fromStorageString()reconstructs a user without them.Current path:
src/User.ts:52-78src/User.ts:109-120src/UserManager.ts:145-155src/UserManager.ts:319-330src/UserManager.ts:787-804src/RefreshState.ts:12-36Likely Cause
Usertreats the callback payload asuserStateinternally, buttoStorageString()omits bothstateandurl_stateentirely.That means:
getUser()always reloads a stripped user from storage, andRefreshStatefrom that stripped user, sovalidateRefreshResponse()has no custom state left to copy back into the renewed response.Source / Trigger
Any app that uses
signinRedirect/signinPopup/signinSilentwith customstateorurl_state, then later callsgetUser()or relies on the stored user for refresh-token silent renew.Control Gap
The repo's own sample still demonstrates reading
user.stateafter login insamples/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
stateandurl_stateinUser.toStorageString()and restore them inUser.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.