CORS issues in headless commerce development

Care to share?

In large applications, such as modern eCommerce platforms, separating the responsibility of the front end from the core of the store is a must. While building a proper headless architecture, you may need to work on a front-end application connected to the commerce engine located under a different domain. One of the reasons might be the desire to use a properly configured back end. Another reason is the dynamic generation of environments with their domains for only one side, the back end or front end, thanks to DevOps tools.

One of the most common issues with connecting those two applications is related to cross-domain requests. It's a specific problem where browser security becomes your adversary. While its goal is to follow the OWASP Top 10 rules and avoid injecting malicious script into the browser, it effectively ceases connection between your front end and back end.

CORS missing allow origin issue

Cross-origin resource sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins, including domain, scheme, or port, other than its own from which a browser should permit loading resources.

Source:MDN Web Docs

Let’s suppose we aren’t prepared to support CORS. In that case, we’ll get the CORS Missing Allow Origin information in the browser console if we’re using Mozilla Firefox. When the browser tries to check the possibility of cross-domain communication, this error is preceded by another problem: NR_ERROR_DOM_BAD_URI.

Modern browsers block requests made to a back-end application located in a different domain. If we get a set-cookie header in the response, the browser won’t set the cookie, and we won’t be able to maintain the session.

How to enable CORS support

The Mozilla Foundation has thoroughly described how to handle CORS in this article. Let's focus on how to quickly achieve success. Generally, it can’t be done without changes to the headless back-end and front-end applications.

Back-end application configuration

In my experience, you only need to set three headers:

Access-Control-Allow-Origin: <FRONT_END_APPLICATION_DOMAIN>
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: *

For example, you can do this in nginx:

add_header 'Access-Control-Allow-Origin' '<FRONT-END_APPLICATION_DOMAIN>' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Headers' '*' always;

You can also do this in Apache:

Header set Access-Control-Allow-Origin "<FRONT-END_APPLICATION_DOMAIN>"
Header set Access-Control-Allow-Credentials "true"
Header set Access-Control-Allow-Headers "*"

Don’t forget to restart the web server service.

Let’s suppose you need the domain address of the front-end application to be dynamically substituted. It’s easy to set the headers in the application, like, for example, in PHP:

if ($_SERVER['HTTP_REFERER']) {
   $url = parse_url($_SERVER['HTTP_REFERER']);
   $url = sprintf('%s://%s', $url['scheme'], $url['host']);
} else {
   $url = ''; // TODO
}
header(sprintf('Access-Control-Allow-Origin: %s', $url));
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Headers: *');

Important note: We can’t put a wildcard for a domain in the Access-Control-Allow-Origin header. Remember that http:// and https:// start different origins from the browsers’ point of view.

Using the possibilities of the front-end application

Unfortunately, headers aren't everything. We still need to send requests from the front-end application properly. We need to “say” to our JavaScript code that it can set cookies from different domains. To achieve that, we need to set withCredentials in XHR request.

You'll need slightly different code depending on the JavaScript framework being used.

In pure Axios, you can use a call like this:

axios.get(<URL>, { withCredentials: true });

In turn, in the Nuxt.js configuration, we can set it globally for Axios:

axios: {
   credentials: true,
},

In fetch method (ES6), just use:

fetch('<URL>', {
   credentials: 'include'
});

In jQuery (R.I.P.), use:

$.ajax({
   type: 'GET',
   url: '<URL>',
   xhrFields: {
       withCredentials: true
   }
);

Additional information

Some sources say cookies should have SameSite set to None. However, I haven’t confirmed this and managed to solve this CORS issue with the default setting of Lax. Further information can be found in the documentation from the Mozilla Foundation. However, if you have a restrictive third-party cookie policy set in a browser, you may find this advice helpful.

Another approach

Another solution to the CORS problem is to not use two domains and configure the environment so that both applications are available under the same domain. For example, that approach can be made thanks to a proxy, like HAProxy, so that appropriate requests (front end in HAProxy configuration and nomenclature) are directed to the proper application (back end → server in configuration). Ask DevOps what opportunities you have in your project.

Summary

Often, a CORS issue can be a blocker in development or make it difficult. It may also turn out that we change the approach to application development with a headless back end and a separate front end, but we don't have to do that. The solution is quite simple, as you can see. Just follow the above advice carefully.

Reference architecture for commercetools - big [alt]

Published January 24, 2023