Skip to content

@ W-21066921 @W-21201637: Add HttpOnly session cookies for SLAS private client proxy#3680

Merged
unandyala merged 15 commits intofeature/httponly-session-cookiesfrom
unandyala.httponly-cookies-private-client
Feb 23, 2026
Merged

@ W-21066921 @W-21201637: Add HttpOnly session cookies for SLAS private client proxy#3680
unandyala merged 15 commits intofeature/httponly-session-cookiesfrom
unandyala.httponly-cookies-private-client

Conversation

@unandyala
Copy link
Contributor

@unandyala unandyala commented Feb 19, 2026

Summary

  • When MRT_DISABLE_HTTPONLY_SESSION_COOKIES is 'false', SLAS token responses are intercepted by the proxy: access_token, refresh_token, and idp_access_token are set as HttpOnly cookies (with siteId suffix) and stripped from the response body
  • Client receives access_token_expires_at in the response body for expiry checks without needing the JWT
  • Proxy reads access token from HttpOnly cookies and sets Authorization: Bearer header for SCAPI requests automatically via applyProxyRequestAuthHeader()
  • New useHttpOnlySessionCookies flag on Auth and CommerceApiProvider controls client-side behavior: skips storing tokens in localStorage, uses access_token_expires_at for expiry checks, and ensures fetch credentials allow cookies
  • Configurable tokenResponseEndpoints and slasEndpointsRequiringAccessToken regexes allow customization in ssr.js

Follow-up work

  • TAOB (Trusted Agent on Behalf) flow with HttpOnly cookies
  • Refresh token flow with HttpOnly cookies (server-side proxy extraction of refresh token from cookie)
  • Support for SLAS public client
  • Handle SLAS response with cookies on the server

Test plan

  • Verify HttpOnly cookies are set on token responses when MRT_DISABLE_HTTPONLY_SESSION_COOKIES=false
  • Verify tokens are stripped from response body and access_token_expires_at is present
  • Verify SCAPI proxy requests include Authorization: Bearer header from cookie
httponly-cookies.mov

How To Test

  1. Checkout the branch
  2. Enable slas private client
  • Set useSLASPrivateClient: true in ssr.js
  • Set enablePWAKitPrivateClient={true} in _app-config/index.jsx on the CommerceApiProvider
  • Update clientId in config/default.js to your SLAS private client ID
  • Set the PWA_KIT_SLAS_CLIENT_SECRET environment variable
  1. Set disableHttpOnlySessionCookies: false under ssrParameters in config/default.js
  2. run `npm ci'
  3. cd packages/template-retail-react-app
  4. npm start

🤖 Generated with Claude Code

When MRT_DISABLE_HTTPONLY_SESSION_COOKIES is 'false', token responses from
SLAS are intercepted: access_token, refresh_token, and idp_access_token are
set as HttpOnly cookies and stripped from the response body. The client
receives access_token_expires_at for expiry checks without needing the JWT.

Server-side (pwa-kit-runtime):
- applyHttpOnlySessionCookies() intercepts token responses, sets HttpOnly
  cookies with siteId suffix, and strips tokens from body
- applyProxyRequestAuthHeader() reads access token from HttpOnly cookie and
  sets Authorization header for SCAPI proxy requests
- isScapiDomain() utility for identifying Commerce API domains
- Configurable tokenResponseEndpoints and slasEndpointsRequiringAccessToken
  regexes for controlling which endpoints are processed

Client-side (commerce-sdk-react):
- useHttpOnlySessionCookies flag on Auth and CommerceApiProvider
- isAccessTokenExpired() uses access_token_expires_at when HttpOnly enabled
- handleTokenResponse() skips storing tokens in localStorage when HttpOnly
- Provider ensures fetch credentials allow cookies to be sent

Note: TAOB (Trusted Agent on Behalf) and refresh token flows with HttpOnly
cookies will be handled in follow-up work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@unandyala unandyala requested a review from a team as a code owner February 19, 2026 22:54
@cc-prodsec
Copy link
Collaborator

cc-prodsec commented Feb 19, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

if (expiresAt == null || expiresAt === '') return true
const expiresAtSec = Number(expiresAt)
if (Number.isNaN(expiresAtSec)) return true
const bufferSeconds = 60
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of bufferSeconds is to avoid race conditions where a token is technically still valid when the check happens, but expires by the time the SCAPI request reaches the server. By refreshing 60 seconds early, you ensure the token is still valid when the downstream API processes the request.

The original code has the same pattern

@@ -1,3 +1,6 @@
## [Unreleased]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just to avoid merge conflict when we merge in to develop branch

ssrFunctionNodeVersion: '24.x',
// Store the session cookies as HttpOnly for enhanced security.
disableHttpOnlySessionCookies: false,
disableHttpOnlySessionCookies: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will enable httponly cookies once we add the implementation for slas public client

isTokenEndpoint
) {
try {
workingBuffer = applyHttpOnlySessionCookies(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

running applyHttpOnlySessionCookies before onSLASPrivateProxyRes

  1. ensures tokens are secured in HttpOnly cookies before any custom code runs.
  2. applyHttpOnlySessionCookies is guaranteed to have unmodified slas response

@unandyala unandyala requested a review from clavery February 20, 2026 04:21
@unandyala unandyala changed the title Add HttpOnly session cookies for SLAS private client proxy @ W-21066921 @W-21201637: Add HttpOnly session cookies for SLAS private client proxy Feb 20, 2026

// Refresh token cookie TTL defaults (seconds). Must stay in sync with commerce-sdk-react auth constants.
const DEFAULT_SLAS_REFRESH_TOKEN_GUEST_TTL = 30 * 24 * 60 * 60
const DEFAULT_SLAS_REFRESH_TOKEN_REGISTERED_TTL = 90 * 24 * 60 * 60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's separate these auth-specific functions into a separate file since build-remote-server.js is quite a large file that does too many things already.

}

// IDP access token
const idpAccessToken = parsed.idp_access_token
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should break this function down into a couple of helpers to aid with readability like applyHttpOnlyAccessToken, applyHttpOnlyIdpToken, etc.

// This will be used to read the correct access token cookie
try {
const config = getConfig({buildDirectory: options.buildDir})
options.siteId = config?.app?.commerceAPI?.parameters?.siteId || null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in a multi-site scenario where a shopper changes sites?

In the app, we determine site id using the logic in AppConfig.restore user has the ability to change it. But here, I think this is set once from the config on server startup so I don't know how this works with multi-site?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. I will create a followup ticket to look into multi-site

* @param targetHost {String} the target hostname (host+port)
* @param slasEndpointsRequiringAccessToken {RegExp} regex for SLAS auth endpoints that need Bearer token
*/
export const applyProxyRequestAuthHeader = ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a function specifically for SCAPI / SLAS calls. We might be able to get away with a more explicit name like applySCAPIAuthHeaders and only go into this function if isSCAPIDomain is true

caching
caching,
siteId = null,
slasEndpointsRequiringAccessToken = /\/oauth2\/logout/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're hard coding /\/oauth2\/logout/ so we might want this in a constants file in case we want to update this default so we only have to change this in 1 place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new SLAS_ENDPOINTS_REQUIRING_ACCESS_TOKEN constant, could we set this as the following?

Suggested change
slasEndpointsRequiringAccessToken = /\/oauth2\/logout/
slasEndpointsRequiringAccessToken = SLAS_ENDPOINTS_REQUIRING_ACCESS_TOKEN

storageType: 'local',
key: 'uido'
},
'cc-at-expires': {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We must validate if cc-at-expires cookies is also set by Hybrid Auth in ECOM whenever a token is generated by hybrid auth.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reminding. We had a discussion with ECOM and we are aligned on the cookies

const configLogger = logger || console

// When HttpOnly cookies are enabled, ensure fetch credentials allow cookies to be sent.
const effectiveFetchOptions =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Consider wrapping this in a useMemo to avoid creating a new object reference on every render.

Copy link
Contributor

@shethj shethj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some non-blocking comments.
Thanks for the great work! :D

Copy link
Contributor

@vcua-mobify vcua-mobify left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a non-blocking comment. Yes, let's follow up on multi-site in a follow up PR.

Thanks @unandyala !

caching
caching,
siteId = null,
slasEndpointsRequiringAccessToken = /\/oauth2\/logout/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new SLAS_ENDPOINTS_REQUIRING_ACCESS_TOKEN constant, could we set this as the following?

Suggested change
slasEndpointsRequiringAccessToken = /\/oauth2\/logout/
slasEndpointsRequiringAccessToken = SLAS_ENDPOINTS_REQUIRING_ACCESS_TOKEN

appHostname,
appProtocol,
siteId = null,
slasEndpointsRequiringAccessToken = /\/oauth2\/logout/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here as well

@unandyala unandyala merged commit 26a2ede into feature/httponly-session-cookies Feb 23, 2026
42 checks passed
@unandyala unandyala deleted the unandyala.httponly-cookies-private-client branch February 23, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants