CORS on Apache: Cross-Origin Resource Sharing
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:
- A
GET
orHEAD
orPOST
verb - Only ‘safe’ request headers like
Accept
,Content-Type
,Viewport-Width
, etc. - Only ‘safe’
Content-Type
likeapplication/x-www-form-urlencoded
,multipart/form-data
ortext/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:
- Allows every origin which is not very specific
- Wildcard
-Allow-Origin
does not allowAccess-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: