How to avoid CORS preflight requests in Single Page Applications?
Asked Answered
E

4

10

I want to build a Single Page Application (SPA) and noticed the following issue during separation of backend API (REST) and frontend assets (static vue.js code):

When serving the index.html from another domain than the API backend, most of the POST/PUT requests trigger a CORS preflight request.

I did some research and found out that blog posts [1][2] discuss this issue - without giving a practical solution. Some headers (e.g. Authorization header and the Content-Type header with the application/json value) are not allowed as cors-safelisted-request-header. Thus POST/PUT requests trigger a CORS preflight request. This is not acceptable since it adds a considerable amount of latency.

Question

Is it possible to avoid these preflight requests if both domains are owned by the same entity?

Research

I did some research on how to avoid CORS requests between frontend and backend. The solution requires the index.html file being served from the same domain as the REST API backend (see Example below). I wonder if not using separate domains is the only solution to avoid CORS requests for SPAs.

Scenario (Example)

  • Single Page Application (SPA); frontend and backend layer
  • hosted in the AWS cloud
  • Layer 1: CloudFront CDN with an S3 bucket origin - serve static assets (Vue.js frontend) on static.example.com
  • Layer 2: Load-Balancer with ECS integration which is running node.js containers to host the (REST) backend on example.com
  • The communication between layer 1 and layer 2 uses the HTTPS protocol and the REST paradigm.
  • The index.html is served by layer 2 and customers open the webapp using example.com.
  • The index.html references the API by pointing to the same domain (example.com). It references the static vue assets by pointing to the CDN (static.example.com).
  • The SPA consists of two parts: a) the public assets (.js files, .css files etc.) and b) the index.html file. The latter one is served by the same fleet of backend containers which also host the REST API.

References

[1] https://www.freecodecamp.org/news/the-terrible-performance-cost-of-cors-api-on-the-single-page-application-spa-6fcf71e50147/
[2] https://developer.akamai.com/blog/2015/08/17/solving-options-performance-issue-spas

Elma answered 25/4, 2020 at 14:31 Comment(0)
O
4

There are not many options there.

The simplest solution would be to deliver the html and assets from the same domain as your API.

The second options is to use only headers that are cors-safelisted-request-header.

What I noticed:

The Content-Type header can be replaced with the Accept header. This header is okay.

If you are doing XHR requests, you can omit the Authentication header and instead add the authentication infos automatically by setting the withCredentials field of your XHR request to true. VanillaJS example:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.example.org/api/whatever', true);
xhr.withCredentials = true;
xhr.send();

If you are using any other XHR client, consult the docs if the option can be set.

Another option would be to authenticate with cookies and server side sessions. As you are using AWS, AWS Cognito might be an option.

If there are more headers in use that are not a safelisted CORS header, you have to get rid of them.

Othilia answered 30/4, 2020 at 14:37 Comment(2)
Does setting withCredentials imply that authorization must be provided via cookies and not using the Authorization header?Asomatous
I looked it up and I cannot write it better than MDN: The XMLHttpRequest.withCredentials property is a Boolean that indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests. See: developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/… Cookies seem to be the safe bet. You still have to be authorized against the API-Domain though.Othilia
C
9

How to avoid CORS preflight requests in Single Page Applications?

First, you cannot "avoid" something which is part of the standard. Second, this question is wrongly stated, as SPAs themselves can use or not use CORS. This entirely depends on your set-up/design. If you want to avoid preflighting, just do not request resources from another origins.

Is it possible to avoid these preflight requests if both domains are owned by the same entity?

No.

Site ownership has nothing to do with CORS. Cross-Origin Resource Sharing means that you want to share resources between different origins. Not necessarily between different domains. Origin is not entity nor owner. Origin is just a part of a URL.

The best solution is to avoid introducing the CORS problem altogether and always stick to the same-origin. Your assumption that you should separate back-end API from front-end assets with different origins is not true.

Introducing multiple origins and domains to your setup is adding more problems than it solves. Ideally your whole setup should be hidden from connecting clients and reduced to a single domain and origin.

The setup you want

                     -> static file server
CDN -> Load balancer -> api server
                     -> api server
                     -> ...

Example configuration

Use your load balancer and its ACL rules. Tell it to route all traffic where it should go. I will use haproxy as an example, because that is what I use, and because from what I know this is pretty much industry standard in terms of software load balancing solution.

This is not the whole config, just a relevant part concerning routing traffic.

# part of haproxy configuration file, usually located at /etc/haproxy/haproxy.cfg

frontend http-in # this is where requests get in to load balancer
  bind *:80
  acl data path_beg /api  # "catch" any request with path beginning with "/api"
  use_backend api if data # then route it to api backend defined below
  default_backend static  # any non-matching request we direct to static file server

backend static
  server node1 127.0.0.1:3000 # server hosting static files (index.html)

backend api
  server node1 127.0.0.1:4000 # application servers
  server node2 ...

Chloroplast answered 6/5, 2020 at 20:53 Comment(5)
your answer really nails the problem! I would also like to route traffic differently on the Load Balancer level - without using two different domains and introducing CORS issues. However, it is not that easy using an AWS managed Application Load Balancer. That being said, thank you for the great answer!Asomatous
I agree that the question is not being phrased very precise and in fact, asking for a workaround to another issue... However as mbuechmann answered above, it is possible (to a certain degree) to prevent preflight requests from being sent by not using particular headers. Thanks to your answer, I understand now, that this is irrelevant for the architectural problem I'm trying to solve but I would like to somehow rephrase the question to provide at least any benefit to the SO community.Asomatous
@MartinLöper If you want to use ELB from AWS, maybe "Application Load Balancer" would fit the task. When I look here aws.amazon.com/elasticloadbalancing/features/?nc=sn&loc=2 it says it supports path-based routing on Layer 7, so it potentialy could mimic behaviour I described above. Not sure since I am not using AWS much.Chloroplast
@MartinLöper I just realised you already mentioned "Application Load Balancer". Maybe Amazon support can help with setup then? I would confirm with them if this kind of setup is possible, or maybe using different product from their lineup (separate box with haproxy on it), or consider switching provider.Chloroplast
I just found a question on serverfault which is also discussing this issue. One answer suggests configuring it at the CDN level rather than on the ALB. I will try out both, but I think the ALB is not supporting it. I want to stick to AWS managed load balancing (because it works and scales out of the box) which means I probably have to configure it on the CDN level.Asomatous
O
4

There are not many options there.

The simplest solution would be to deliver the html and assets from the same domain as your API.

The second options is to use only headers that are cors-safelisted-request-header.

What I noticed:

The Content-Type header can be replaced with the Accept header. This header is okay.

If you are doing XHR requests, you can omit the Authentication header and instead add the authentication infos automatically by setting the withCredentials field of your XHR request to true. VanillaJS example:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.example.org/api/whatever', true);
xhr.withCredentials = true;
xhr.send();

If you are using any other XHR client, consult the docs if the option can be set.

Another option would be to authenticate with cookies and server side sessions. As you are using AWS, AWS Cognito might be an option.

If there are more headers in use that are not a safelisted CORS header, you have to get rid of them.

Othilia answered 30/4, 2020 at 14:37 Comment(2)
Does setting withCredentials imply that authorization must be provided via cookies and not using the Authorization header?Asomatous
I looked it up and I cannot write it better than MDN: The XMLHttpRequest.withCredentials property is a Boolean that indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests. See: developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/… Cookies seem to be the safe bet. You still have to be authorized against the API-Domain though.Othilia
V
2

Access-Control-Max-Age may help:

The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.

...Actually the link you've posted mentions it:

You might say yes. We can use the Access-Control-Max-Age header to cache the results of a preflight request.

Then they go on with caveat:

The way preflight cache works is per URL, not just the origin. This means that any change in the path (which includes query parameters) warrants another preflight request.

But you can keep using the same URL and send all parameters in the request body.

Also

Is it possible to avoid these preflight requests if both domains are owned by the same entity?

No. There is no practical way for a browser to check ownership of domains. Sometimes even for a human it's not an easy task.

Vetchling answered 2/5, 2020 at 12:28 Comment(0)
B
-1

From what I understand, you want to avoid the CORS restrictions. To do that you must modify the web.config file of your website to allow CORS like so:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <system.webServer>
   <httpProtocol>
     <customHeaders>
       <add name="Access-Control-Allow-Origin" value="*" />
     </customHeaders>
   </httpProtocol>
 </system.webServer>
</configuration> 

I think there might be a way to do that programmatically but this is the way it worked for me. You might still get a couple of CORS warnings or errors in your browser console but apart from that it works.

Burin answered 6/5, 2020 at 17:20 Comment(1)
The question is about CORS preflight requests and not enabling CORS at server side. The latter was already configured successfully ;)Asomatous

© 2022 - 2024 — McMap. All rights reserved.