How Do Cors and Access-Control-Allow-Headers Work

How does the 'Access-Control-Allow-Origin' header work?

Access-Control-Allow-Origin is a CORS (cross-origin resource sharing) header.

When Site A tries to fetch content from Site B, Site B can send an Access-Control-Allow-Origin response header to tell the browser that the content of this page is accessible to certain origins. (An origin is a domain, plus a scheme and port number.) By default, Site B's pages are not accessible to any other origin; using the Access-Control-Allow-Origin header opens a door for cross-origin access by specific requesting origins.

For each resource/page that Site B wants to make accessible to Site A, Site B should serve its pages with the response header:

Access-Control-Allow-Origin: http://siteA.com

Modern browsers will not block cross-domain requests outright. If Site A requests a page from Site B, the browser will actually fetch the requested page on the network level and check if the response headers list Site A as a permitted requester domain. If Site B has not indicated that Site A is allowed to access this page, the browser will trigger the XMLHttpRequest's error event and deny the response data to the requesting JavaScript code.

Non-simple requests

What happens on the network level can be slightly more complex than explained above. If the request is a "non-simple" request, the browser first sends a data-less "preflight" OPTIONS request, to verify that the server will accept the request. A request is non-simple when either (or both):

  • using an HTTP verb other than GET or POST (e.g. PUT, DELETE)
  • using non-simple request headers; the only simple requests headers are:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (this is only simple when its value is application/x-www-form-urlencoded, multipart/form-data, or text/plain)

If the server responds to the OPTIONS preflight with appropriate response headers (Access-Control-Allow-Headers for non-simple headers, Access-Control-Allow-Methods for non-simple verbs) that match the non-simple verb and/or non-simple headers, then the browser sends the actual request.

Supposing that Site A wants to send a PUT request for /somePage, with a non-simple Content-Type value of application/json, the browser would first send a preflight request:

OPTIONS /somePage HTTP/1.1
Origin: http://siteA.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type

Note that Access-Control-Request-Method and Access-Control-Request-Headers are added by the browser automatically; you do not need to add them. This OPTIONS preflight gets the successful response headers:

Access-Control-Allow-Origin: http://siteA.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type

When sending the actual request (after preflight is done), the behavior is identical to how a simple request is handled. In other words, a non-simple request whose preflight is successful is treated the same as a simple request (i.e., the server must still send Access-Control-Allow-Origin again for the actual response).

The browsers sends the actual request:

PUT /somePage HTTP/1.1
Origin: http://siteA.com
Content-Type: application/json

{ "myRequestContent": "JSON is so great" }

And the server sends back an Access-Control-Allow-Origin, just as it would for a simple request:

Access-Control-Allow-Origin: http://siteA.com

See Understanding XMLHttpRequest over CORS for a little more information about non-simple requests.

How should I be using Access-Control-Allow-Headers?

  1. See my response to your 3rd point.
  2. Yes. Your server, your rules.
  3. Setting both Access-Control-Allow-Credentials: true and Access-Control-Allow-Headers: * is never useful:
  • For security reasons, browsers that support the wildcard in Access-Control-Allow-Headers treat the * value literally in the case of credentialed requests.
  • Browsers that don't support the wildcard in Access-Control-Allow-Headers always treat the * value literally.

Access-Control-Allow-Headers what does it do exactly and why is it included in a response?

Whenever a request to a server is made from cross origin (say you're requesting an API hosted at Domain A from Domain B), generally the browser blocks them for security reasons. To navigate (this might not be the 'right' word; it's, in fact, the only way of doing) this, there's a mechanism called CORS (cross origin resource sharing) in which the server has to explicitly say that it wants to allow resource sharing with other domains(origins) too. Now, to 'tell' this, the response has to contain right CORS headers which is Access-Control-Allow-Origin header.

So in layman terms, it's just a mechanism to inform the browser, "Hey! Look, I deliberately want to send this response to another origin. Don't block it."

So let's say. if the server responds with a wildcard:

 Access-Control-Allow-Origin: *, 

This means, the resource can be accessed by any domain.

Similarly, if the server sends

Access-Control-Allow-Origin: "https://example.com"

Then, this means the resource can be accessed only on this particular domain which is "https://example.com"

This is my understanding.

For more technical details, you may read:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers

Request header field Access-Control-Allow-Headers is not allowed by itself in preflight response

When you start playing around with custom request headers you will get a CORS preflight. This is a request that uses the HTTP OPTIONS verb and includes several headers, one of which being Access-Control-Request-Headers listing the headers the client wants to include in the request.

You need to reply to that CORS preflight with the appropriate CORS headers to make this work. One of which is indeed Access-Control-Allow-Headers. That header needs to contain the same values the Access-Control-Request-Headers header contained (or more).

https://fetch.spec.whatwg.org/#http-cors-protocol explains this setup in more detail.

How do CORS and Access-Control-Allow-Headers work?

Yes, you need to have the header Access-Control-Allow-Origin: http://domain.example:3000 or Access-Control-Allow-Origin: * on both the OPTIONS response and the POST response. You should include the header Access-Control-Allow-Credentials: true on the POST response as well.

Your OPTIONS response should also include the header Access-Control-Allow-Headers: origin, content-type, accept to match the requested header.

Omitting access-control-allow-headers in CORS / preflight response

I’m having trouble finding information explaining what happens if the response to a preflight request or the response to the actual CORS request omits Access-Control-Allow-Headers. I have noticed that several sites say this header is required when the request includes Access-Control-Request-Headers.

Those sites are correct: if the preflight request contains an Access-Control-Request-Headers header, then the response to that request must, at the very least, contain an Access-Control-Allow-Headers header, or CORS preflight will fail. The actual header value required for CORS preflight to succeed will differ on the basis of which header names were specified in the preflight request.

I would like to allow CORS access to my API with any headers, so I’m wondering if omitting this header will do just that and allow all headers.

No, omitting the Access-Control-Allow-Headers header in the response to the preflight request does not count as a pass.

My API requires that you send an Authorization header containing an OAuth token and typically the client will also send a cookie. There are sites that say that using * for Access-Control-Allow-Headers only acts as a wildcard when Access-Control-Allow-Credentials is false.

Close, but imprecise. Rather, you cannot use the wildcard for Access-Control-Allow-Headers if you use Access-Control-Allow-Credentials: true. See https://fetch.spec.whatwg.org/#http-new-header-syntax

This seems to put me in a bind since I want the CORS request to include credentials (the Authorization header and a cookie) but I also want to allow all headers. How can I make this work?

In that case, you cannot use the wildcard. You only have one way: take all the header names listed in the preflight request's Access-Control-Request-Headers header and reflect them in the response's Access-Control-Allow-Headers header.

A secondary question is why would I want to restrict the request to an allowed set of headers? In my case the API is a GET which makes no changes on the server. I want allowed origins to always be able to retrieve this information. Under what circumstances would the headers they include play into that?

I may be missing something, but I don't think this is problematic if the set of allowed origins is finite. However, if you allow arbitrary origins, you probably shouldn't allow arbitrary headers on authenticated endpoints. In particular, allowing the Authorization for arbitrary origins opens the door to client-side, distributed brute-force of Bearer tokens (or whatever the Authorization normally contains); more about that in https://github.com/whatwg/fetch/issues/251#issuecomment-209265586

Set cookies for cross origin requests

Cross site approach

To allow receiving & sending cookies by a CORS request successfully, do the following.

Back-end (server) HTTP header settings:

  • Set the HTTP header Access-Control-Allow-Credentials value to true.

  • Make sure the HTTP headers Access-Control-Allow-Origin and Access-Control-Allow-Headers are set. Don't use a wildcard *. When you set the allowed origin make sure to use the entire origin including the scheme, i.e. http is not same as https in CORS.

For more info on setting CORS in express js read the docs here.

Cookie settings:
Cookie settings per Chrome and Firefox update in 2021:

  • SameSite=None
  • Secure

When doing SameSite=None, setting Secure is a requirement. See docs on SameSite and on requirement of Secure. Also note that Chrome devtools now have improved filtering and highlighting of problems with cookies in the Network tab and Application tab.

Front-end (client): Set the XMLHttpRequest.withCredentials flag to true, this can be achieved in different ways depending on the request-response library used:

  • ES6 fetch() This is the preferred method for HTTP. Use credentials: 'include'.

  • jQuery 1.5.1 Mentioned for legacy purposes. Use xhrFields: { withCredentials: true }.

  • axios As an example of a popular NPM library. Use withCredentials: true.

Proxy approach

Avoid having to do cross site (CORS) stuff altogether. You can achieve this with a proxy. Simply send all traffic to the same top level domain name and route using DNS (subdomain) and/or load balancing. With Nginx this is relatively little effort.

This approach is a perfect marriage with JAMStack. JAMStack dictates API and Webapp code to be completely decoupled by design. More and more users block 3rd party cookies. If API and Webapp can easily be served on the same host, the 3rd party problem (cross site / CORS) dissolves. Read about JAMStack here or here.

Sidenote

It turned out that Chrome won't set the cookie if the domain contains a port. Setting it for localhost (without port) is not a problem. Many thanks to Erwin for this tip!

How does Access-Control-Expose-Headers work?

I figured it out. The reason I was receiving my custom header was that I was reading the response headers in the Network tab of Chrome Dev Tools. When I run this script:

fetch('http://127.0.0.1:3000/')
.then(r => {console.log(response.headers.get('foo'))})

It prints null. So the header is not actually accessible to the fetch request, only to the Dev Tools.

Access-Control-Allow-Headers in preflight response

You would set the response headers in the server that you are making the request to.



Related Topics



Leave a reply



Submit