Skip to content

Commit 9ee940f

Browse files
unandyalaclaude
andcommitted
Add Bearer token and refresh token injection for SLAS private client logout
When HttpOnly session cookies are enabled, the shopper's access token and refresh token are stored in HttpOnly cookies and are not accessible to client-side JavaScript. The SLAS /oauth2/logout endpoint requires both a Bearer token in the Authorization header and a refresh_token query parameter. This change injects both from HttpOnly cookies in the SLAS private client proxy. Also moves SLAS-specific Bearer token logic out of configure-proxy.js (regular /mobify/proxy path) into the SLAS private client proxy where it belongs, since SLAS calls don't go through the regular proxy when useSLASPrivateClient is enabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c1fcb8c commit 9ee940f

File tree

4 files changed

+57
-80
lines changed

4 files changed

+57
-80
lines changed

packages/pwa-kit-runtime/src/ssr/server/build-remote-server.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import path from 'path'
8+
import cookie from 'cookie'
89
import {
910
BUILD,
1011
CONTENT_TYPE,
@@ -399,13 +400,7 @@ export const RemoteServerFactory = {
399400
*/
400401
_configureProxyConfigs(options) {
401402
const siteId = options.siteId || null
402-
const slasEndpointsRequiringAccessToken = options.slasEndpointsRequiringAccessToken
403-
configureProxyConfigs(
404-
options.appHostname,
405-
options.protocol,
406-
siteId,
407-
slasEndpointsRequiringAccessToken
408-
)
403+
configureProxyConfigs(options.appHostname, options.protocol, siteId)
409404
},
410405

411406
/**
@@ -982,6 +977,34 @@ export const RemoteServerFactory = {
982977
// SLAS logout (/oauth2/logout), use the Authorization header for a different
983978
// purpose so we don't want to overwrite the header for those calls.
984979
proxyRequest.setHeader('Authorization', `Basic ${encodedSlasCredentials}`)
980+
} else if (
981+
incomingRequest.path?.match(options.slasEndpointsRequiringAccessToken)
982+
) {
983+
// Inject tokens from HttpOnly cookies for endpoints like /oauth2/logout
984+
const cookieHeader = incomingRequest.headers.cookie
985+
if (cookieHeader) {
986+
const cookies = cookie.parse(cookieHeader)
987+
const siteId = options.mobify?.app?.commerceAPI?.parameters?.siteId
988+
if (siteId) {
989+
const site = siteId.trim()
990+
991+
// Inject Bearer token from access token cookie
992+
const accessToken = cookies[`cc-at_${site}`]
993+
if (accessToken) {
994+
proxyRequest.setHeader('Authorization', `Bearer ${accessToken}`)
995+
}
996+
997+
// Inject refresh_token into query string from HttpOnly cookie
998+
// Try registered user cookie first, then guest
999+
const refreshToken =
1000+
cookies[`cc-nx_${site}`] || cookies[`cc-nx-g_${site}`]
1001+
if (refreshToken) {
1002+
const url = new URL(proxyRequest.path, 'http://localhost')
1003+
url.searchParams.set('refresh_token', refreshToken)
1004+
proxyRequest.path = url.pathname + url.search
1005+
}
1006+
}
1007+
}
9851008
}
9861009

9871010
// Allow users to apply additional custom modifications to the proxy request

packages/pwa-kit-runtime/src/ssr/server/build-remote-server.test.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -483,11 +483,15 @@ describe('HttpOnly session cookies', () => {
483483
}
484484
})
485485

486-
test('skips non-token endpoints like logout', async () => {
486+
test('injects Bearer token and refresh token from HttpOnly cookies for logout endpoint', async () => {
487487
process.env.MRT_DISABLE_HTTPONLY_SESSION_COOKIES = 'false'
488488

489+
let capturedAuthHeader
490+
let capturedRefreshToken
489491
const mockSlasServer = mockExpress()
490492
mockSlasServer.post('/shopper/auth/v1/oauth2/logout', (req, res) => {
493+
capturedAuthHeader = req.headers.authorization
494+
capturedRefreshToken = req.query.refresh_token
491495
res.status(200).json({success: true})
492496
})
493497

@@ -517,12 +521,14 @@ describe('HttpOnly session cookies', () => {
517521

518522
RemoteServerFactory._setupSlasPrivateClientProxy(app, options)
519523

520-
const response = await request(app).post(
521-
'/mobify/slas/private/shopper/auth/v1/oauth2/logout'
522-
)
524+
const response = await request(app)
525+
.post('/mobify/slas/private/shopper/auth/v1/oauth2/logout')
526+
.set('Cookie', 'cc-at_testsite=mock-access-token; cc-nx_testsite=mock-refresh-token')
523527

524528
expect(response.status).toBe(200)
525529
expect(response.body.success).toBe(true)
530+
expect(capturedAuthHeader).toBe('Bearer mock-access-token')
531+
expect(capturedRefreshToken).toBe('mock-refresh-token')
526532
expect(response.headers['set-cookie']).toBeUndefined()
527533
} finally {
528534
mockSlasServerInstance.close()

packages/pwa-kit-runtime/src/utils/ssr-server/configure-proxy.basic.test.js

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,7 @@ describe('applyScapiAuthHeaders', () => {
113113
incomingRequest,
114114
caching: false,
115115
siteId: 'RefArch',
116-
targetHost: 'abc-001.api.commercecloud.salesforce.com',
117-
slasEndpointsRequiringAccessToken: /\/oauth2\/logout/
116+
targetHost: 'abc-001.api.commercecloud.salesforce.com'
118117
})
119118

120119
expect(proxyRequest.setHeader).toHaveBeenCalledWith(
@@ -123,7 +122,7 @@ describe('applyScapiAuthHeaders', () => {
123122
)
124123
})
125124

126-
it('applies Bearer token for SLAS logout endpoint', () => {
125+
it('skips all SLAS auth endpoints (handled by SLAS private proxy)', () => {
127126
utils.isScapiDomain.mockReturnValue(true)
128127
cookie.parse.mockReturnValue({'cc-at_RefArch': 'test-access-token'})
129128

@@ -140,38 +139,10 @@ describe('applyScapiAuthHeaders', () => {
140139
incomingRequest,
141140
caching: false,
142141
siteId: 'RefArch',
143-
targetHost: 'abc-001.api.commercecloud.salesforce.com',
144-
slasEndpointsRequiringAccessToken: /\/oauth2\/logout/
142+
targetHost: 'abc-001.api.commercecloud.salesforce.com'
145143
})
146144

147-
expect(proxyRequest.setHeader).toHaveBeenCalledWith(
148-
'authorization',
149-
'Bearer test-access-token'
150-
)
151-
})
152-
153-
it('does not apply Bearer token for SLAS token endpoint', () => {
154-
utils.isScapiDomain.mockReturnValue(true)
155-
cookie.parse.mockReturnValue({'cc-at_RefArch': 'test-access-token'})
156-
157-
const proxyRequest = {
158-
setHeader: jest.fn()
159-
}
160-
const incomingRequest = {
161-
url: '/shopper/auth/v1/oauth2/token',
162-
headers: {cookie: 'cc-at_RefArch=test-access-token'}
163-
}
164-
165-
applyScapiAuthHeaders({
166-
proxyRequest,
167-
incomingRequest,
168-
caching: false,
169-
siteId: 'RefArch',
170-
targetHost: 'abc-001.api.commercecloud.salesforce.com',
171-
slasEndpointsRequiringAccessToken: /\/oauth2\/logout/
172-
})
173-
174-
// Should not set authorization header for token endpoint (uses Basic Auth)
145+
// SLAS auth endpoints are handled by the SLAS private client proxy
175146
expect(proxyRequest.setHeader).not.toHaveBeenCalled()
176147
})
177148

@@ -192,8 +163,7 @@ describe('applyScapiAuthHeaders', () => {
192163
incomingRequest,
193164
caching: true,
194165
siteId: 'RefArch',
195-
targetHost: 'abc-001.api.commercecloud.salesforce.com',
196-
slasEndpointsRequiringAccessToken: /\/oauth2\/logout/
166+
targetHost: 'abc-001.api.commercecloud.salesforce.com'
197167
})
198168

199169
// Caching proxies don't use auth
@@ -217,8 +187,7 @@ describe('applyScapiAuthHeaders', () => {
217187
incomingRequest,
218188
caching: false,
219189
siteId: null,
220-
targetHost: 'abc-001.api.commercecloud.salesforce.com',
221-
slasEndpointsRequiringAccessToken: /\/oauth2\/logout/
190+
targetHost: 'abc-001.api.commercecloud.salesforce.com'
222191
})
223192

224193
expect(proxyRequest.setHeader).not.toHaveBeenCalled()
@@ -241,8 +210,7 @@ describe('applyScapiAuthHeaders', () => {
241210
incomingRequest,
242211
caching: false,
243212
siteId: 'RefArch',
244-
targetHost: 'external-api.example.com',
245-
slasEndpointsRequiringAccessToken: /\/oauth2\/logout/
213+
targetHost: 'external-api.example.com'
246214
})
247215

248216
expect(proxyRequest.setHeader).not.toHaveBeenCalled()
@@ -265,8 +233,7 @@ describe('applyScapiAuthHeaders', () => {
265233
incomingRequest,
266234
caching: false,
267235
siteId: 'RefArch',
268-
targetHost: 'abc-001.api.commercecloud.salesforce.com',
269-
slasEndpointsRequiringAccessToken: /\/oauth2\/logout/
236+
targetHost: 'abc-001.api.commercecloud.salesforce.com'
270237
})
271238

272239
expect(proxyRequest.setHeader).not.toHaveBeenCalled()

packages/pwa-kit-runtime/src/utils/ssr-server/configure-proxy.js

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {processExpressResponse} from './process-express-response'
1212
import {isRemote, localDevLog, verboseProxyLogging, isScapiDomain} from './utils'
1313
import logger from '../logger-instance'
1414
import {getEnvBasePath} from '../ssr-namespace-paths'
15-
import {SLAS_ENDPOINTS_REQUIRING_ACCESS_TOKEN} from '../../ssr/server/constants'
1615

1716
export const ALLOWED_CACHING_PROXY_REQUEST_METHODS = ['HEAD', 'GET', 'OPTIONS']
1817

@@ -37,8 +36,8 @@ const generalProxyPathRE = /^\/mobify\/proxy\/([^/]+)(\/.*)$/
3736
* 1. Caching proxies never use auth (skip)
3837
* 2. siteId must be provided (skip if not)
3938
* 3. Target must be SCAPI domain (skip if not)
40-
* 4. For SLAS auth endpoints (/shopper/auth/*): Only apply if they match the regex
41-
* - Most use Basic Auth (client credentials), but some like /oauth2/logout need Bearer token
39+
* 4. SLAS auth endpoints (/shopper/auth/*) are skipped — Bearer injection for SLAS
40+
* is handled by the SLAS private client proxy in build-remote-server.js
4241
* 5. For non-SLAS auth endpoints (e.g., /shopper/products, /shopper/baskets): Always apply Bearer token
4342
*
4443
* @private
@@ -48,32 +47,24 @@ const generalProxyPathRE = /^\/mobify\/proxy\/([^/]+)(\/.*)$/
4847
* @param caching {Boolean} true for a caching proxy, false for a standard proxy
4948
* @param siteId {String} the site ID for the current request
5049
* @param targetHost {String} the target hostname (host+port)
51-
* @param slasEndpointsRequiringAccessToken {RegExp} regex for SLAS auth endpoints that need Bearer token
5250
*/
5351
export const applyScapiAuthHeaders = ({
5452
proxyRequest,
5553
incomingRequest,
5654
caching,
5755
siteId,
58-
targetHost,
59-
slasEndpointsRequiringAccessToken
56+
targetHost
6057
}) => {
6158
const url = incomingRequest.url
6259

6360
// Skip if: caching proxy, no siteId, not SCAPI domain, or no URL
6461
if (caching || !siteId || !isScapiDomain(targetHost) || !url) {
6562
return
6663
}
64+
// SLAS auth endpoints are handled by the SLAS private client proxy
6765
if (url.startsWith('/shopper/auth/')) {
68-
// For SLAS auth endpoints, only apply if they match the configured regex
69-
// Most SLAS endpoints use Basic Auth, only specific ones like /oauth2/logout need Bearer token
70-
if (!slasEndpointsRequiringAccessToken || !url.match(slasEndpointsRequiringAccessToken)) {
71-
return
72-
}
66+
return
7367
}
74-
// If we reach here, either:
75-
// 1. It's a SLAS auth endpoint that matched the regex, OR
76-
// 2. It's a non-SLAS auth endpoint (which always requires Bearer token)
7768

7869
// Get access token from HttpOnly cookie
7970
const cookieHeader = incomingRequest.headers.cookie
@@ -185,7 +176,6 @@ export const applyProxyRequestHeaders = ({
185176
* @param caching {Boolean} true for a caching proxy, false for a
186177
* standard proxy.
187178
* @param siteId {String} the site ID for the current request
188-
* @param slasEndpointsRequiringAccessToken {RegExp} regex for SLAS auth endpoints that require Bearer token
189179
* @returns {middleware} function to pass to expressApp.use()
190180
*/
191181
export const configureProxy = ({
@@ -195,8 +185,7 @@ export const configureProxy = ({
195185
targetHost,
196186
appProtocol = /* istanbul ignore next */ 'https',
197187
caching,
198-
siteId = null,
199-
slasEndpointsRequiringAccessToken = SLAS_ENDPOINTS_REQUIRING_ACCESS_TOKEN
188+
siteId = null
200189
}) => {
201190
// This configuration must match the behaviour of the proxying
202191
// in CloudFront.
@@ -279,8 +268,7 @@ export const configureProxy = ({
279268
incomingRequest,
280269
caching,
281270
siteId,
282-
targetHost,
283-
slasEndpointsRequiringAccessToken
271+
targetHost
284272
})
285273
},
286274

@@ -374,15 +362,9 @@ export const configureProxy = ({
374362
* @param {String} appProtocol {String} the protocol to use to make requests to
375363
* the origin ('http' or 'https', defaults to 'https')
376364
* @param {String} siteId - the site ID for the current request
377-
* @param {RegExp} slasEndpointsRequiringAccessToken - regex for SLAS auth endpoints that require Bearer token
378365
* @private
379366
*/
380-
export const configureProxyConfigs = (
381-
appHostname,
382-
appProtocol,
383-
siteId = null,
384-
slasEndpointsRequiringAccessToken = SLAS_ENDPOINTS_REQUIRING_ACCESS_TOKEN
385-
) => {
367+
export const configureProxyConfigs = (appHostname, appProtocol, siteId = null) => {
386368
localDevLog('')
387369
proxyConfigs.forEach((config) => {
388370
localDevLog(
@@ -395,8 +377,7 @@ export const configureProxyConfigs = (
395377
appProtocol,
396378
appHostname,
397379
caching: false,
398-
siteId,
399-
slasEndpointsRequiringAccessToken
380+
siteId
400381
})
401382
config.cachingProxy = configureProxy({
402383
proxyPath: config.cachingPath,

0 commit comments

Comments
 (0)