feat(security): let a consumer supply the access token via setAccessTokenResolver - #324
feat(security): let a consumer supply the access token via setAccessTokenResolver#324gcutrini wants to merge 6 commits into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…okenResolver Add setAccessTokenResolver: when a resolver is registered, getAccessToken delegates to it; otherwise the built-in flow is unchanged. Passing a non-function resets to the built-in. The resolver is module-level state, so externalize security/methods in the webpack build so every uicore lib entry shares one instance and sees the registered resolver.
Delegates to a registered resolver, a later resolver replaces the previous one, and a non-function argument clears it.
670afb2 to
63e4c51
Compare
|
@gcutrini i cant approve this , this bypass entirely the renewal flow please provide a rationale for this |
|
@gcutrini following up on the earlier request for a rationale: the current PR description explains why the resolver has to live inside What's still missing is the actual concern raised: when
|
_fetch rejected its promise after already calling back with the error. The query* functions never await that promise, so the rejection only surfaced as an unhandled rejection. Return instead. Also guard the 404 branch so a missing response or callback cannot throw.
With a resolver registered, query-actions, getUserInfo and AttendanceTracker send the resolver's token, and a resolver failure in query-actions reaches the caller's callback without a request.
|
Note on the externals rule added here. security/methods.js imported the SET_LOGGED_USER string from ./actions, and actions.js imports ./methods back. That import cycle already existed on v4.x and was harmless: the methods bundle just carried its own inlined copy of actions and methods. This PR externalizes security/methods so every bundle shares one instance (needed for the resolver state). With that rule, the inlined actions copy inside the methods bundle resolved to lib/security/methods.js itself, so the file required itself. It still loaded (nothing reads it at load time), but the inlined copies would see an empty module, and a consumer bundling uicore from source with esbuild can fail to resolve it. So it had to be fixed here, not later. Fix: the action-type strings moved to security/constants.js; actions.js re-exports them, so existing imports keep working. reducers.js and utils/actions.js also read from constants now, so they stop inlining the actions module. No API change. |
…for global slot
The resolver moves from module state to
globalThis[Symbol.for('openstack-uicore-foundation.accessTokenResolver')],
so every copy of security/methods reads the same slot: bundles that inline
it, nested installs of the package, and symlinked dev installs. This drops
the webpack externals rule that made the module a cross-bundle singleton;
no bundle requires the package by its own name anymore, so yarn link
installs keep working and hosts no longer need a single hoisted copy.
976e221 to
4eaf3a7
Compare
ref: https://app.clickup.com/t/86bbnquuq
uicore's
getAccessTokenassumes the access token lives on the client. It reads and refreshes the token from local storage and serializes refreshes withnavigator.locks. That model does not fit a server-rendered host such as the Next.js event site, where the token is held in an encrypted httpOnly cookie and is never exposed to JavaScript. There is no client-side token for uicore to read, lock, or renew.Keeping the token out of JavaScript is also a deliberate security property. A token that never reaches the browser's JS context cannot be stolen by XSS or replayed from client code.
Several uicore modules call
getAccessToken, so the source must be injectable inside uicore rather than overridden by the consumer. Today a cookie-based host has to work around this by aliasingsecurity/methodsat build time and substituting its own implementation, which is invasive (build-system specific, replaces the whole module) and easy to drift out of sync.What
Add
setAccessTokenResolvertosecurity/methods: a consumer registers where the access token comes from, without replacing anything.getAccessToken. When a resolver is registered,getAccessTokendelegates to it; otherwise the built-in flow is unchanged. Passing a non-function resets to the built-in.setAccessTokenResolver.Renewal is the consumer's concern
When a resolver is registered, uicore delegates token retrieval to it and does not apply its own storage, locking, or renewal to the returned value. Freshness is left entirely to the consumer. A cookie-based host, for example, renews server-side (the server refreshes the session cookie via the refresh_token grant, and proxied API calls attach the real bearer from the cookie) and hands uicore only a "session present" placeholder, so there is nothing on the client to lock or renew.
Implementation note
The resolver lives on globalThis under a registered symbol:
globalThis[Symbol.for('openstack-uicore-foundation.accessTokenResolver')].setAccessTokenResolverwrites that slot;getAccessTokenreads it.Module state was not an option here. Every copy of
security/methodshas itsown module scope, and modern package managers routinely create several
copies of the package: pnpm materializes one per peer combination, widgets
can carry nested installs, and symlinked dev checkouts resolve to their own
tree. A resolver kept in module state is only visible to the copy that
registered it. The
Symbol.forregistry is shared by every copy on the page,so the singleton holds with no build-system help. React uses the same
pattern for its element symbols.
The webpack build is untouched: no bundle requires the package by its own
name, so
yarn linkinstalls keep working and hosts do not need a singlehoisted copy of the package.