@salesforce/commerce-sdk-react 5.4.0-preview.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -1
- package/auth/index.d.ts +1 -0
- package/auth/index.js +18 -2
- package/hooks/useTrustedAgent.js +9 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
## v5.4.0
|
|
1
|
+
## v5.4.0 (Aug 12, 2026)
|
|
2
|
+
- [Bugfix] Fix Trusted Agent (Order on Behalf) login hanging on a blank popup because `authorizeTrustedAgent` never sent an OAuth `state`. The storefront recognises the trusted agent callback by the presence of `state`: `request-processor.js` only keeps `code` on `/callback` (and `ssr.js` only serves that variant `no-store` and renders it) when `state` is set, and the callback page only hands `code`+`state` back to the opener when both are present. Without a `state` on the authorize request, SLAS redirected the popup to `/callback` with `code` only, the code was stripped as a standard-login redirect, an empty cacheable body was served, and the popup stayed blank with the login spinner never resolving. `authorizeTrustedAgent` now generates a CSRF `state` (a nonce distinct from the PKCE code verifier), sends it on the authorize request (SLAS echoes it back on the redirect), and returns it; `useTrustedAgent` compares the popup-echoed `state` against the one it minted before exchanging the code, and SLAS additionally binds `state`↔`code` on the token request.
|
|
2
3
|
- [Bugfix] Fix Trusted Agent (Order on Behalf) login failing when the storefront sends a `Cross-Origin-Opener-Policy: same-origin` header. That header severs the authentication popup so the opener can no longer read the popup location and `popup.closed` wrongly reports `true`, which made the flow reject with "Popup closed without authenticating." before the agent finished. `useTrustedAgent` now receives the result out of band via `postMessage` from the same origin callback page with a `BroadcastChannel` fallback, and it no longer treats a severed `popup.closed` as user cancellation (a genuinely abandoned popup is still caught by the existing timeout). Exposes a `useTrustedAgentPopupCallback` hook for the same origin callback page to hand the OAuth result back to the opener. The hook owns the message contract and the delivery. After delivering the result the callback page also closes itself, since the opener's `popup.close()` is unreliable through the COOP-severed window reference. Additive to the public API. Adds `TRUSTED_AGENT_RUNBOOK.md` documenting the callback contract, the upgrade steps, and how to reproduce and verify the flow.
|
|
3
4
|
- [Bugfix] Gracefully handle stale or malformed session tokens on load. A truncated `cc-at` cookie chunk or a value left by an older token format could be handed to `jwt-decode`, surfacing an `Invalid token specified: missing part #2` error to the storefront during `ready()`. The auth module now decodes such tokens defensively: an undecodable access token is discarded (and its cookie cleared) and treated as expired, an undecodable SFRA `cc-at` handoff token is cleared with a fallback to the local store, and a malformed `fetchedToken` is ignored — in every case the flow falls through to a refresh / guest login instead of throwing. Only affects non-HttpOnly / SSR / hybrid mode.
|
|
4
5
|
|
package/auth/index.d.ts
CHANGED
package/auth/index.js
CHANGED
|
@@ -1181,10 +1181,26 @@ class Auth {
|
|
|
1181
1181
|
const loginId = credentials.loginId || 'guest';
|
|
1182
1182
|
const isGuest = loginId === 'guest';
|
|
1183
1183
|
const idpOrigin = isGuest ? 'slas' : 'ecom';
|
|
1184
|
-
|
|
1184
|
+
// CSRF `state` for the OAuth authorize request. SLAS echoes it back on the
|
|
1185
|
+
// final redirect to `/callback`, and the storefront relies on its presence:
|
|
1186
|
+
// request-processor keeps `code` (and serves that variant no-store) only when
|
|
1187
|
+
// `state` is set, and the callback page hands `code`+`state` back to the opener
|
|
1188
|
+
// to finish login. Without it the redirect lands with `code` only, the code is
|
|
1189
|
+
// stripped as a standard-login redirect, and the popup hangs. So it must be sent
|
|
1190
|
+
// on every trusted agent authorize request.
|
|
1191
|
+
//
|
|
1192
|
+
// NOTE: this is a distinct nonce from the PKCE `codeVerifier` above and must NOT
|
|
1193
|
+
// be collapsed into it. `createCodeVerifier` is reused only as a random
|
|
1194
|
+
// high-entropy string generator; the two values serve different security roles
|
|
1195
|
+
// (PKCE proof-of-possession vs. OAuth CSRF `state`) and are compared/validated on
|
|
1196
|
+
// separate requests. The caller re-verifies the echoed `state` matches this value
|
|
1197
|
+
// before exchanging the code, and SLAS also binds `state`↔`code` on the token request.
|
|
1198
|
+
const state = _commerceSdkIsomorphic.helpers.createCodeVerifier();
|
|
1199
|
+
const url = `${slasClient.clientConfig.proxy || ''}/shopper/auth/v1/organizations/${organizationId}/oauth2/trusted-agent/authorize?${[...[`client_id=${clientId}`, `channel_id=${siteId}`, `login_id=${loginId}`, `redirect_uri=${_this9.redirectURI}`, `idp_origin=${idpOrigin}`, `response_type=code`, `state=${state}`], ...(!_this9.clientSecret ? [`code_challenge=${codeChallenge}`] : [])].join('&')}`;
|
|
1185
1200
|
return {
|
|
1186
1201
|
url,
|
|
1187
|
-
codeVerifier
|
|
1202
|
+
codeVerifier,
|
|
1203
|
+
state
|
|
1188
1204
|
};
|
|
1189
1205
|
})();
|
|
1190
1206
|
}
|
package/hooks/useTrustedAgent.js
CHANGED
|
@@ -357,7 +357,8 @@ const useTrustedAgent = () => {
|
|
|
357
357
|
}
|
|
358
358
|
const {
|
|
359
359
|
url,
|
|
360
|
-
codeVerifier
|
|
360
|
+
codeVerifier,
|
|
361
|
+
state: expectedState
|
|
361
362
|
} = yield authorizeTrustedAgent.mutateAsync({
|
|
362
363
|
loginId
|
|
363
364
|
});
|
|
@@ -365,6 +366,13 @@ const useTrustedAgent = () => {
|
|
|
365
366
|
code,
|
|
366
367
|
state
|
|
367
368
|
} = yield createTrustedAgentPopup(url, refresh);
|
|
369
|
+
// CSRF check: the `state` echoed back through the popup must match the one we
|
|
370
|
+
// minted for this authorize request. SLAS also binds `state`↔`code` on the
|
|
371
|
+
// token request below, but comparing here fails fast and stops a mismatched
|
|
372
|
+
// code from ever being exchanged.
|
|
373
|
+
if (state !== expectedState) {
|
|
374
|
+
throw new Error('Trusted agent login failed: state mismatch on authentication callback.');
|
|
375
|
+
}
|
|
368
376
|
return yield loginTrustedAgent.mutateAsync({
|
|
369
377
|
loginId,
|
|
370
378
|
code,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/commerce-sdk-react",
|
|
3
|
-
"version": "5.4.0
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "A library that provides react hooks for fetching data from Commerce Cloud",
|
|
5
5
|
"homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/ecom-react-hooks#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"jwt-decode": "^4.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@salesforce/pwa-kit-dev": "3.20.0
|
|
49
|
+
"@salesforce/pwa-kit-dev": "3.20.0",
|
|
50
50
|
"@tanstack/react-query": "^4.28.0",
|
|
51
51
|
"@testing-library/jest-dom": "^5.16.5",
|
|
52
52
|
"@testing-library/react": "^14.0.0",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@types/react-helmet": "~6.1.6",
|
|
62
62
|
"@types/react-router-dom": "~5.3.3",
|
|
63
63
|
"cross-env": "^5.2.1",
|
|
64
|
-
"internal-lib-build": "3.20.0
|
|
64
|
+
"internal-lib-build": "3.20.0",
|
|
65
65
|
"jsonwebtoken": "^9.0.0",
|
|
66
66
|
"nock": "^13.3.0",
|
|
67
67
|
"nodemon": "^2.0.22",
|
|
@@ -98,5 +98,5 @@
|
|
|
98
98
|
"publishConfig": {
|
|
99
99
|
"directory": "dist"
|
|
100
100
|
},
|
|
101
|
-
"gitHead": "
|
|
101
|
+
"gitHead": "bb0a49284fed371ae582ced1268991a843f33be4"
|
|
102
102
|
}
|