Dealings with cookies with CORS

Written byKalanKalan
💡

If you have any questions or feedback, pleasefill out this form

This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.

Introduction

CORS and cookies are significant issues in front-end development. However, during development, since the front-end and back-end often share the same domain, these issues are rarely considered. Generally, developers just expect the back-end to set up Access-Control-Allow-Origin: * without truly understanding the underlying mechanisms.

For a thorough explanation of this topic, there is an extensive resource on MDN, so this article aims to summarize the key points and address common issues encountered in practical operations.

Same-Origin Policy

To prevent JavaScript from wreaking havoc on webpages, the same-origin policy stipulates that certain specific resources and code must be accessed under the same origin.

So, what qualifies as the same origin? The origin of a document is defined by its protocol, host, and port. This means that if Document 1 comes from http://kalan.com and Document 2 comes from https://kalan.com, they are considered different origins. What about subdomains? For instance, https://api.foobar.com and https://app.foobar.com are not considered the same origin because their hosts differ.

However, some resources can be accessed across origins by default:

  • <img />
  • <video />, <audio />
  • <iframe />: can be prevented from being embedded by setting specific headers
  • CSS loaded via <link rel="stylesheet" href />
  • JavaScript loaded via <script src="" />

Cross-origin requests made through code (like Fetch and XHR) are restricted by the same-origin policy.

Clearly, this policy is too strict. If everything had to comply with the same-origin policy, front-end and back-end development would be extremely challenging, and developers wouldn't be able to apply other SDKs’ APIs via XHR. This is where CORS (Cross-Origin Resource Sharing) comes into play.

CORS (Cross-Origin Resource Sharing)

Many people think that CORS is knowledge only front-end developers need. However, CORS often requires back-end configuration of relevant headers and an understanding of their implications to work correctly.

So how does cross-origin request work? It primarily involves two headers that control access: Origin and Access-Control-Allow-Origin.

As long as the Origin of the request matches the value of Access-Control-Allow-Origin in the response header, or if it's set to Access-Control-Allow-Origin: * (which allows any domain to access the resource), the request will succeed.

If CORS requirements are not met, the following message will appear:

2019-01-18 10 12 54

If you attempt to read the returned object, you will receive a warning as well.

So... what happens if we follow the prompt and change the fetch mode to no-cors?

Indeed, we can suppress the annoying error message, but the situation doesn't seem to improve.

no-cors is not a panacea; even when using this mode, CORS will not open the floodgates, meaning that your request will not be successfully sent. This leads to the SyntaxError: Unexpected end of input error. This mode is typically used in conjunction with service workers.

From this experiment, we can conclude that the only way to lift the CORS restriction is to add the correct Access-Control-Allow-Origin on the server side (the host must match the origin or be *).

Additionally, the CORS mechanism only operates when JavaScript sends XHR or fetch requests. In general, tools like curl or Postman do not enforce this mechanism, which often leads to discrepancies when testing API endpoints between the front-end and back-end.

Some cross-origin requests will not trigger a preflight, while others will. MDN clearly outlines the conditions for this:

  1. The request method must be one of GET, HEAD, or POST.
  2. It should not include any headers other than those automatically set by the user-agent and specific headers. Acceptable headers include:
  3. If there is a Content-Type (note, this is the request header, not the response header), it must be one of the following values: application/x-www-form-urlencoded, text/plain, or multipart/form-data.

This means that if the above conditions are not met, a preflight request will be sent.

We can change the Content-Type to application/json to trigger preflight requirements (not one of application/x-www-form-urlencoded, text/plain, or multipart/form-data).

Preflight

A preflight request is made using HTTP OPTIONS to check if everything is in order before sending the actual request. Once this condition is triggered, the process becomes significantly more complex.

  1. You must add an OPTIONS request for the same API endpoint, and set Access-Control-Allow-Origin to meet CORS requirements.
  2. You must include Access-Control-Allow-Headers, which must contain all headers that are not included in the allowed conditions; otherwise, the request will fail.

If the preflight check fails, you will receive an error message like this:

Access to fetch at 'http://localhost:3001/trigger-preflight' from origin 'http://localhost:3000' has been blocked by CORS policy:
Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.

Or if you didn't include Access-Control-Allow-Origin in the response headers of the OPTIONS request:

Access to fetch at 'http://localhost:3001/trigger-preflight' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

If successful, you will see two requests in the network panel: one for OPTIONS and another for the actual request.

What happens if we add a custom header? According to the requirements defined by MDN, this should also trigger a preflight request. Let's add an X-Access-Token and see what occurs.

fetch("http://localhost:3001/trigger-preflight", {
  headers: { "X-Access-Token": "dontbeserious" },
})
  .then(res => res.json())
  .then(log)
2019-01-18 11 20 07

Indeed, the preflight fails. To pass, you must include X-Access-Token in Access-Control-Allow-Headers.

Requests with Credentials

Cookies cannot be transmitted across domains, meaning that cookies from different origins cannot be shared or accessed; otherwise, chaos would ensue. However, if you make a request from domain A to domain B and domain B returns a cookie, a copy of the cookie will be stored in domain A in the format of domain B. Still, if you do not set withCredentials or credentials: 'include', even if the server returns a Set-Cookie, it will not be saved. As illustrated below:

2019-01-18 1 10 06 2019-01-18 1 10 18

Typically, when using the API from domain B, cookies are not automatically sent. In this case, you must set withCredentials in XHR or add { credentials: 'include' } to the fetch options. Since this is also a cross-origin request, you must also comply with CORS requirements and add Access-Control-Allow-Origin.

fetch(`${hostname}/cookie`, {
  method: "POST",
  credentials: "include",
})
Access to fetch at 'http://localhost:3001/cookie' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

To avoid security issues, browsers specify that Access-Control-Allow-Origin cannot be *.

Access to fetch at 'http://localhost:3001/cookie' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.

However, simply having this is not enough; the browser will automatically reject responses that do not include Access-Control-Allow-Credentials. Therefore, to send credential information to a cross-origin server, you must also add Access-Control-Allow-Credentials: true. If everything is set correctly, you should see the cookie successfully sent in the Request Cookie section as shown below.

2019-01-18 1 11 37

Even if you successfully configure all these settings, you may still encounter issues sending cookies to the server under certain circumstances:

1. The user has blocked cookies for this domain.

The user may have blacklisted you, preventing the cookie from being sent successfully.

Solution:

  • Change the domain.
  • Reflect on why you were blocked by the user.
2. The user is blocking cookies from all external sites.

Safari can sometimes activate this feature, leading to significant headaches during debugging.

2019-01-18 1 13 21

Conclusion

Dealing with CORS can be a thankless task, especially when you forget to include Access-Control-Allow-Origin or Access-Control-Allow-Credentials, and running CI/CD or deploying might take another day. I've compiled some common issues in hopes that if similar situations arise in the future, you'll know how to address them.

However, with AWS API Gateway, you can add the necessary headers without touching your main codebase, or you can simply set up a proxy layer under the same domain for a more straightforward solution.

References

If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨

Buy me a coffee