CORS in Practice
Introduction
What is the need ?
All modern browsers implement the same-origin policy as a critical security mechanism that restricts how a document or script loaded by one origin can interact with a resource from another origin. Given this policy and the fact that web applications often make cross-origin requests (meaning requesting resources from another origin), a mechanism is required for allowing cross-origin requests in a secure way. The most popular mechanism used in this scope is the Cross-Origin Resource Sharing (CORS) mechanism that this codelab will present and that you will practice with a number of examples.
What you’ll build
You will first learn how to build client and server applications that follow the CORS mechanism and rules. In this codelab, you will learn the principles behind CORS and how to develop client and server applications that apply properly the CORS rules.
What you’ll learn
- The same-origin policy and the CORS mechanism.
- The integration of the CORS mechanism into your COVID express application.
- The proper use of the CORS mechanism for the fetch API.
What you’ll need
- WebStorm.
- The Node.js framework.
- The Node.js and OpenAPI codelab is a prerequisite for this codelab.
The Same-Origin Policy and the CORS Mechanism
The same-origin policy is a policy that restricts the ability for a website to interact with resources outside of the source domain. It is a protection against a (malicious) website stealing private data from another. It generally allows a domain to issue requests to other domains, but not to access the responses.
You can find a detailed description of the Same-Origin policy under MDN - Same-origin policy and under Same-origin policy. From these descriptions, you should understand why such a policy is required. You should also understand why cross-origin requests are necessary in some cases and why a mechanism is required for managing those requests while implementing safe client and server applications.
The most popular mechanism used for managing cross-origin requests is the Cross-Origin Resource Sharing (CORS) mechanism. A detailed description of the CORS mechanism is available under Cross-Origin Resource Sharing (CORS) - HTTP | MDN and Cross-Origin Resource Sharing (CORS).
The CORS sharing protocol uses HTTP headers for defining trusted web origins, together with associated properties such as whether authenticated access is permitted. HTTP headers are exchanged between the browser and the cross-origin web site that it is trying to access. Thanks to the headers, the cross-origin website will understand the origin of the request and the browser may determine whether the response may be accessed.
CORS by Examples
The CORS mechanism can be better understood with some examples. A typical scenario where CORS is required is when a main page delivered from a given origin is requesting resources from another origin. This is a quite common use case and it is easy to demonstrate. For this purpose, we create a simple client application that loads a resource from another origin once the main page is loaded. The code of the client application is shown below:
index_same_origin.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<div id="content"> </div>
</head>
<body>
<!-- script -->
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js">
</script>
<script>
// function executed when the document is ready (fully loaded)
$(document).ready(function () {
function status(response) {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response.json());
} else {
return Promise.reject(new Error(response.statusText));
}
}
function showError(error) {
alert(`Sorry, there was a problem: ${error}`);
}
// Important notice:
// CORS failures result in errors, but for security reasons, specifics
// about the error are not available to JavaScript. All the code knows is
// that an error occurred.
// The only way to determine what specifically went wrong is to look at the
// browser's console for details.
// see https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
// for details about the mode
// if you change the mode, the expected result is:
// 1. same-origin: we simply get an error since the request is not from
// the same origin (both localhost, but different ports)
// > This means that the browser generates the error without fetching
// > from the server.
// > You can observe this using the network debugger (no request is
// > made to localhost)
// > You can also observe that no log is added to the server console
// > this is used for forcing requests to your origin only
// 2. no-cors: you get an opaque response
// (see https://fetch.spec.whatwg.org/#concept-filtered-response-opaque)
// > Opaque responses are of no use for an application since they cannot
// > be accessed by JavaScript.
// > Opaque responses can be cached and this mechanism is used for
// > caching purposes.
// 3. cors: this allows cross origins requests. you get a response that
// depends on the server
// > the request header always contains an
// origin:scheme:://host:port property
// - the server may respond with Access-Control-Allow-Origin: *,
// which means that the resource can be accessed by any domain
// - the server (resource owner) may wish to restrict access to the
// resource to requests only from https://foo.example, they would
// send: Access-Control-Allow-Origin: https://foo.example.
// And in our case, we would get an error
const baseUrl = "http://localhost/demo.json";
fetch(baseUrl, {
method: 'GET', // *GET, POST, PUT, DELETE, etc.
mode: 'same-origin', // no-cors, cors, same-origin
headers: {
'Accept': 'application/json'
}
})
.then(response => status(response))
.then(response => {
console.log(response);
document.getElementById("content").textContent = JSON.stringify(response);
})
.catch(error => showError(error));
});
</script>
</body>
</html>
The example above makes use of the Fetch API - Web APIs | MDN, which is one of the most popular Web API for fetching resources across the network in web applications. It is similar to the XMLHttpRequest - Web APIs | MDN mechanism, but the Fetch API is more powerful and allows to work with Promises as you can see in the example above.
The server application that serves the demo.json resource can be implemented very easily as an Express application. For simplicity, the code of the Express application is shown below:
http_without_cors.js
const express = require('express');
const resources = require('../demo.json');
const app = express();
app.get('/demo.json', function (req, res, next) {
res.status(200);
res.contentType('application/json');
res.send(resources);
});
module.exports = app;
Requests from The Same Origin
If you add a demo.json file in the root directory of your application and run both the client and server applications, you should observe a popup windows alerting for an error:
As you can observe from the alert window above, CORS failures result in errors, but for security reasons, specifics about the errors are not available to the JavaScript code. The only thing that the JavaScript code knows is that an error occurred and for the developer, the only way to determine what specifically went wrong is to look at the browser’s console for details, as shown below for this particular case:
If you open the Network tab of the Browser debug tools, you should observe that the browser did not even make a request to the network:
In other words, while executing the fetch()
call, the browser identified that
the fetch request was made in the ‘same-origin'
mode and that the resource to
be fetched was from another origin. It thus immediately rejects the request.
Note that the index.html page is served from an origin like
“http://localhost:63343” while the origin of the demo.json is ”
http://localhost”. Two origins are considered identical when
the scheme, the host and the port are the same. For demonstrating that a
resource could be delivered from the same origin, you may modify the baseUrl
value in the index.html file to use the same port as the origin of the
index.html file (e.g. “63343” in this example). In this case, the resource will
be delivered properly by the server upon fetch()
:
Requests From A Different Origin
As explained above, fetching resources to a different origin is often a
requirement. In this case, the fetch request should not be made using the
'same-origin'
mode but rather using the 'cors'
mode. By using the 'cors'
mode, the browser will execute the fetch request with the CORS mechanism
enabled. If you modify the mode to 'cors'
in the index.html file, you should
still observe a CORS error in the browser and the following result should be
displayed in the network tab:
As you can read above, the browser has added an Origin
header in the request headers
sent to the server. This header follows the Origin:scheme:://host:port
syntax.
The request is received by the server and the response is sent back from the
server to the client. However, the server has not added the required headers in
the response and the fetch()
request thus results in an error.
CORS in Node.js
For handling the CORS mechanism properly in Node.js mechanism, one very popular module is the cors module. This module allows you to very easily configure the CORS mechanism and to use it as a middleware in your Express application. For using it, you must modify your “http.js” file as follows:
http_cors_basic.js
const express = require('express');
const resources = require('../demo.json');
const cors = require('cors');
const app = express();
app.options(cors());
app.use(cors());
app.get('/demo.json', function (req, res, next) {
res.status(200);
res.contentType('application/json');
res.send(resources);
});
module.exports = app;
If you now run both the client and server applications, the fetch() call should
now
succeed and you should observe the following headers in the response from the
server:
The server has now added an Access-Control-Allow-Origin
header in the response
with the value *
. This tells the browser that the server is allowing requests
from all origins and the browser can thus use the response in the JavaScript
code.
Preflight requests
Although simple
requests
don’t trigger CORS preflight
requests,
it is likely that the browser will trigger preflight requests, even for
requests that looks like simple requests (GET
, Accept
header). Preflight
requests is an OPTIONS
request
and it is issued by the client to check if the CORS protocol is understood
by the server. For handling preflight requests properly in your Node.js
application, you need to add a call to app.options(cors())
next to your
app.use(cors())
call. The options passed to the cors()
module should be
the same as for the app.use(cors())
call.
Allowing all origins for a request is of course not appropriate in many cases. In general, origins may be restricted and this can be done in the Express application by configuring CORS. An example of configuration is shown below:
http_cors_basic_options.js
const express = require('express');
const resources = require('../demo.json');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: 'http://localhost:63347'
};
app.options(cors(corsOptions));
app.use(cors(corsOptions));
app.get('/demo.json', function (req, res, next) {
res.status(200);
res.contentType('application/json');
res.send(resources);
});
module.exports = app;
With this change, the response headers should be:
The Access-Control-Allow-Origin
header in the response has now the value
specified using the corsOptions
in the Express application. Of course,
configuring CORS in this way does not bring the required flexibility. For this
reason, the origin
property of the corsOptions
can also be set to other
values like:
RegExp:
this allows to set the origin to a regular expression pattern which will be used to test the request origin.Array:
this sets the origin to an array of valid origins.Function:
this is the most flexible option and this allows to set the origin to a function implementing some custom logic.
It is also possible to delegate the CORS mechanism to a function and to check whether a request should be accepted asynchronously:
http_cors_delegate.js
const express = require('express');
const resources = require('../demo.json');
const cors = require('cors');
const app = express();
const corsOptionsDelegate = (req, callback) => {
let corsOptions = { origin: false};
// check whether the origin of the request is accepted
// if yes, corsOptions = { origin: true };
// otherwise, corsOptions = { origin: false };
const urlObject = new URL(req.get('origin'));
const hostName = urlObject.hostname;
if (hostName === "localhost") {
corsOptions.origin = true;
}
// the callback expects two parameters: error and options
callback(null, corsOptions)
};
app.options(cors(corsOptionsDelegate));
app.use(cors(corsOptionsDelegate));
app.get('/demo.json', function (req, res, next) {
res.status(200);
res.contentType('application/json');
res.send(resources);
});
module.exports = app;
Please note also that the CORS mechanism can also be applied with different options for different routes. This can be done by adding CORS options when mounting a route like in:
app.get('/planets', cors(corsOptionsDelegate), (req, res, next) => {});
Specifying accepted origins using Regular Expressions
Another common way of specifying accepted cross-origins is through the use of regular expressions, as demonstrated below:
http_cors_regexp.js
const express = require('express');
const resources = require('../demo.json');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: /:\/\/localhost\:[0-9]{4}$/
};
app.options(cors(corsOptions));
app.use(cors(corsOptions));
app.get('/demo.json', function (req, res, next) {
res.status(200);
res.contentType('application/json');
res.send(resources);
});
module.exports = app;
One has to be very careful when using regular expressions for specifying accepted cross-origins. Say that you want access to
heia-fr.ch
hackerheia-fr.ch
heia-fr.ch.hacker.net
This is why it is extremely important to gain a good understanding of how to build proper regular expressions in JavaScript.
Regular Expressions in JavaScript
Regular expressions are important for developing JavaScript applications and also when developing Express Node.js applications. They allow to perform string matching, that are useful for defining routes for instance.
Regular expressions are presented as one lesson using WebStorm EduTools. They are important for routing in Express applications. In this codelab, we apply them to the CORS mechanism.
For testing and understanding regular expression, you can also use the interactive interface. This application explains in all details how a text is matched with a regular expression. A debugger is available for understanding for instance why a match does not occur.
Deliverables
-
Implement the CORS mechanism such that requests are accepted for all origins that have the same scheme and the same host than the server delivering the requested resource. This means that the origin can have a different port. Implement this mechanism using a
RegExp
. -
Test that the behavior is correctly implemented by modifying the port used for debugging your html application (see instructions under Configuring JavaScript debugger | WebStorm).