Apache and CORS – Enabling Cross Origin Resource Sharing

Just a few days ago, I was working on an applicaiton which needed to make Cross Origin request to a rails application. This applicaiton was being served by Passenger on Apache server.

In front of the application server sits a public facing Apache server where the Load Balancing happens.

A custom javascript app deployed at https://myfrontendapp.com is making requests via an api to https://mybackendapp.com. This meant that when the app made an XHR (XMLHttpRequest) request to the endpoint at https://mybackendapp.com, the browser wanted to make sure that the requesting app had permission to access the resources at a different origin.

For security reasons, browsers restrict cross-origin HTTP requests by following the same origin policy. This means that unless the response from the other origin includes the right CORS headers, the front end app is only allowed to request HTTP resources from the same origin.

CORS request and headers

CORS origin request when made to an endpoint makes a pre-flight request and then the original request. The preflight request uses the OPTIONS HTTP method before making the original HTTP request using any of the HTTP verbs.

You must enable the following headers in your app, Apache / Nginx configuration to set the CORS headers:

Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials

In Apache, this can be done in <Directory> and <Location> contexts

<Location /var/www/mybackendapp>
Header always set Access-Control-Allow-Origin "https://"
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
Header always set Access-Control-Max-Age "86400"
Header always set Access-Control-Allow-Headers "x-requested-with, Content-Type, origin, authorization, accept, client-security-token"
Header always set Access-Control-Allow-Credentials "true"
</Location>

With these headers set, when the browser makes a pre-flight request, it returns the expected CORS headers to let thhe browser know that the front end app is allowed to request for resources on a different origin.

Along with setting these Headers, you have to make sure you handle the OPTIONS request so it returns a 200 status. For this, in Apache, you could do something like this

RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]

With these in place, you should be all ready to make cross origin request.

One thing you have to make sure is that the OPTIONS request response is a 200 which returns a blank response of Content-Length 0.

So if you made an OPITONS request, the response should look like this:

$ curl -H "Origin: https://myfrontendapp.com" --verbose -X OPTIONS https://mybackendapp.com/api/v1/end_point
* Trying xxx.xx.xx.xx ...
* TCP_NODELAY set
* Connected to mybackendapp.com (xxx.xx.xx.xx) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
* subject: CN=mybackendapp.com
* start date: Apr 23 00:00:00 2018 GMT
* expire date: Apr 22 12:00:00 2020 GMT
* subjectAltName: host "mybackendapp.com" matched cert's "*.mybackendapp.com"
* issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=Thawte RSA CA 2018
* SSL certificate verify ok.
> OPTIONS /api/v1/end_point HTTP/1.1
> Host: mybackendapp.com
> User-Agent: curl/7.54.0
> Accept: */*
> Origin: https://myfrontendapp.com
>
< HTTP/1.1 200 OK
< Date: Wed, 13 Feb 2019 23:24:32 GMT
< Server: Apache
< Access-Control-Allow-Origin: https://myfrontendapp.com
< Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT
< Access-Control-Max-Age: 86400
< Access-Control-Allow-Headers: x-requested-with, Content-Type, origin, Authorization, accept, client-security-token
< Access-Control-Allow-Credentials: true
< Last-Modified: Wed, 06 Feb 2019 09:26:02 GMT
< ETag: "0-58136507f9df3"
< Accept-Ranges: bytes
< Content-Length: 0
< X-Content-Type-Options: nosniff
< Vary: User-Agent
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host mybackendapp.com left intact

The browsers do not like anything other than a 200 blank response for an OPTIONS request. 201 No Content does not work.

Issue:

After setting up all the expected configuration in Apache, the CORS request was still not working. After hours of tinkering, reading online and checking the Apache documentation and my own setup, I found that this was due to an error response to the OPTIONS request. The browser was being returned a 200 but there was also an Apache error in the response. This resulted in the pre-flight request failing and the main request never getting triggered.

Fix:

What I did to fix this issue was to set an ErrorDocument for the OPTIONS request which was returning a 200 but with an Apache error in the response body.

In Apache, this can be done in <Location> and <VirtualHost>

ErrorDocument 200 /blank.html

blank.html file as the name suggests is a no content, blank html file which returns a 0 Content Length response when an OPTIONS request is made.

I continue to look into the issue (Apache’s built in error response for OPTIONS request) for a proper fix but until then, this workaround will let me continue my work on the 2 apps deployed on 2 different hosts (origins).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s