SalesforceCommerceCloud / pwa-kit

React-based JavaScript frontend framework to create a progressive web app (PWA) storefront for Salesforce B2C Commerce.
https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/pwa-kit-overview.html
BSD 3-Clause "New" or "Revised" License
278 stars 130 forks source link

[FEATURE] Support for `x-forwarded-host` header #1257

Open johnboxall opened 1 year ago

johnboxall commented 1 year ago

When a CDN like eCDN is stacked on top of PWA Kit / MRT, the app may be unaware of the origin that the shoppers browser has landed on. This can result in:

  1. Requests from MRT inadvertently routing through the stacked CDN, often being erroneously blocked by WAF rules.
  2. Requests from browser inadvertently skipping the stacked CDN, and going direct to MRT, created CORS errors.

For the purposes of describing the problem, lets imagine a stacked CDN with the domain www.example.com and a MRT environment with the domain example-production.mobify-storefront.com in a stacked setup:

Browser -> CDN -> MRT
Browser -> www.example.com -> example-production.mobify-storefront.com

Within the PWA Kit, the getAppOrigin utility function and process.env.APP_ORIGIN environment variable are used to help construct URLs used to make requests both in browser and on MRT:

https://github.com/SalesforceCommerceCloud/pwa-kit/blob/51bf787c046d7d8d53cb97d21bb6a79e5bc20c60/packages/pwa-kit-react-sdk/src/utils/url.js#L21-L35 https://github.com/SalesforceCommerceCloud/pwa-kit/blob/51bf787c046d7d8d53cb97d21bb6a79e5bc20c60/packages/pwa-kit-runtime/src/ssr/server/build-remote-server.js#L140

They are set to process.env.EXTERNAL_DOMAIN_NAME, which comes from a MRT environment's external domain (eg. example-production.mobify-storefront.com)

In a stacked setup, this is the wrong domain for building client side absolute URLs.

The browser's origin will be www.example.com, but requests will end up being routed to example-production.mobify-storefront.com, which will result in CORS issues.

There are a few potential user-space work arounds today:

1️⃣ Make the MRT env accept requests on the same domain as the CDN

2️⃣ Static Configuration

3️⃣ Dynamic Configuration

4️⃣ Use relative URLs in browser

One other solution that may work is to use relative URLs when requests are being made to proxies in browser.


It would seem useful to have first class support for one of these options.


When we think about "desirable qualities" of a solution ... ideally:

  1. The solution is performant. When requests originate from the browser, we avoid CORS requests. Whatever the request path, we make the minimal amount of hops.
  2. The solution is "simple". It doesn't add significant complexity to the CDN logic or the PWA Kit code.
  3. It is standards based. That is, its likely that most CDNs support it out of the box as a configuration option as opposed to requiring custom edge logic.
johnboxall commented 6 months ago

A specific case where it is useful to have the "shopper" facing host is when doing CORS – you might want to decide whether to respond with Access-Control-Allow-Origin headers based on the requested host and to do that, you'd need the origin host.

To implement X-Forwarded-Host in user space, you can:

  1. Configure your Stacked CDN to add the X-Forwarded-Host header. Most CDN provide rules to allow you to do this, for example in CloudFlare, you can use Transform rules to dynamically add the header.
  2. Enable cookies on your MRT environment. This is required to forward all HTTP request headers to the App Server.
  3. Update your request-processor.js to grab the X-Forwarded-Host header and add it to the request class. This is required to make the cache key vary this header.
  4. Update ssr.js to use the header!

// request-processor.js

// 1️⃣  Grab the shopper facing host and put it in the request class to vary cache keys by it.
export const processRequest = ({setRequestClass, headers, path, querystring}) => {
    const host = headers.getHeader('x-forwarded-host')
    setRequestClass(new URLSearchParams({host}))
    return { path, querystring }
}

// app/ssr.js

// 2️⃣ Dig out the value. Use it however you want to vary responses!
app.get('/', (req, res) => {
    res.json({host: req.get('x-forwarded-host'})
})
johnboxall commented 6 months ago

When we think about how we want to expose this, it should use Express' trust proxy feature:

https://expressjs.com/en/guide/behind-proxies.html

This would make req.hostname behave as folks expect.

This is related to #1667.

drewzboto commented 6 months ago

Not super keen on 1 as that means you have to setup the custom domain twice on eCDN and MRT. More friction to the launch process.

mgalassi commented 4 months ago

Hi @drewzboto @johnboxall, sorry to bother you but any news on this? As far as we know the eCDN currently does not pass the x-forwarded-host header by default so I'm afraid the suggested solution will not work as intended when the stacked CDN is the eCDN one.

johnboxall commented 3 months ago

Hey @mgalassi – if you submit a support request, internal teams can help enabled the x-forwarded-host header on your eCDN zone:

https://help.salesforce.com/

Please note that this is a best effort process and no SLA is provided so if you require eCDN to append this header ask early!

This issue is also related to https://github.com/SalesforceCommerceCloud/pwa-kit/issues/1415, as you may want to use it to vary site based on the incoming request.

mgalassi commented 3 months ago

Hi @johnboxall, that's what I did some days after writing the comment and I can confirm to everyone that the support team can indeed try to add the header to specific domains. I have asked the team to add it to one of our domains for testing and then they would take approx. 2-3 weeks to add it to all the other domains as well (we are talking about 8-9 four/third level domains that can be easily managed by a single environment).

As always, thank you very much John!