More often than not CORS mean little more to developers than just getting rid of the infamous browser error in order to continue cranking out features as soon as possible. In other words, it’s not the best understood concept out there.
Hence, to shed more light upon the topic, I’d like to give some working examples of CORS setups and show why they work. The more detailed explanation about the backing concepts I relinquish to the excellent MDN documentation if you’d like to dig deeper.
What is it
For security reasons a browser denies frontend JavaScript loaded from origin A (i.e. http://dogpics.com
) to embed resources which have to be fetched from a different origin B (i.e. http://catpics.com
). That is in essence what is normally refered to as same-origin-policy.
To bypass that restriction (but still prevent the inadvertent usage of resources) origin A and origin B can agree on a deliberate exchange of resources. They do this by defining a Cross-Origin Resource Sharing strategy (or CORS for short). When done properly the scenario described above becomes possible again and frontend JavaScript loaded via http://dogpics.com
can also embed resources from http://catpics.com
.
When do I need it
Well, whenever your frontend JavaScript tries to fetch resources from different origins according to the same-origin-policy. In other words, it applies if origin B differs in either the protocol, the domain or the port. So for our origin A http://dogpics.com
this means:
origin B | Same Origin | Why |
---|---|---|
https://dogpics.com | nope | protocol differs |
http://dogpics.com/cat.png | yes | only the path is different - the origin is the same |
http://api.dogpics.com | nope | domain is different (yes, this includes sub-domains) |
http://dogpics.com:8443 | nope | port is different |
CORS headers and their effect
Basically, it’s the origin B server which determines who is able to embed its resources by using a bunch of HTTP-headers. Nevertheless, for some use-cases the requesting party (here the frontend JavaScript of origin A) has to be adjusted too. Also depending on the use-case is the choice of CORS-headers to be applied. Here’s a short recap of the most important CORS-headers (again, details can be found here) by which origin B can control who else can fetch its resources:
Access-Control-Allow-Origin
: can only be set to one specific origin which is then allowed to fetch from this server. It can also be set to wildcard'*'
but this value is mutually exclusive to"include"
credentials mode (which makes it useless when dealing with auth-cookies).Access-Control-Allow-Credentials
: When a request’s credentials mode (Request.credentials
) is"include"
, browsers will only expose the response to frontend JavaScript code if theAccess-Control-Allow-Credentials
value istrue
(see here for more details)Access-Control-Allow-Methods
: all allowed methods which can be used in the actual fetch callAccess-Control-Allow-Headers
: Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request. Also allows wildcard'*'
but again doesn’t work with auth-cookies/header etc.
Examples of CORS requests
Example 1 (without auth):
Situation:
We’d like to fetch public (no authentication is necessary) JSON-content using a GET-request. Since we request Content-Type: application/json
this will cause a preflight request before the actual GET-request is issued (find details here about the difference between simple requests and preflight requests).
Request:
const config = { "headers": {"Content-Type": "application/json"} };
fetch(url, config).then(response => response.json());
Header-Setup:
Access-Control-Allow-Origin: '*'
Access-Control-Allow-Headers: 'content-type'
Access-Control-Allow-Methods: 'GET'
Result:
- Since we don’t have to deal with authentication whatsoever we can use the
'*'
-value forAccess-Control-Allow-Origin
(Notice: the fact that it works doesn’t mean that it’s a good idea to allow access for the whole world). - Since we want to use HTTP-GET we have to allow that method using
Access-Control-Allow-Methods: 'GET'
- And finally, since we want the server to return a JSON-representation of the resource we have to allow the
Content-Type
header to be used.
Example 2 (with Cookies):
Situation:
For whatever reason the server of origin B needs its cookies to be included in the request (i.e. session-cookie).
Request:
const config = {
"credentials": "include", // makes sure that cookies will be sent
"headers": {"Content-Type": "application/json"}
};
fetch(url, config).then(response => response.json());
Header-Setup:
Access-Control-Allow-Origin: 'http://localhost:8081'
Access-Control-Allow-Credentials: 'true'
Access-Control-Allow-Headers: 'content-type'
Access-Control-Allow-Methods: 'GET'
Result:
The usage of cookies has a bunch of implications for the setup to work:
- first, the client has to use
include
credentials mode to make sure the browser includes the origin B cookies when issuing the GET-request. - consequently, origin B has to set the CORS-header
Access-Control-Allow-Credentials
totrue
- if that is not set (or set tofalse
), it won’t work. - when using
Access-Control-Allow-Credentials:true
the wildcard'*'
forAccess-Control-Allow-Origin
is not applicable anymore - hence we have to be explicit about who actually is origin A.
Example 3 (with Auth-Tokens):
Situation:
When your authentication setup doesn’t use cookies but auth-headers you strangly don’t have to use the credentials: include
mode (although you can of course).
Request:
const config = {
"headers": {
"Content-Type": "application/json",
"Authorization": "Basic SXQncyBub3Qgd29yayB0aGUgZWZmb3J0IGR1ZGU="
}
};
fetch(url, config).then(response => response.json());
Header-Setup:
Access-Control-Allow-Origin: '*'
Access-Control-Allow-Credentials: 'false'
Access-Control-Allow-Headers: 'content-type, authorization'
Access-Control-Allow-Methods: 'GET'
Result:
This is basically the same setup as in Example 1 except that we explicitly allow the Authorization
header as well.
Notice: In those cases (with or without credentials-mode) make sure that you use a TLS-connection.
Test it out yourself
When you’d like to test this yourself head over to this repository in order to setup a simple 2-server constellation where one origin (http://localhost:8081
) is embedding a resource of the other one (http://localhost:8080
). Using the UI you can tweak the server-headers and the config for the fetch-request.
In the server-logs you can inspect what headers and cookies are transmitted by the CORS-request (and how OPTIONS- a.k.a. preflight- and GET-request differ).