Skip to content

Commit b1ecd5b

Browse files
authored
Merge pull request #3042 from SalesforceCommerceCloud/slas-proxy-improvements
@W-19078442@ Extra check to disallow trusted-system request from proxy
2 parents f0993c0 + 4f970af commit b1ecd5b

File tree

3 files changed

+110
-80
lines changed

3 files changed

+110
-80
lines changed

packages/pwa-kit-runtime/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## v3.12.0-dev (Jul 28, 2025)
22
- This feature introduces an AI-powered shopping assistant that integrates Salesforce Embedded Messaging Service with PWA Kit applications. The shopper agent provides real-time chat support, search assistance, and personalized shopping guidance directly within the e-commerce experience. [#2658](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2658)
3+
- Disallow the SLAS private client proxy from handling trusted system on behalf of requests [#3042](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3042)
34

45
## v3.11.0 (Jul 22, 2025)
56
- Fix the logger so that it will now print out details of the given Error object [#2486](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2486)

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

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,17 @@ export const RemoteServerFactory = {
695695
return
696696
}
697697

698+
// This is the full path to the SLAS trusted-system endpoint
699+
// We want to throw an error if the regex defined options.applySLASPrivateClientToEndpoints
700+
// matches this path as an early warning to developers that they should update their regex
701+
// in ssr.js to exclude this path.
702+
const trustedSystemPath = '/shopper/auth/v1/oauth2/trusted-system/token'
703+
if (trustedSystemPath.match(options.applySLASPrivateClientToEndpoints)) {
704+
throw new Error(
705+
'It is not allowed to include /oauth2/trusted-system endpoints in `applySLASPrivateClientToEndpoints`'
706+
)
707+
}
708+
698709
localDevLog(`Proxying ${slasPrivateProxyPath} to ${options.slasTarget}`)
699710

700711
const clientId = options.mobify?.app?.commerceAPI?.parameters?.clientId
@@ -721,24 +732,30 @@ export const RemoteServerFactory = {
721732
targetProtocol: 'https'
722733
})
723734

724-
// We pattern match and add client secrets only to endpoints that
725-
// match the regex specified by options.applySLASPrivateClientToEndpoints
726-
// (see option defaults at the top of this file).
727-
// Other SLAS endpoints, ie. SLAS authenticate (/oauth2/login) and
728-
// SLAS logout (/oauth2/logout), use the Authorization header for a different
729-
// purpose so we don't want to overwrite the header for those calls.
730-
if (incomingRequest.path?.match(options.applySLASPrivateClientToEndpoints)) {
731-
proxyRequest.setHeader('Authorization', `Basic ${encodedSlasCredentials}`)
732-
} else if (!incomingRequest.path?.match(options.slasApiPath)) {
735+
// We don't want the proxy to handle any non-SLAS requests
736+
// or any trusted system requests
737+
if (
738+
!incomingRequest.path?.match(options.slasApiPath) ||
739+
incomingRequest.path?.match(/\/oauth2\/trusted-system/)
740+
) {
733741
const message = `Request to ${incomingRequest.path} is not allowed through the SLAS Private Client Proxy`
734742
logger.error(message)
735743
return res.status(403).json({
736744
message: message
737745
})
738746
}
739747

740-
// /oauth2/trusted-agent/token endpoint requires a different auth header
741-
if (incomingRequest.path?.match(/\/oauth2\/trusted-agent\/token/)) {
748+
// We pattern match and add client secrets only to endpoints that
749+
// match the regex specified by options.applySLASPrivateClientToEndpoints.
750+
//
751+
// Other SLAS endpoints, ie. SLAS authenticate (/oauth2/login) and
752+
// SLAS logout (/oauth2/logout), use the Authorization header for a different
753+
// purpose so we don't want to overwrite the header for those calls.
754+
if (incomingRequest.path?.match(options.applySLASPrivateClientToEndpoints)) {
755+
proxyRequest.setHeader('Authorization', `Basic ${encodedSlasCredentials}`)
756+
} else if (incomingRequest.path?.match(/\/oauth2\/trusted-agent\/token/)) {
757+
// /oauth2/trusted-agent/token endpoint auth header comes from Account Manager
758+
// so the SLAS private client is sent via this special header
742759
proxyRequest.setHeader('_sfdc_client_auth', encodedSlasCredentials)
743760
}
744761
}

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

Lines changed: 81 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,9 +1086,24 @@ describe('SLAS private client proxy', () => {
10861086
const savedEnvironment = Object.assign({}, process.env)
10871087

10881088
let proxyApp
1089+
let proxyServer
10891090
const proxyPort = 12345
10901091
const proxyPath = '/shopper/auth/responseHeaders'
10911092
const slasTarget = `http://localhost:${proxyPort}${proxyPath}`
1093+
const appConfig = {
1094+
mobify: {
1095+
app: {
1096+
commerceAPI: {
1097+
parameters: {
1098+
clientId: 'clientId',
1099+
shortCode: 'shortCode'
1100+
}
1101+
}
1102+
}
1103+
},
1104+
useSLASPrivateClient: true,
1105+
slasTarget: slasTarget
1106+
}
10921107

10931108
beforeAll(() => {
10941109
// by setting slasTarget, rather than forwarding the request to SLAS,
@@ -1097,15 +1112,38 @@ describe('SLAS private client proxy', () => {
10971112
proxyApp.use(proxyPath, (req, res) => {
10981113
res.send(req.headers)
10991114
})
1100-
proxyApp.listen(proxyPort)
1115+
proxyServer = proxyApp.listen(proxyPort)
11011116
})
11021117

11031118
afterEach(() => {
11041119
process.env = savedEnvironment
11051120
})
11061121

1107-
afterAll(() => {
1108-
proxyApp.close()
1122+
// There is a lot of cleanup done here to ensure the proxy server is closed
1123+
// after these tests.
1124+
afterAll(async () => {
1125+
if (proxyServer) {
1126+
// Close the server and wait for it to fully close
1127+
await new Promise((resolve) => {
1128+
proxyServer.close(() => {
1129+
resolve()
1130+
})
1131+
})
1132+
1133+
// Additional cleanup to ensure all connections are closed
1134+
proxyServer.unref()
1135+
1136+
// Force close any remaining connections
1137+
if (proxyServer._handle) {
1138+
proxyServer._handle.close()
1139+
}
1140+
1141+
// Clear any remaining event listeners
1142+
proxyServer.removeAllListeners()
1143+
}
1144+
1145+
// Clear any remaining timers or intervals
1146+
jest.clearAllTimers()
11091147
})
11101148

11111149
test('should not create proxy by default', () => {
@@ -1121,22 +1159,7 @@ describe('SLAS private client proxy', () => {
11211159
test('does not insert client secret if request not for /oauth2/token', async () => {
11221160
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret'
11231161

1124-
const app = RemoteServerFactory._createApp(
1125-
opts({
1126-
mobify: {
1127-
app: {
1128-
commerceAPI: {
1129-
parameters: {
1130-
clientId: 'clientId',
1131-
shortCode: 'shortCode'
1132-
}
1133-
}
1134-
}
1135-
},
1136-
useSLASPrivateClient: true,
1137-
slasTarget: slasTarget
1138-
})
1139-
)
1162+
const app = RemoteServerFactory._createApp(opts(appConfig))
11401163

11411164
return await request(app)
11421165
.get('/mobify/slas/private/shopper/auth/v1/somePath')
@@ -1152,22 +1175,7 @@ describe('SLAS private client proxy', () => {
11521175

11531176
const encodedCredentials = Buffer.from('clientId:a secret').toString('base64')
11541177

1155-
const app = RemoteServerFactory._createApp(
1156-
opts({
1157-
mobify: {
1158-
app: {
1159-
commerceAPI: {
1160-
parameters: {
1161-
clientId: 'clientId',
1162-
shortCode: 'shortCode'
1163-
}
1164-
}
1165-
}
1166-
},
1167-
useSLASPrivateClient: true,
1168-
slasTarget: slasTarget
1169-
})
1170-
)
1178+
const app = RemoteServerFactory._createApp(opts(appConfig))
11711179

11721180
return await request(app)
11731181
.get('/mobify/slas/private/shopper/auth/v1/oauth2/token')
@@ -1181,24 +1189,7 @@ describe('SLAS private client proxy', () => {
11811189
test('does not add _sfdc_client_auth header if request not for /oauth2/trusted-agent/token', async () => {
11821190
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret'
11831191

1184-
const encodedCredentials = Buffer.from('clientId:a secret').toString('base64')
1185-
1186-
const app = RemoteServerFactory._createApp(
1187-
opts({
1188-
mobify: {
1189-
app: {
1190-
commerceAPI: {
1191-
parameters: {
1192-
clientId: 'clientId',
1193-
shortCode: 'shortCode'
1194-
}
1195-
}
1196-
}
1197-
},
1198-
useSLASPrivateClient: true,
1199-
slasTarget: slasTarget
1200-
})
1201-
)
1192+
const app = RemoteServerFactory._createApp(opts(appConfig))
12021193

12031194
return await request(app)
12041195
.get('/mobify/slas/private/shopper/auth/v1/oauth2/other-path')
@@ -1242,25 +1233,46 @@ describe('SLAS private client proxy', () => {
12421233
test('returns 403 if request is not for /shopper/auth endpoints', async () => {
12431234
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret'
12441235

1245-
const app = RemoteServerFactory._createApp(
1246-
opts({
1247-
mobify: {
1248-
app: {
1249-
commerceAPI: {
1250-
parameters: {
1251-
clientId: 'clientId',
1252-
shortCode: 'shortCode'
1253-
}
1254-
}
1255-
}
1256-
},
1257-
useSLASPrivateClient: true,
1258-
slasTarget: slasTarget
1259-
})
1260-
)
1236+
const app = RemoteServerFactory._createApp(opts(appConfig))
12611237

12621238
return await request(app)
12631239
.get('/mobify/slas/private/shopper/auth-admin/v1/other-path')
12641240
.expect(403)
12651241
}, 15000)
1242+
1243+
test('returns 403 if request is for /oauth2/trusted-system/* endpoint', async () => {
1244+
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret'
1245+
1246+
const app = RemoteServerFactory._createApp(opts(appConfig))
1247+
1248+
return await request(app)
1249+
.get('/mobify/slas/private/shopper/auth/v1/oauth2/trusted-system/token')
1250+
.expect(403)
1251+
}, 15000)
1252+
1253+
test('throws an error if /oauth2/trusted-system/* is included in applySLASPrivateClientToEndpoints', async () => {
1254+
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret'
1255+
1256+
expect(() => {
1257+
RemoteServerFactory._createApp(
1258+
opts({
1259+
mobify: {
1260+
app: {
1261+
commerceAPI: {
1262+
parameters: {
1263+
clientId: 'clientId',
1264+
shortCode: 'shortCode'
1265+
}
1266+
}
1267+
}
1268+
},
1269+
useSLASPrivateClient: true,
1270+
slasTarget: slasTarget,
1271+
applySLASPrivateClientToEndpoints: /\/oauth2\/trusted-system/
1272+
})
1273+
)
1274+
}).toThrow(
1275+
'It is not allowed to include /oauth2/trusted-system endpoints in `applySLASPrivateClientToEndpoints`'
1276+
)
1277+
}, 15000)
12661278
})

0 commit comments

Comments
 (0)