The Cross-Origin Resource Sharing or CORS policies of a webhost indicate whether a browser should allow a script to access their resources.

Traditionally, the browser do not allow scripts loaded from an origin to interact with resources from another origin. This behaviour is the same-origin policy. CORS allows hosts to relax those constraints by instructing which external origins are allowed to interact with it. The host communicates this intent via a specific set of headers.

Note that the same-origin policy is client-enforced and as such does not restrict access to the resource with any other method than XHR.

As webservers can typically modify response headers, they allow you to set up a CORS policy. In this example, Apache acts as a proxy for Jenkins, an automation server. Jenkins provides API access to its information but does not allow the administrator to modify the CORS settings easily. The goal is to allow external origins to access the Jenkins API information that Apache proxies.

How does CORS work

By default, the browser does not allow cross-origin requests. However, CORS provides a way for hosts to opt-in for this kind of requests.

The server communicates its intent to the browser by adding headers to the request:

One of the central components is the Access-Control-Allow-Origin response header. This header describes whether all (*), or a specific origin is allowed to access resources.

Then, the headers Access-Control-Allow-Methods, Access-Control-Allow-Headers and Access-Control-Allow-Credentials indicate what types of requests the browser should allow on behalf of the server. Note that Access-Control-Allow-Credentials may only be enabled when the Access-Control-Allow-Origin is a specific origin.

Simple Requests

When the client code requests an XHR, the browser determines whether this is a Simple request. That is:

  1. A GET or HEAD or POST verb
  2. Only ‘safe’ request headers like Accept, Content-Type, Viewport-Width, etc.
  3. Only ‘safe’ Content-Type like application/x-www-form-urlencoded, multipart/form-data or text/plain

If the request is ‘simple’, it is executed. The browser checks the response headers and determines if the current Origin is allowed to execute this request. If this is not the case, an error occurs, otherwise the client continuous as intended.

Non-simple Requests

Non-simple requests are those that do not satisfy all of the rules mentioned above. For non-simple requests, the browser first executes a so-called preflight request. The preflight request contains information about the type of requests that the client intended to do. Typically, this is an OPTIONS request with an Origin, an Access-Control-Request-Method and an Access-Control_Request-Headers header.

OPTIONS /api/user/2
Access-Control-Request-Method: DELETE 
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://www.api-client.com

The host should respond with the appropriate Access-Control-Allow-* headers to authorize the browser to execute the actual request:

HTTP/1.1 204 No Content
Connection: keep-alive
Access-Control-Allow-Origin: https://www.api-client.com
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 600

Note that the response is a 204 No Content and does not contain the actual response to the request.

Enabling CORS with Apache

Let’s assume a simple VirtualHost based configuration that proxies requests to an internal service.

<VirtualHost *:443>
    ServerName service.com

    # ... SSL configuration ...
    # ... proxy configuration ... ProxyPass, ProxyPassReverse, etc

    # ... CORS configuration, discussed in this article ...
</VirtualHost>

This configuration works perfectly fine when accessing the service directly in the browser.

A crude and simple way to allow XHR is to add a wildcard -Allow-Origin header:

<IfModule mod_headers.c>
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Methods "POST, GET, OPTIONS, DELETE, PUT"
Header merge Vary "Origin"
</IfModule>

This approach has two problems:

  1. Allows every origin which is not very specific
  2. Wildcard -Allow-Origin does not allow Access-Control-Allow-Credentials

In this example, the hosted API requires the client to supply credentials. This requirement imposes an additional constraint on the Access-Control-Allow-Origin that wildcards are not allowed. The -Allow-Origin header may also contain only a single origin. That may sound problematic for configurations that should allow multiple allowed origins. To our help, when performing an XHR, the browser adds an Origin request header. Apache can use this header to adjust the Access-Control-Allow-Origin header to suit any origin.

First, instruct Apache to set an environment variable conditionally based on whether the Origin parameter matches a PCRE regex. In this example, try to match both http and https of both www.api-client.com and beta.api-client.com:8000.

SetEnvIf Origin "http(s)?://(www.api-client.com|beta.api-client.com:8000)$" AccessControlAllowOrigin=$0

Now, instruct Apache to set the Access-Control-Allow-Origin header to the value of Origin if the environment variable exists. Make sure to use Header always set to enable the headers for RewriteRules too.

Header always set Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin

Additionally, set remaining Access-Control-* headers to instruct the browser:

Header always set Access-Control-Allow-Credentials true
Header always set Access-Control-Allow-Headers "Origin, Authorization, Accept"
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
Header always set Access-Control-Max-Age "600"
Header always set Access-Control-Expose-Headers "Content-Security-Policy, Location"
Header merge Vary Origin

Set the header Vary to Origin is to indicate that responses might differ on a per-origin basis.

This approach works fine as long as no preflight request is performed. Thus, the final step is to configure a standard response for the preflight request which has the OPTIONS verb.

# Answer pre-flight requests with a 204 No Content
# https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]

This simply instructs to respond to OPTIONS requests with a 204 No Content response.

The final configuration might look as follows: