In this article, we see how to enable CORS (Cross-Origin Resource Sharing) with HTTPOnly cookie to secure our access tokens.

Nowadays, backend servers and frontend clients are deployed on different domains. Therefore the server has to enable CORS to allow clients to communicate with the server on browsers.

Also, servers are implementing stateless authentication for better scalability. Tokens are stored and maintained on the client-side, but not on the server side like session. For security, it’s better to store tokens in HTTPOnly cookies.

Why are Cross-Origin requests blocked?

Let’s assume that our frontend application deployed at https://app.geekflare.com. A script loaded in https://app.geekflare.comcan only request same-origin resources.

Whenever we try to send a cross-origin request to another domain https://api.geekflare.com or another port https://app.geekflare.com:3000 or another scheme http://app.geekflare.com, the cross-origin request will get blocked by the browser.

But why the same request blocked by the browser be sent from any backend server using curl request or sent by using tools like the postman without any CORS problem. It’s actually for security to protect users from attacks like CSRF(Cross-Site Request Forgery).

Let’s take an example, suppose if any user logged in their own PayPal account in their browser. If we can send a cross-origin request to paypal.com from a script loaded on another domain malicious.com without any CORS error/blocking like we send the same-origin request.

Attackers can easily send their malicious page https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account  by converting it to short-URL to hide the actual URL. When the user clicks a malicious link, the script loaded in the domain malicious.com will send a cross-origin request to PayPal to transfer the user amount to the attacker PayPal account will get executed. All the users who have logged in to their PayPal account and clicked this malicious link will lose their money. Anyone can easily steal money without a PayPal account user knowledge.

For the above reason, browsers block all the cross-origin requests.

What is CORS(Cross-Origin Resource Sharing)?

CORS is a header-based security mechanism used by the server to tell the browser to send a cross-origin request from trusted domains.
The server enabled with CORS headers used to avoid cross-origin requests blocked by browsers.

How CORS works?

As the server already defined its trusted domain in its CORS configuration. When we send a request to the server, the response will tell the browser the requested domain is trusted or not in its header.

Two types of CORS requests are there:

  • Simple request
  • Preflight Request

Simple Request:

CORS-simple request flow tells that it sends a cross-origin request but when it received response. It checks for headers.

 

  • The browser sends the request to a cross-origin domain with origin(https://app.geekflare.com).
  • The server sends back the corresponding response with allowed methods and allowed origin.
  • After receiving the request, the browser will check the sent origin header value(https://app.geekflare.com) and received access-control-allow-origin value(https://app.geekflare.com) are the same or wildcard(*). Otherwise, It will throw a CORS error.

Preflight Request:

CORS-Preflight Request Image which show the flow of cross-origin request with OPTIONS preflight request before sending actual request for verifying headers.

  • Depending on the custom request parameter from the cross-origin request like methods(PUT, DELETE) or custom headers or different content-type, etc. The browser will decide to send a preflight OPTIONS request to check whether the actual request is safe to send or not.
  • After receiving the response(status code: 204, which means no content), the browser will check for the access-control-allow parameters for the actual request. If the request parameters are allowed by the server. The actual cross-origin request sent and received

If access-control-allow-origin: *,then the response is allowed for all origins. But it is not safe unless you need it.

How to enable CORS?

To enable CORS for any domain, enable CORS headers to allow origin, methods, custom headers, credentials, etc.

The browser reads the CORS header from the server and allows actual requests from the client only after verifying request parameters.

  • Access-Control-Allow-Origin: To specify exact domains(https://app.geekflate.com, https://lab.geekflare.com) or wildcard(*)
  • Access-Control-Allow-Methods: To allow the HTTP methods(GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) that only we need.
  • Access-Control-Allow-Headers: To allow only specific Headers(Authorization, csrf-token)
  • Access-Control-Allow-Credentials: Boolean value used to allow cross-origin-credentials(cookies, authorization header).
  • Access-Control-Max-Age: Tells the browser to cache the preflight response for some time.
  • Access-Control-Expose-Headers: Specify headers that are accessible by client-side script.

For enabling CORS in apache and Nginx webserver, follow this tutorial.

Enabling CORS in ExpressJS

Let’s take an example ExpressJS app without CORS:

const express = require('express');
const app = express()

app.get('/users', function (req, res, next) {
  res.json({msg: 'user get'})
});

app.post('/users', function (req, res, next) {
    res.json({msg: 'user create'})
});

app.put('/users', function (req, res, next) {
    res.json({msg: 'User update'})
});

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})

In the above example, we have enabled users API endpoint for POST, PUT, GET methods but not the DELETE method.

For easy enabling CORS in the ExpressJS app, you can install the cors

npm install cors

Access-Control-Allow-Origin

Enabling CORS for all domain

app.use(cors({
    origin: '*'
}));

Enabling CORS for a single domain

app.use(cors({
    origin: 'https://app.geekflare.com'
}));

If you want to allow CORS for origin https://app.geekflare.com and https://lab.geekflare.com

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ]
}));

Access-Control-Allow-Methods

For enabling CORS for all methods, omit this option in the CORS module in the ExpressJS. But for enabling specific methods(GET, POST, PUT).

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST']
}));

Access-Control-Allow-Headers

Used to allow headers other than defaults to send with actual requests.

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token']
}));

Access-Control-Allow-Credentials

Omit this if you don’t want to tell the browser to allow credentials on request even on withCredentials is set to true.

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true
}));

Access-Control-Max-Age

To intimate the browser to cache the preflight response information in the cache for a specified second. Omit this if you don’t want to cache the response.

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true,
    maxAge: 600 
}));

The cached preflight response will be available for 10 mins in the browser.

Access-Control-Expose-Headers

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true,
    maxAge: 600,
    exposedHeaders: ['Content-Range', 'X-Content-Range']
}));

If we put the wildcard(*) in exposedHeaders, it won’t expose the Authorization header. So we have to expose explicitly like below

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true,
    maxAge: 600,
    exposedHeaders: ['*', 'Authorization', ]
}));

The above will expose all headers and Authorization header too.

What is an HTTP cookie?

A cookie is a small piece of data that the server will send to the client browser. On later requests, the browser will send all the cookies related to the same domain on every request.

Cookie has its attribute, which can be defined to make a cookie work differently as we need.

  • Name Name of the cookie.
  • value: data of cookie respective to cookie-name
  • Domain:  cookies will be sent only to the defined domain
  • Path: cookies sent only after the defined URL prefix path. Suppose if we have defined our cookie path like path=’admin/’. Cookies not sent for the URL https://geekflare.com/expire/ but sent with URL prefix https://geekflare.com/admin/
  • Max-Age/Expires(number in second): When should the cookie expires. A lifetime of the cookie makes the cookie invalid after the specified time.
  • HTTPOnly(Boolean): The backend server can access that HTTPOnly cookie but not the client-side script when true.
  • Secure(Boolean): Cookies only sent over an SSL/TLS domain when true.
  • sameSite(string [Strict, Lax, None]): Used to enable/restrict cookies sent over on cross-site requests. To know more details about cookies sameSite see MDN. It accepts three options Strict, Lax, None. Cookie secure value set to true for the cookie configuration sameSite=None.

Why HTTPOnly cookie for tokens?

Storing the access token sent from the server in client-side storage like local storage, indexed DB, and cookie (HTTPOnly not set to true) are more vulnerable to XSS attack. Suppose if any one of your pages is weak to an XSS attack. Attackers may misuse user tokens stored in the browser.

HTTPOnly cookies are only set/get by server/backend but not on the client-side.

Client-side script restricted to access that HTTPonly cookie. So HTTPOnly cookies are not vulnerable to XSS Attacks and are more secure. Because it’s only accessible by the server.

Enable HTTPOnly cookie in CORS enabled backend

Enabling Cookie in CORS needs the below configuration in the application/server.

  • Set Access-Control-Allow-Credentials header to true.
  • Access-Control-Allow-Origin and Access-Control-Allow-Headers should not be a wildcard(*).
  • Cookie sameSite attribute should be None.
  • For enabling sameSite value to none, set the secure value to true: Enable backend with SSL/TLS certificate to work in the domain name.

Let’s see an example code that sets an access token in HTTPOnly cookie after checking the login credentials.

const express = require('express'); 
const app = express();
const cors = require('cors');

app.use(cors({ 
  origin: [ 
    'https://app.geekflare.com', 
    'https://lab.geekflare.com' 
  ], 
  methods: ['GET', 'PUT', 'POST'], 
  allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], 
  credentials: true, 
  maxAge: 600, 
  exposedHeaders: ['*', 'Authorization' ] 
}));

app.post('/login', function (req, res, next) { 
  res.cookie('access_token', access_token, {
    expires: new Date(Date.now() + (3600 * 1000 * 24 * 180 * 1)), //second min hour days year
    secure: true, // set to true if your using https or samesite is none
    httpOnly: true, // backend only
    sameSite: 'none' // set to none for cross-request
  });

  res.json({ msg: 'Login Successfully', access_token });
});

app.listen(80, function () { 
  console.log('CORS-enabled web server listening on port 80') 
}); 

You can configure CORS and HTTPOnly cookies by implementing the above four steps in your backend language and webserver.

You can follow this tutorial for apache and Nginx for enabling CORS by following the above steps.

withCredentials for Cross-Origin request

Credentials(Cookie, Authorization) sent with the same-origin request by default. For cross-origin, we have to specify the withCredentials to true.

XMLHttpRequest API

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://api.geekflare.com/user', true);
xhr.withCredentials = true;
xhr.send(null);

Fetch API

fetch('http://api.geekflare.com/user', {
  credentials: 'include'
});

JQuery Ajax

$.ajax({
   url: 'http://api.geekflare.com/user',
   xhrFields: {
      withCredentials: true
   }
});

Axios

axios.defaults.withCredentials = true

Conclusion

I hope the above article helps you understand how CORS works and enable CORS for cross-origin requests in the server. Why storing cookies in HTTPOnly is secure and how withCredentials used in clients for cross-origin requests.