An Interest In:
Web News this Week
- March 25, 2024
- March 24, 2024
- March 23, 2024
- March 22, 2024
- March 21, 2024
- March 20, 2024
- March 19, 2024
Understanding Cross-Origin Resource Sharing (CORS)
Cross-Origin Resource Sharing (CORS) is a way of making HTTP requests from one place to another. Historically browsers have only allowed requests in JavaScript to be made from the same domain enforced by the same-origin policy which prevents cross-origin type of requests.
CORS gives the server authority of who can make requests and what type of requests are allowed. Browsers are the clients that enforce CORS policies.
The server can configure:
- Which domains are allowed to make the HTTP request
- Which HTTP methods are allowed (GET, POST, PUT, DELETE, etc).
- Which headers are allowed on the request.
- Whether or not requests may include cookie information.
- Which response headers the client can read.
Intro TLDR;
CORS allows for restricted resources on a server to be accessed by a client on a different host.
We'll be covering all that things that you need to know about CORS beyond Access-Control-Allow-Origin: *
in this tutorial. Yes there's a lot to learn!
Same-Origin vs Cross-Origin
For simplification purposes, a same-origin request is like 2 people in a house communicating.
There's no communication barrier between the Alice and Bob since they are in the same house (the same origin).
VS
A cross-origin request is like having 2 different houses and a person from one house communicating from a person on the other house.
Since Bob wants to communicate with Charlie who lives in a different house then it's considered cross-origin communication because the homes live in different origins. And since Bob is initiating the call then the house Charlie is in needs to approve Bobs call.
TLDR;
A same-origin request is when the request is made from a host to the same host while a cross-origin request is when the request is made from a host to a different host.
Same-origin request
As an example, let's first make a same-origin request. We'll create a simple server offering an endpoint for the client and serving an HTML page. Throughout this this tutorial we'll be using Node.js and the express server for simplicity sake.
File server.js
const express = require('express')const app = express()const port = 8000const posts = [ {id: 1, content: 'foo'}, {id: 1, content: 'bar'},]app.use(express.static(__dirname))app.get('/api/posts', (req, res) => { res.json(posts)})app.listen(port, () => { console.log(`listening on port ${port}`)})
File index.html
<!DOCTYPE html><html lang="en-US"><head> <meta charset="UTF-8"> <title></title></head><body> <script> (async () => { const res = await fetch('http://localhost:8000/api/posts') console.log(await res.json()) })() </script></body></html>
$ node server.jslistening on port 8000
If you visit http://localhost:8000/ you can see the request was successful. Nothing crazy and expected because it's from the same origin.
Cross-origin request
Now to show a cross-origin request let's spin up a new server to server the HTML file listening on a different port.
File server2.js
const express = require('express')const app = express()const port = 9000app.use(express.static(__dirname))app.listen(port, () => { console.log(`listening on port ${port}`)})
$ node server.jslistening on port 8000
$ node server2.jslistening on port 9000
Let's see what happens when we visit the second server's webpage at http://localhost:9000/
The browser threw the error:
Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: 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.
To demonstrate why this happened, let's see how the browser and server communicate with a simple chat example.
Browser: "hello server http://127.0.0.1:8000 please give me the data at /api/posts and let me know if the client at http://localhost:9000 can access it. Here's the HTTP message:"
Get /api/posts HTTTP1.1User-Agent: ChromeHost: 127.0.0.1:8000Accept: */*Origin: http://localhost:9000
Server: "here is the data, and the client cannot have access because I don't have the origin http://localhost:9000 as an allowed origin to access the data."
HTTP/1.1 200 OK
The server responds with the data but the browser will not give it to the JavaScript client unless the server says that the client is allowed based on their origin.
What server has to do to allow cross-origin requests is set the Access-Control-Allow-Origin
response header.
So let's try again by setting up CORS on the server now by adding a new header to every request. We'll be adding the Access-Control-Allow-Origin
with a wildcard *
value which tells the browser that any origin can access the resource.
File server.js
const express = require('express')const app = express()const port = 8000const posts = [ {id: 1, content: 'foo'}, {id: 1, content: 'bar'},]app.use(express.static(__dirname))app.use((req, res, next) => { res.set('Access-Control-Allow-Origin', '*') next()})app.get('/api/posts', (req, res) => { res.json(posts)})app.listen(port, () => { console.log(`listening on port ${port}`)})
Restart the server:
$ node server.jslistening on port 8000
$ node server2.jslistening on port 9000
Now let's see what happens when we visit the second server's webpage at http://localhost:9000/
To demonstrate why it works now, let's see how the browser and server communicate with a simple chat example again.
Browser: "hello server http://127.0.0.1:8000 please give me the data at /api/posts and let me know if the client at http://localhost:9000 can access it. Here's the HTTP message:"
Get /api/posts HTTTP1.1User-Agent: ChromeHost: 127.0.0.1:8000Accept: */*Origin: http://localhost:9000
Server: "here is the data, and yes any client can have access:"
HTTP/1.1 200 OKAccess-Control-Allow-Origin: *
Access-Control-Allow-Origin
The most important CORS headers are:
- The
Origin
request header - The
Access-Control-Allow-Origin
response header
The origin is the host which is composed of the protocol (http, https, etc), hostname (example.com, localhost, etc), and port (80, 443, 8000, etc)
Origin = protocol + hostname + port
The value null
can be used a valid value for Origin
when the browser origin can't be determined such as when using an absolute file path like file:///Users/alice/wwww/index.html
Browsers don't allow changing the origin header otherwise the client can pretend to be someone else.
The value of the Access-Control-Allow-Origin
header can either be a wildcard or an origin value.
Example of valid headers:
Access-Control-Allow-Origin: *Access-Control-Allow-Origin: http://localhost:8000Access-Control-Allow-Origin: http://example.comAccess-Control-Allow-Origin: null
You can only have one value so having multiple values like this is not valid:
# Not valid values, can't have multiple!Access-Control-Allow-Origin: http://localhost:8000, http://example.com
TLDR;
Use the
Access-Control-Allow-Origin
header to tell the browser which origins are allowed to access the data.
Preflight requests
For certain HTTP methods, the browser asks for permissions by doing what is known as a preflight request. If the browser approves the response from the preflight request then the actual request is made. If the browser doesn't approve the response from the preflight request then the actual request is never made.
HTTP methods that trigger a preflight request are:
PUT
PATCH
DELETE
TRACE
HTTP methods that don't trigger a preflight request are known as simple requests which are:
HEAD
GET
POST
A preflight request is a small request to the server from the browser before the main request and contains information such as which HTTP method is used and if any http headers are present. The server determines if the browser should send the actual request by returning a 2xx HTTP status code or return an error indicating that the client should not send the actual request.
The preflight request protects servers from receiving cross-origin requests they may not want. If a server is CORS-enabled it will know how to handle a preflight request and can respond accordingly. If the server doesn't understand or care about CORS then the server won't send the correct preflight response.
The preflight request uses the OPTIONS
HTTP method.
A preflight is triggered when:
The client request is using a method other than
GET
,POST
orHEAD
(which are known as simple methods)Client sets the
Content-Type
request header with values other than:application/x-www-form-urencoded
multipart/form-data
text/plain
Client sets additional request headers that are not:
Accept
Accept-Language
Content-Language
Preflight responses should be in the 200
range (HTTP code 204 is standard) and contain no body. Containing a body can also confuse developers.
Let make a DELETE
request to demonstrate a preflight request. First we'll check if the request is a preflight request. And if it is then return the HTTP 204 No Content
status code.
File server.js
const express = require('express')const app = express()const port = 8000const posts = [ {id: 1, content: 'foo'}, {id: 1, content: 'bar'},]const isPreflight = (req) => { return ( req.method === 'OPTIONS' && req.headers['origin'] && req.headers['access-control-request-method'] )}app.use(express.static(__dirname))app.use((req, res, next) => { res.set('Access-Control-Allow-Origin', '*') if (isPreflight(req)) { res.status(204).end() return } next()})app.get('/api/posts', (req, res) => { res.json(posts)})app.delete('/api/posts', (req, res) => { res.json({success: true})})app.listen(port, () => { console.log(`listening on port ${port}`)})
File index.html
<!DOCTYPE html><html lang="en-US"><head> <meta charset="UTF-8"> <title></title></head><body> <script> (async () => { const res = await fetch('http://localhost:8000/api/posts', { method: 'DELETE' }) console.log(await res.json()) })() </script></body></html>
Restart the server:
$ node server.jslistening on port 8000
$ node server2.jslistening on port 9000
Now let's see what happens when we visit the second server's webpage at http://localhost:9000/
We can see the error saying that the method DELETE
is not allowed by CORS:
Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.
Remember, that simple requests such as GET
, POST
, or HEAD
don't initiate a preflight request and they are automatically allowed methods by CORS but methods such as DELETE
or PUT
need to be explicit allowed by the server.
To allow the DELETE
method to be performed by the client we'll need to add the response header Access-Control-Allow-Methods
in the preflight request.
app.use((req, res, next) => { res.set('Access-Control-Allow-Origin', '*') if (isPreflight(req)) { res.set('Access-Control-Allow-Methods', 'DELETE') // Add this! res.status(204).end() return } next()})
And now the the DELETE
request works as expected:
Notice the different number of requests made in the two screenshots.
- In the first screenshot, the preflight request failed so the actual
DELETE
request was never made, creating only 1 request.
- In the second screenshot, the preflight request succeeded so the actual
DELETE
request was made, creating 2 requests.
To reject a preflight request:
- Leave out the
Access-Control-Allow-Origin
header. - Return a value in
Access-Control-Allow-Methods
that doesn't matchAccess-Control-Request-Method
header. - If preflight request has an
Access-Control-Request-Header
- Leave out the
Access-Control-Allow-Headers
header. - Return a value in the
Access-Control-Allow-Headers
header that doesn't match the Access-Control-Request-Headers header.
- Leave out the
Both preflight response and actual response need the Access-Control-Allow-Origin
header.
The browser may cache the preflight response from the first request to a resource from that origin to avoid sending additional queries all the time.
Preflights are also stateless, meaning that the actual request doesn't contain any information connecting it to the preflight request.
TLDR;
A preflight request determines if the client is allowed to make the actual request based on what the server allows. The request must be an
OPTIONS
method, have theAccess-Control-Request-Method
header (such asDELETE
orPUT
), and contain theOrigin
header to be considered a preflight request.
Access-Control-Request-Method
The Access-Control-Request-Method
is a single request header value that asks permission to use a specific HTTP method. This header is set by the browser when the client sends a non-simple method request.
The Access-Control-Request-Method
is only sent in the preflight request.
This screenshot shows the Access-Control-Request-Method
request header in the preflight request and shows the response header Access-Control-Allow-Methods
in the preflight response from when we did the DELETE
request earlier.
TLDR;
The browser will send the
Access-Control-Request-Method
request header in the preflight request to let the server know it's intending to use the requested method in the actual request. If the server allows that method, then the browser will make the actual request.
Access-Control-Allow-Methods
The Access-Control-Allow-Methods
is used in preflight requests to tell the client which methods are allowed with CORS.
For example, having the Access-Control-Allow-Methods: DELETE
header indicates the server allows the client to make DELETE
requests to the URL.
The Access-Control-Allow-Methods
request header can have multiple values, for example:
Access-Control-Allow-Methods: HEAD, GET, POST, PUT, DELETE
Remember that GET
, POST
, and HEAD
are simple methods and always allowed so having them in the header is redundant and unnecessary, but some people like having them in the header because it's more to clear to them and avoids confusion.
Example showing PUT and DELETE (in addition to simple methods) as allowed:
TLDR;
The server should respond with the
Access-Control-Allow-Methods
response header to let the browser know which HTTP methods the client is allowed to make.
Access-Control-Request-Headers
The Access-Control-Request-Headers
request header is sent in the preflight request to let the server know which headers the client will be sending in the actual response.
Let's try to send a custom header to the server.
File index.html
<!DOCTYPE html><html lang="en-US"><head> <meta charset="UTF-8"> <title></title></head><body> <script> (async () => { const res = await fetch('http://localhost:8000/api/posts', { headers: new Headers({ 'My-Custom-Header': 'hello world' }) }) console.log(await res.json()) })() </script></body></html>
Now let's see what happens when we visit the second server's webpage at http://localhost:9000/
The browser responds with the error:
Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: Request header field my-custom-header is not allowed by Access-Control-Allow-Headers in preflight response.
Since browsers enforce CORS policies, the headers that cannot be set by JavaScript are:
Accept-Charset
Accept-Encoding
Access-Control-Request-Headers
Access-Control-Request-Method
Connection
Content-Length
Cookie
Cookie2
Date
DNT
Expect
Host
Keep-Alive
Origin
Referer
TE
Trailer
Transfer-Encoding
Upgrade
User-Agent
Via
- headers starting with
Proxy-
orSec-
These headers can only be set by the browser because they have special meaning. The browser will simply ignore the value that you set for those headers.
For other headers, the server will have to give permission to the client to include custom request headers on the cross-origin request.
We can see that the browser asked permission to access the custom header in the preflight request with the header Access-Control-Request-Headers
:
The server needs to whitelist approved custom request headers and if doesn't whitelist the request headers then the request will fail.
If doing a same-origin request, then the request can include any custom request header since the origin is trusted.
By default CORS only allows the client to read these response headers:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
If the server sets additional response headers then the client won't be able to see them. In order for the client to see additional response headers then the server to expose those headers which we'll go over in a bit.
So to have the server be able to accept the client's custom header, it needs to explicitly allow that header by setting the response header Access-Control-Allow-Headers
in the preflight response.
const express = require('express')const app = express()const port = 8000const posts = [ {id: 1, content: 'foo'}, {id: 1, content: 'bar'},]const isPreflight = (req) => { return ( req.method === 'OPTIONS' && req.headers['origin'] && req.headers['access-control-request-method'] )}app.use(express.static(__dirname))app.use((req, res, next) => { res.set('Access-Control-Allow-Origin', '*') if (isPreflight(req)) { res.set('Access-Control-Allow-Methods', 'DELETE') res.set('Access-Control-Allow-Headers', 'My-Custom-Header') // Add this! res.status(204).end() return } next()})app.get('/api/posts', (req, res) => { res.json(posts)})app.delete('/api/posts', (req, res) => { res.json({success: true})})app.listen(port, () => { console.log(`listening on port ${port}`)})
Restart the server:
$ node server.jslistening on port 8000
$ node server2.jslistening on port 9000
Now let's see what happens when we visit the second server's webpage at http://localhost:9000/
The preflight responds with the allowed headers so the browser proceeds to make the actual request sending the custom request header.
Remember that headers starting with Access-Control-Request-
are request headers by the browser asking for permission by the server and headers starting with Access-Control-Allow-
are response headers sent by the server granting permissions to the browser.
TLDR;
The browser sends a
Access-Control-Request-Headers
request header in the preflight request to let the server know that the actual request will request the specified headers. If the server denies those headers to be requested, then the actual request will not be sent.
Access-Control-Expose-Headers
The Access-Control-Allow-Headers
header is used by the preflight to indicate which headers are allowed on the request while the Access-Control-Expose-Headers header
is used by the actual response to indicate which response headers are visible to the client.
The client won't be able to read a a response header if the server doesn't set in the expose header.
Headers that are always exposed to the client are simple headers which are:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
As an example let's try to read all the response headers on the client:
File index.html
<!DOCTYPE html><html lang="en-US"><head> <meta charset="UTF-8"> <title></title></head><body> <script> (async () => { const res = await fetch('http://localhost:8000/api/posts') console.log(Array.from(await res.headers.entries())) })() </script></body></html>
Now let's see what happens when we visit the second server's webpage at http://localhost:9000/
Nothing surprising yet, we get back standard headers.
Let's set a custom response header on the server side.
const express = require('express')const app = express()const port = 8000const posts = [ {id: 1, content: 'foo'}, {id: 1, content: 'bar'},]const isPreflight = (req) => { return ( req.method === 'OPTIONS' && req.headers['origin'] && req.headers['access-control-request-method'] )}app.use(express.static(__dirname))app.use((req, res, next) => { res.set('Access-Control-Allow-Origin', '*') //res.set('Access-Control-Expose-Headers', 'Timezone-Offset') if (isPreflight(req)) { res.set('Access-Control-Allow-Methods', 'DELETE') res.set('Access-Control-Allow-Headers', 'My-Custom-Header') res.status(204).end() return } else { res.set('Timezone-Offset', '240') // Add this! } next()})app.get('/api/posts', (req, res) => { res.json(posts)})app.delete('/api/posts', (req, res) => { res.json({success: true})})app.listen(port, () => { console.log(`listening on port ${port}`)})
Now let's see what happens when we visit the second server's webpage at http://localhost:9000/
Wait, we just see the same headers. The new header wasn't provided. Well this is because the server needs to set which headers are allowed to be read by the client with the Access-Control-Expose-Headers
response header.
Let's add Access-Control-Expose-Headers
as a regular response header:
server.js
const express = require('express')const app = express()const port = 8000const posts = [ {id: 1, content: 'foo'}, {id: 1, content: 'bar'},]const isPreflight = (req) => { return ( req.method === 'OPTIONS' && req.headers['origin'] && req.headers['access-control-request-method'] )}app.use(express.static(__dirname))app.use((req, res, next) => { res.set('Access-Control-Allow-Origin', '*') if (isPreflight(req)) { res.set('Access-Control-Allow-Methods', 'DELETE') res.set('Access-Control-Allow-Headers', 'My-Custom-Header') res.status(204).end() return } else { res.set('Access-Control-Expose-Headers', 'Timezone-Offset') // Add this! res.set('Timezone-Offset', '240') } next()})app.get('/api/posts', (req, res) => { res.json(posts)})app.delete('/api/posts', (req, res) => { res.json({success: true})})app.listen(port, () => { console.log(`listening on port ${port}`)})
Restart the server:
$ node server.jslistening on port 8000
$ node server2.jslistening on port 9000
Now let's see what happens when we visit the second server's webpage at http://localhost:9000/
We can now read the custom header the server sent to the browser in the client.
TLDR;
Use
Access-Control-Expose-Headers
to allow the client to read additional non-simple headers.
Access-Control-Max-Age
The Access-Control-Max-Age
header indicates how long in seconds a response should stay cached.
if (isPreflight(req)) { res.set('Access-Control-Allow-Methods', 'GET, DELETE') res.set('Access-Control-Max-Age', '120') // cache for 2 minutes res.status(204).end() return }
Firefox doesn't allow items to be cached longer than 24 hours while Chrome, Opera, and Safari cache items for a maximum of 5 minutes. If the Access-Control-Max-Age
ins't specified then Firefox doesn't cache the preflight while Chrome, Opera, and Safari cache the preflight for 5 seconds
Access-Control-Allow-Credentials
Cookies require additional configuration for security and safety reason because they contain sensitive information. It would be dangerous to accidentally send personal information to a website on a different origin if opt-in was enabled all the time.
The JavaScript document.cookie
API cannot read or write the value from another origin. Calling document.cookie
only returns the clients own cookies and not the cross-origin cookies. Cookies themselves use the same-origin policy and each cookie has a path and a domain and only pages that match the path and domain can read the cookie.
Cookies work best in situations where:
- You want to authorize users within your own ecosystem of clients and servers.
- You know exactly which clients will be accessing your server.
To enable cookie support the server must have the Access-Control-Allow-Credentials
header set to true in order to indicate that the server can receive cookies.
app.use((req, res, next) => { res.set('Access-Control-Allow-Origin', '*') res.set('Access-Control-Allow-Credentials', 'true') if (isPreflight(req)) { res.set('Access-Control-Allow-Methods', 'GET, DELETE') res.status(204).end() return } next() })
The Access-Control-Allow-Credentials
header be present on both the preflight and actual request but the cookie will only be sent on the actual request.
In the JavaScript fetch call you'll need to set withCredentials: true
to send cookies on cross-origin requests
Websites identify users through user credentials, with the most popular form being the cookie. Servers use cookies to store a unique indentifier that identifies the use such as a session ID tied to a user ID.
The same-origin HTTP request will always contain the cookie but cross-origin doesn't contain cookie by default.
Use withCredentials
to send:
- Cookies
- Basic authentication
- Client-side SSL certs
Browser support
CORS is fully supported on:
- Chrome 3+
- Firefox 3.5+
- Safari 4+
- IE 10+
- Opera 12+
- iOS 3.2+
- Android 2.1+
Conclusion
We covered a variety of headers around CORS, including:
Access-Control-Allow-Origin
Access-Control-Request-Method
Access-Control-Request-Headers
Access-Control-Allow-Headers
Access-Control-Allow-Methods
Access-Control-Expose-Headers
Access-Control-Allow-Credentials
Access-Control-Max-Age
Questions to ask when adding CORS support:
- Why does the server need to support cross-origin requests?
- Is CORS being added to new service or existing?
- Which client should have access to the site?
- What browsers and devices will users be accessing the site from?
- Which HTTP methods and headers is the server supporting?
- Should the API support user-specific data?
- Will cookies be used to authenticate the user?
Resources:
Original Link: https://dev.to/miguelmota/understanding-cross-origin-resource-sharing-cors-2i3e
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To