Cross Origin Request and Sharing (CORS) has been around for a while now, and browser support is pretty much ubiquitous. It appears CORS is still a bit of a dark art since we still don’t see that many CORS enabled services. In this article I’ll go over some things we encountered when implementing CORS for our single page application hosting service. Specifically, we’ll discuss the Access-Control-Allow-Origin header, whitelisting domains, and token based authentication. In this article we assume all communication is secured with HTTPS.
What Is CORS?
Most simply, it’s a standard method for web browsers to request resources from servers of a different domain than the server the page was retrieved from:
The Same-Origin Policy restricts the browser from performing certain actions by scripts or documents based on the origin. The origin is everything in the URL before the path (for example, http://www.example.com). For certain actions, the browser will compare origins and, if they don’t match, won’t allow things to proceed. [source: staticapps.org]
The link above is a fantastic overview of CORS, we recommend reviewing it it if you need background for this article.
Enabling CORS for an API
As stated in the link above, setting the Access-Control-Allow-Origin header correctly in HTTP responses your server sends to the browser is critical. It can either be the fully qualified domain (ie https://www.your-api.com) or an asterisk “*” representing any domain is allowed. Many resources online will say using an asterisk is too permissive. But, if like us, your service allows users to specify their CORS domain, then you pretty much have to allow “*” and programmatically whitelist the callers Origin header with every call to the API. More detail on how we handled this below.
Complications arise when considering other HTTP clients than a browser. The Same-Origin Policy is a mechanism intended to protect users of websites from malicious actors, there is no such mechanism for mobile or desktop applications because they’re not typically subject to executing potentially malicious third party code (ie javascript). For example, curl won’t send on Origin header, unless told too. Typically, HTTP client libraries used by application developers won’t send Origin headers either.
Since we consider mobile applications and IoT (internet of things) applications to be equal consumers of our APIs, in our implementation we chose to only whitelist Origin headers if provided. Browsers will always send an Origin, we consider that to be sufficient.
Another complication arises when browsers make requests from pages loaded from the users local filesystem (ie file://path/to/file.html). When making cross origin requests in this manner, browsers will enforce the Same-Origin Policy by populating the Origin header with the string “null”. We’d like users to be able to develop web applications with our APIs with a minimum of hassle, so we whitelist “null” Origin headers. From a security perspective, we consider this to be the same as curl or a mobile application calling our API with no Origin header.
So, the implementation is really quiet simple. For each request to our API we check if the Origin header is set or the HTTP method is OPTIONS and set the Access-Control headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Max-Age) as described in the link above.
If the HTTP method is OPTIONS we return, otherwise we check the Origin header against the domain whitelisted for the users account, or allow “null”, and proceed with the request. This Apache mod_lua snippet shows an example:
function dispatch(r)
-- check if we need cors headers
local origin_header = r.headers_in['Origin']
if origin_header ~= nil or r.method == 'OPTIONS' then
-- set cors response headers
r.headers_out['Access-Control-Allow-Origin'] = '*'
r.headers_out['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
r.headers_out['Access-Control-Allow-Headers'] = 'Authorization, X-Query-Cache'
r.headers_out['Access-Control-Max-Age'] = '10'
end -- if OPTIONS go ahead and return
if r.method == 'OPTIONS' then
r.status = 200
return apache2.OK
end -- whitelist Origin header and proceed with request
end
Securing CORS APIs
There’s a mechanism in the CORS specification to allow the setting and getting of cookies by CORS servers. Check this link for more information. Basically, it involves client side Javascript setting the XMLHttpRequests withCredentials property, and the server returning an Access-Control-Allow-Credentials header. As noted above, we consider mobile applications and IoT (internet of things) apps to be equal consumers of your APIs, we don’t consider setting cookies on the browser to be viable authorization strategy. We instead opted for token based authorization so didn’t pursue this approach.
As you see in the snippet above, the two headers our API accepts are Authorization and X-Query-Cache. We use the Authorization header for securing calls to our API. The Authorization header is set with two space separated values: “account-key authorization-key”. We use the authorization-key to lookup the Authorization Token stored in redis, and use it to whitelist the domain, and validate the permissions for the request.
Users define Authorization Tokens in their account admin page, and then use their account key and the Authorization Tokens authentication-key to populate the header in their Javascript that calls our service (we provide a Javascript helper library for this). Our Authorization Tokens have permissions as to what resources (ie API Queries in our application) the bearer of the Authorization Tokens authorization key can access. So the user of our API can specify public and privately accessible data. Our User API allows our users users to acquire a ‘private’ authorization key that can be used to access private data, via username and password login.
Summary
We’ve seen how an API can enable CORS requests, whitelist domains, and secure resources with token base authentication. There are a lot of great resources available on CORS, we hope this document helps pull together some of these ideas.