@W-22476773@ Storefront Preview / HttpOnly - dynamic SameSite for HttpOnly session cookies #3851
Conversation
|
Git2Gus App is installed but the |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Signed-off-by: vcua-mobify <47404250+vcua-mobify@users.noreply.github.com>
…efront-preview-samesite Signed-off-by: vcua-mobify <47404250+vcua-mobify@users.noreply.github.com>
…efront-preview-samesite
…efront-preview-samesite
| * `{sameSite: 'lax'}` (the existing top-level behavior). | ||
| * @private | ||
| */ | ||
| function getSiteAttrsForRequest(req) { |
There was a problem hiding this comment.
Edge case with multiple tabs: in Firefox and Safari Partitioned is ignored, so the __Host-pwakit_preview_ctx cookie becomes a plain SameSite=None; Secure; HttpOnly cookie under the storefront domain.
If the merchant opens a second tab with the storefront, the cookie will be carried and here getSiteAttrsForRequest here will flip the session cookies to SameSite=None; Partitioned even though the storefront in the new tab is not iframed.
Non blocking, cookie is the session scoped so it's more a browser compatibility note
There was a problem hiding this comment.
Yup, there's a slight degradation in Firefox / Safari as noted in the PR description but it should be fine as cookies are still Secure, HttpOnly
…ceCommerceCloud/pwa-kit into vc/http-only-storefront-preview
…efront-preview-samesite
The base branch was changed.
…front-preview-samesite

Summary
Completes the 2-part fix for cross-site iframe cookie loss in Storefront Preview. PR #3850 added the
__Secure-pwakit_preview_ctxmarker cookie writer (built onSec-Fetch-Dest/Sec-Fetch-Site/Refererbrowser-attested headers); this PR is the consumer that flips session-cookie attributes based on the marker.sameSite: 'lax'from every entry inSESSION_COOKIE_CONFIGso SameSite is resolved per-request.setHttpOnlySessionCookiesnow reads__Secure-pwakit_preview_ctxviareadStorefrontPreviewMarker(re-validates against the allow-list) and threads{sameSite: 'none', partitioned: true}into every Set-Cookie when the marker validates,{sameSite: 'lax'}otherwise.expireHttpOnlySessionCookies(logout path) does the same for deletion writes — Partitioned-cookie deletion is partition-keyed, so the deletion attributes must match the original write or the cookie won't actually clear. Logout also clears the marker cookie itself.clearStorefrontPreviewMarker(res, options)helper inpreview-context.jsso the marker writer and clearer live in the same module.After this PR lands, the storefront iframe under a trusted Runtime Admin preview parent gets session cookies that survive the cross-site boundary; SLAS refresh works, SCAPI calls return 200 instead of 400
access_token_cookie_missing, andgetUsidForPreviewresolves before the 10 srequestUsidtimeout.Why
With
MRT_ENABLE_HTTPONLY_SESSION_COOKIES=true, the BFF wrote session cookies (cc-at,cc-nx-g,cc-nx,idp_access_token,idp_refresh_token, plus indicator cookies) withSameSite=Laxhardcoded inhttponly-cookie-config.js. Storefront Preview iframes the storefront under a cross-site Runtime Admin parent (e.g.runtime-admin-preview.mobify-storefront.com). Browsers stripLaxcookies on the iframe's subresource fetches, so SLAS refresh fails, every SCAPI proxy call returns400 {"message":"access_token_cookie_missing"},getUsidForPreviewtimes out, and Runtime Admin reports "Failed to receive a response to 'requestUsid' within the 10000ms timeout." The same storefront URL opened in a top-level tab works fine — the bug is specific to the cross-site iframe boundary.#3850 captured the trust signal at the iframe document load (browser-attested
Sec-Fetch-Dest: iframe+Sec-Fetch-Site: cross-site+Refererorigin on the allow-list) into a server-only marker cookie. This PR consumes that signal on SLAS responses to make the actual attribute switch.MRT impact
None. Same as #3850: the change is entirely in
pwa-kit-runtimeSet-Cookie response headers and per-request resolution. CloudFront/MRT does not strip or rewrite the SameSite/Partitioned attributes, the cookies stay scoped to the storefront origin via the existingcookieDomainflow, and no new request headers are introduced. No WAF allow-list change required.Changes
pwa-kit-runtimepackages/pwa-kit-runtime/src/ssr/server/httponly-cookie-config.js— removedsameSite: 'lax'from every entry'sattributes.The remaining static attributes (
httpOnly,secure,path) stay frozen on the config. Updated the doc-comment to point readers atpreview-context.jsfor where SameSite/Partitioned are now resolved.packages/pwa-kit-runtime/src/ssr/server/process-token-response.jsreadStorefrontPreviewMarkerandclearStorefrontPreviewMarkerfrom./preview-context.DEFAULT_SITE_ATTRS = {sameSite: 'lax'}andPREVIEW_IFRAME_SITE_ATTRS = {sameSite: 'none', partitioned: true}— and agetSiteAttrsForRequest(req)helper that picks one based on whether the marker validates.makeAppendCookienow takes a thirdsiteAttrsargument and applies it to both the primary write and the host-scoped cleanup write. The cleanup-write change is load-bearing: Partitioned-cookie deletion is partition-keyed, so the deletion's attributes must match the original write or the cookie persists.setHttpOnlySessionCookiesandexpireHttpOnlySessionCookiescomputesiteAttrsonce per call and thread it through.expireHttpOnlySessionCookiesnow callsclearStorefrontPreviewMarker(res, options)after expiring the session cookies, so logout sweeps the iframe-context state too.packages/pwa-kit-runtime/src/ssr/server/preview-context.js— addsclearStorefrontPreviewMarker(res, options)mirroring the writer's attributes (Path, Secure, HttpOnly, SameSite=None, Partitioned, optional Domain). WhencookieDomainis configured, also emits a host-scoped deletion to clean up any pre-cookieDomainmarker.Tests
packages/pwa-kit-runtime/src/ssr/server/httponly-cookie-config.test.js— updated the legacy "sets secure, sameSite lax, andpath / on all cookies" assertion to "sets secure and path / on all cookies" and added a sibling assertion that no entry declares a static
sameSite(resolved per-request now).packages/pwa-kit-runtime/src/ssr/server/process-token-response.test.jsdescribe('trusted Storefront Preview iframe')block, 7 cases:SameSite=none; Partitionedwhen the marker validates.Secure,HttpOnly-where-applicable,Path,Expires) preserved under the iframe path.SameSite=Lax, noPartitioned.SameSite=Lax.expireHttpOnlySessionCookiesunder the iframe path emitsSameSite=None; Partitionedon every deletion (verifies partitioned-cookie clearing).Path=/; Secure; HttpOnly; SameSite=none; Partitioned; Expires=epoch-0.cookieDomainis set, two marker deletions are emitted (domain-scoped + host-scoped).'expires all session cookies for the given site'and'expireHttpOnlySessionCookies expires both domain-scoped and host-scoped versions') toaccount for the +1/+2 marker deletion.
Behavioral notes
SameSite=Lax, noPartitioned. The default path is exactly today's behavior.SameSite=None; Partitionedfor every session cookie on that response. CHIPS partitions by the parent's top-level site, so the cookies are isolated to the legitimate Runtime Admin preview origin's partition.Known limitation: multi-tab in non-CHIPS browsers
Browsers without
Partitionedsupport (Firefox/Safari today) ignore the partition attribute and store the marker as plainSameSite=None; Secure; HttpOnlyscoped to the storefront's eTLD+1. In a multi-tab scenario where the user has Storefront Preview open in Tab 1 (iframe under Runtime Admin) and then opens the storefront directly in Tab 2 (top-level navigation in the same browser), Tab 2's request carries the marker too. Any SLAS response on Tab 2 (e.g. token refresh) will then issue session cookies asSameSite=Noneinstead ofSameSite=Lax.Practical impact is small: cookies remain
Secure; HttpOnly; the BFF still enforces SLAS token validation and the SCAPI proxy guards. The CSRF margin fromLax → Nonenarrows slightly in those browsers, but state-changing endpoints don't auth purely from the cookie.In CHIPS-supporting browsers (Chrome/Edge) — the documented preview browsers — partitioning isolates the marker to the Runtime Admin partition and Tab 2 sees no marker, so it falls back to
SameSite=Laxexactly as expected. No fix at the cookie-jar layer is possible in non-CHIPS browsers without browser-level partition support; treating Chromium as the supported preview browser is the pragmatic stance.Test plan
npm testinpackages/pwa-kit-runtime— 678/678 passing (a flakyexpress.test.jscase unrelated to this PR sometimes drops to 677/678; passes on retry).npm run lintinpackages/pwa-kit-runtime— 0 errors (7 pre-existing warnings in files this PR doesn't touch).Manual smoke (replace once verified):
https://runtime-admin-preview.mobify-storefront.com. DevTools → Application → Cookies on the iframe origin: token cookies (cc-at_<siteId>,cc-nx-g_<siteId>/cc-nx_<siteId>,idp_refresh_token_<siteId>) all showSameSite=None,Partitioned,Secure,HttpOnly. Indicator cookies show the same minusHttpOnly. SCAPI XHRs return 200; noaccess_token_cookie_missing. The parent receives the USID viaMessageChannelreply within ~1 s.SameSite=Lax, noPartitioned. Marker is not present.https://example.com. Marker is not set (PR 1/2 gate); cookies stayLax.__Secure-pwakit_preview_ctx=https://attacker.example.comvia DevTools, trigger a SLAS refresh. BFF re-validates, falls back toLax. (Confirmed in unit tests;smoke optional.)
__Secure-pwakit_preview_ctx. Reload the iframe; marker re-issues from PR 1/2's middleware onthe document load.
cookieDomaininterop — setcommerceAPI.cookieDomain, trigger SLAS in the iframe; verify session cookies includeDomain=...; SameSite=None; Partitionedand the host-scoped duplicate. Signout; verify both versions of every cookie (including the marker) are expired.
SameSite=None; Secure); accept the multi-tab limitation noted above as known-and-documented.