A Simple Cross-Origin Resource Sharing Example Explained
I was working on a problem where there was a requirement to log in from a Ajax enabled application to a third party application running on a different host. Authentication functionality on this third party application was only available via the POST mechanism and I hit the well known (for others) Same Origin policy.
I’m in the middle of rediscovering JavaScript again after several years in the Flex world, so I decided to take some time out and figure out what can be done here. In short, nothing can really be done in the Ajax application side. The browser implementation will in general block access to any response from the third party server not letting the JavaScript anywhere near the information you might need ( in this case the authentication token).
The browser also won’t allow you to set the Authorization header on an outgoing Ajax request by default either, so using that as authentication (along with SSL) was also a non starter. That is totally understandable from a security viewpoint, and while their are hacks and workarounds, I wasn’t comfortable with recommending them.
First, let’s have a look at the request/response flow when trying to access a cross domain script via POST and Ajax. The client and server scripts don’t really change in what I am trying to show Cross-Origin Resource Sharing (CORS) so I’ll list them here (download at the end of the post).
Step 1: Without CORS
Firstly, I want to demonstrate the request/response flow without CORS implemented. I’ve dropped the code listed below in a folder, and I have both localhost, and a VirtualHost (called ‘cf901′) pointing to the same htdocs folder. This allows me to make cross domain requests without leaving my development machine. The basic flow of events is:
- Call http://localhost/tests/accesscontrol/cors-step01/client.html
- Click on a button, which uses the code listed above to make an Ajax POST request to http://cf901/tests/accesscontrol/cors-step01/post.cfm
- If successful, the client.html will get a simple JSON return from post.cfm
Step 1.1: Call the client.html
I’m utilising JQuery to make the Ajax calls easier, and the code is pretty simple, Base64 is a utility function found here.
$(document).ready(function() { //LOGIN $("#callMe").click(function() { $.ajax({ url: 'http://cf901/tests/accesscontrol/post.cfm', type: 'POST', success: function() { alert('success'); }, error: function() { alert('error'); }, beforeSend: function(req) { var headerValue = 'Basic ' + Base64.encode($("#username").val() + ':' + $("#password").val()); req.withCredentials = 'true'; req.setRequestHeader('Authorization', headerValue); } }); return false; }); });
The important points to note from the code above are:
- Setting the ‘withCredentials’ on the jQuery Ajax request.
- We add our HTTP Basic Authorization header to the outgoing request (more later)
Here are the headers of the request, there is nothing abnormal and the response is just to return the HTML page:
GET /tests/accesscontrol/cors-step01/client.html HTTP/1.1 Host: localhost Cache-Control: no-cache Pragma: no-cache User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Encoding: gzip,deflate,sdch Accept-Language: en-GB,en-US;q=0.8,en;q=0.6 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Step 1.2: Do a post to the different origin post.cfm
For ease of use, the server side target script is just a CFML page which echoes back some JSON. Since I only want this data to be available to POST requests, I am doing a quick and dirty check in the script for simplicity:
<!--- I only want to return when this is POSTed to --->
<cfset httpRequestData = getHTTPRequestData()/>
<cfif listFindNoCase("POST", httpRequestData.method) eq 1>
<cfset returnStruct = {}/>
<cfset returnStruct.form = form/>
<cfset returnStruct.date = now()/>
<cfcontent reset="true"/><cfoutput>#serializeJSON(returnStruct)#</cfoutput>
</cfif>Since we haven’t actually configured the Cross-Origin Resource sharing yet, and there is currently no HTTP Basic Auth protection, this should be a straightforward request and response if we were carrying out the action from HTML with a POST operation. As we are making a Ajax request and I’m using Chrome, it does a pre-flight OPTIONS check. You can see the request headers below:
OPTIONS /tests/accesscontrol/cors-step01/post.cfm HTTP/1.1 Host: cf901 Access-Control-Request-Method: POST Origin: http://localhost User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2 Access-Control-Request-Headers: Origin, Authorization, Accept Accept: */* Referer: http://localhost/tests/accesscontrol/cors-step01/client.html Accept-Encoding: gzip,deflate,sdch Accept-Language: en-GB,en-US;q=0.8,en;q=0.6 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
The important points to note from the above request are:
- We didn’t get a POST request, instead we got a Pre-flight OPTIONS request.
- There are extra headers in the request, which wouldn’t be in a normal POST from an HTML page (Access-Control-* and Origin).
- These headers give the server a chance to respond and tell the browser if it will allow the request (more on these later in Step 2)
- Note that there is no Authorization header, even though we asked for one to be added.
Step 1.3: Response to the client.html Ajax call
So what does the response look like? See below:
HTTP/1.1 200 OK Date: Thu, 08 Dec 2011 12:24:00 GMT Server: Apache/2.2.13 (Win32) Communique/4.0.11 JRun/4.0 Transfer-Encoding: chunked Content-Type: text/html; charset=UTF-8
The response looks good, an HTTP 200 but no JSON struct is returned as this wasn’t a POST request, but referring to our client it made the error callback, not the success? Lets have a look at the JavaScript console:
XMLHttpRequest cannot load http://cf901/tests/accesscontrol/cors-step01/post.cfm. Origin http://localhost is not allowed by Access-Control-Allow-Origin.
So basically, even though the HTTP POST from Ajax completed successfully, the browser denied the JavaScript access to the return request, as well as us denying access to the data since it didn’t come from a POST request (in reality, our web application is not responding to the OPTIONS request properly, but that is a whole different topic). With access denied to the returned information from the server to the Ajax application, we cannot do any real functionality on the web application, since by W3C specs, the only allowed HTTP methods in this configuration are by definition idempotent and cannot make any changes to the resources being accessed. So what can we do to solve this? Cross-Origin Resource Sharing (CORS) is our friend.
Step 2: With Cross-Origin Resource Sharing
For the 2nd step of this excercise, I’m going to introduce HTTP Basic Authentication to the sample. This was the original intent of my testing, but in the 1st step I wanted to keep it as simple as possible.
Thankfully we stand on the shoulders of giants, and one of the ColdFusion community has kindly shared a snippet on how to implement a psudeo Basic Authentication via the Application.cfc. You can get the original code from Nathan Mische’s example. The client.html and post.cfm are the same as in Step 1.
The flow should be fairly similar, although this time we are going to implement CORS at the Apache level. Implementing CORS means adding some extra headers to the original response to the Ajax call. The flow this time will look slightly different when we have it configured successfully:
- Call http://localhost/tests/accesscontrol/cors-step02/client.html
- Click on a button, which uses the code listed above to make an Ajax POST request to http://cf901/tests/accesscontrol/cors-step02/post.cfm and browser makes an OPTIONS request to the web server.
- The Web Server responds with the appropriate CORS enabled response.
- The browser then makes a 2nd POST request if it checks the CORS response and determines that the Ajax script is allowed to access the server side resource.
- Browser passes the POST response back to the JavaScript execution.
Step 2.1: Call the client.html
Step 2.2: Client makes Ajax POST Request
The server side post.cfm is the still the same, and we get the same OPTIONS HTTP request:
OPTIONS /tests/accesscontrol/cors-step02/post.cfm HTTP/1.1 Host: cf901 Access-Control-Request-Method: POST Origin: http://localhost User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2 Access-Control-Request-Headers: Origin, Authorization, Accept Accept: */* Referer: http://localhost/tests/accesscontrol/cors-step02/client.html Accept-Encoding: gzip,deflate,sdch Accept-Language: en-GB,en-US;q=0.8,en;q=0.6 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
This time, we will go into more detail on the extra headers being added:
- Since this is a Ajax request from the browser, the browser makes a pre-flight OPTIONS request.
- This gives the server a chance to respond and tell the browser if it will allow the request.
- The ‘Access-Control-Request-Method’ tells the responding server which HTTP method is being used.
- The ‘Origin’ header tells the server where the request is coming from
- The ‘Access-Control-Request-Headers’ tells the server which headers it can expect from the next request if allowed
- This information can then be used by the Web Application, to decide whether it will respond to the request.
- Note that there is still no Authorization header, even though we asked for one to be added, but this isn’t the actual request for the resource yet, this is only the pre-flight check to see if the original request can be made.
Step 2.3: Web Server responds to the OPTIONS request with CORS enabled response.
<Location /> Options FollowSymLinks Order allow,deny Allow from all Header always set Access-Control-Allow-Methods "GET, POST, DELETE, OPTIONS, PUT" Header always set Access-Control-Allow-Headers "Content-Type, X-Requested-With, X-HTTP-Method-Override, Origin, Accept, Authorization" Header always set Access-Control-Allow-Credentials "true" Header always set Cache-Control "max-age=0" Header always set Access-Control-Allow-Origin * </Location>
If you want more detail on what these headers do, read the W3C specification as well as the Mozilla Developer Network article. I’ll give a brief description here:
- Access-Control-Allow-Methods – fairly clear, the methods that the Web Application will accept
- Access-Control-Allow-Headers – An instruction to the browser about what headers it will allow
- Access-Control-Allow-Credentials – If the browser should sent HTTP Basic Auth or Cookie credentials in the request
- Access-Control-Allow-Origin – What origin it will allow from (there are reports IE needs this to be set to the actual origin that the request is coming from, and that a wild card doesn’t work).
The effect of these headers (which remember can be set by the web application!) is the following response:
HTTP/1.1 200 OK Date: Thu, 08 Dec 2011 13:54:41 GMT Server: Apache/2.2.13 (Win32) Communique/4.0.11 JRun/4.0 Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS, PUT Access-Control-Allow-Headers: Content-Type, X-Requested-With, X-HTTP-Method-Override, Origin, Accept, Authorization Access-Control-Allow-Credentials: true Cache-Control: max-age=0 Access-Control-Allow-Origin: * Transfer-Encoding: chunked Content-Type: text/html; charset=UTF-8
NOTE: Since this is the response to an OPTIONS request, I had to modify the Application.cfc to not apply Basic Authentication to that OPTIONS request, more details on why that was necessary here.
Step 2.4: Successful CORS request, perform original POST
The browser can then see that the server will allow the following, it can reconcile them with the Ajax call from the client.html:
- Allow a POST (from Access-Control-Allow-Methods)
- Will allo an Authorization header (from Access-Control-Allow-Headers)
- Instructs the browser to send credentials if it has them (Access-Control-Allow-Credentials)
- And is allowed form any other host (from Access-Control-Allow-Origin)
The browser then allows the original request from the JavaScript to them execute:
POST /tests/accesscontrol/cors-step02/post.cfm HTTP/1.1 Host: cf901 Content-Length: 0 Origin: http://localhost Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2 Accept: */* Referer: http://localhost/tests/accesscontrol/cors-step02/client.html Accept-Encoding: gzip,deflate,sdch Accept-Language: en-GB,en-US;q=0.8,en;q=0.6 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
And the server happily responds with the response, which the browser passes back to the JavaScript execution:
HTTP/1.1 200 OK Date: Thu, 08 Dec 2011 13:54:41 GMT Server: Apache/2.2.13 (Win32) Communique/4.0.11 JRun/4.0 Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS, PUT Access-Control-Allow-Headers: Content-Type, X-Requested-With, X-HTTP-Method-Override, Origin, Accept, Authorization Access-Control-Allow-Credentials: true Cache-Control: max-age=0 Access-Control-Allow-Origin: * Transfer-Encoding: chunked Content-Type: text/html; charset=UTF-8 {"FORM":{},"DATE":"December, 08 2011 13:54:41"}
Step 2.5: Browser passes through server response to JavaScript execution
Looking at the JavaScript console, there should be no errors, and the $.ajax call’s success handler fires!
In Conclusion
CORS works on all modern browsers, I couldn’t find a definitive list at the time of writing nczonline.net has them listed as:
- Internet Explorer 8+
- Firefox 3.5+
- Safari 4+
- Chrome
Sample Code
Basic configuration steps to get the samples above running are:
For Step 1
- Have CF901 and Apache installed on your system.
- Add a HOSTS entry to 127.0.0.1
- Place code from ‘download/cors-step01′ into web root, and change the script variable to point to the post.cfm location
- Add a VirtualHost into your Apache configuration, and add the directives in Step 2.3
- Place code from ‘download/cors-step02′ into web root, and change the script variable to point to the post.cfm location
- You can play about with the username/password value to show Basic Auth working/failing.
Sample code is here - accesscontrol