Cross-Origin Resource Sharing for Windows Azure Cloud Apps

Cross-Origin Resource Sharing for Windows Azure Cloud Apps

Enable your Azure web apps to make cross-site requests

Download the Float Fan source code

Cross-Origin Resource Sharing (CORS) defines a policy-driven approach for controlling how web applications make cross-domain requests. This is in contrast to the long-established single-origin policy (SOP), which says that URLs in JavaScript (and elsewhere) can only refer to the origin URL of the page. The advantage of CORS is that it allows a web app to pull resources (e.g., data and other features) from multiple websites onto one page without having to construct proxies or use other workarounds. CORS, which is described in a W3C Working Draft, defines a contract between the client-side browser and the server application as well as an algorithm that is followed to enable server applications to opt in to allowing cross-origin requests.

In this article, I demonstrate a unified architecture and code base that lets you support single-origin and cross-origin requests from the outset. Specifically, I show how to implement support for CORS for an HTML5 web application built with ASP.NET MVC 3 and hosted within various Azure Web roles.

Finding Your Origin

A typical web application consists of a rich browser-based client (using HTML5, JavaScript, and CSS3), served up from a web server application (such as with ASP.NET or MVC 3 in an Azure Web role) and integrating data from various Representational State Transfer (REST) services. From the perspective of the browser, there are two scenarios for how the web app gets its data from the services: same-origin and cross-origin (aka cross-domain).

In the same-origin scenario, the services are hosted within the same web server application that delivered the web page, or they may exist externally and be called from the web server application logic with their resultant data integrated in the web server application's response. In either case, from the browser's perspective they are being reached from the same server -- the same origin. More specifically, requests are treated by the browser as same-origin when they are to resources accessed with the same protocol (e.g., HTTP/S), port (e.g., 80), and hostname (e.g., "http://myapp.cloudapp.net/" or "http://localhost/") as the one from which the web page was loaded. Internet Explorer (IE) is a little bit more forgiving on this than Safari, Chrome, or Opera in that if only the ports differ, IE considers the request same-origin.

When services are accessed with different hostnames, protocols, or ports, the request is considered cross-origin. This is very common for websites that use third-party services for their data, where the browser-based client calls the service directly and the domain used to access the service (e.g., "http://www.mydataservice.com") is different from the domain that delivered the web page (e.g., "http://myapp.cloudapp.net"). The subtle issue here is that browsers enforce an SOP whereby such cross-domain requests are not allowed, in order to protect the browser from malicious code loaded from the external domain.

This is not an issue specific to web applications hosted on Azure, but if you are doing any amount of web application development on Azure you will undoubtedly encounter the SOP. In fact, Azure introduces additional scenarios where cross-origin resource access is required (for example, if your web application and service roles live in different hosted services). Naturally it is best to architect your Azure-hosted solution with the SOP in mind, rather than encounter SOP and have to make drastic changes to your web application when you least expect it.

With this in mind, let us turn our attention to having a little fun building a simple Azure application that relies on CORS to enable cross-origin communication.

Introducing Float Fan

Float Fan is a website that allows fans of parades to plan for and communicate their parade-viewing experience. For the purposes of this article, we focus on a feature that lists parades in a web page, similar to that shown in Figure 1. (You can download the source for the Float Fan app discussed in this article.)

Figure 1: Float Fan parade listing UI
Figure 1: Float Fan parade listing UI

The architecture of Float Fan is as illustrated in Figure 2.

Figure 2: Float Fan architecture
Figure 2: Float Fan architecture

Notice that we have two Azure-hosted services, FloatFanWeb and FloatFanService. FloatFanWeb hosts the website within an Azure Web role. The website is built using MVC 3 on the server side and implemented with a touch of HTML5, JavaScript, and CSS3 on the client side. The server side also provides the image used for the floats in the listing. FloatFanService hosts an Azure Web role containing the REST service that provides parade data (e.g., parade name, date, location, route). This service is also implemented using MVC 3, where controller actions return JavaScript Object Notation (JSON)-encoded data in response to browser client requests issued using XmlHttpRequest.

Observe that because the website and service exist within different hosted services, they have different DNS names. From the perspective of the browser, any requests from the web page to get parade data from the service are inherently cross-domain.

So what is the problem? Assuming that we started with the website and service hosted from within the same web role, the XmlHttpRequest made from the browser would have worked fine because the requests to the service for data are targeting the same origin from which the web page was loaded. However, in preparation for a big parade such as the New Year's Day Rose Parade, we decided to split out the functionality into separate roles (which would imply differing ports) or separate hosted services (which would create different hostnames) to be able to scale up the services more aggressively or upgrade them at different times than the website. In this scenario, we would find that no parade data is returned to the browser because we have now violated the SOP enforced by the browser. We would then be scrambling to implement a mechanism to allow cross-origin requests, such as JSONP ("JSON with padding") or a proxy. Fortunately, we built the solution from the beginning to allow such cross-origin requests using CORS.

GETting Cross-Origin Data

In its most basic form, the contract between the client-side browser and the server application mentioned earlier amounts to the browser evaluating the value of the Access-Control-Allow-Origin response header returned from the server. If this value matches "*" or is the same as the address from which the web page was loaded, the request succeeds and the data is made available to the web page. If it does not match, the request is blocked, appearing as if a 404 resource not found error occurred with no data being made available to the page.

Let's use Float Fan as our example. The web page is loaded in the browser from "http://floatfanweb.cloudapp.net." For the browser to allow the cross-origin GET request to the service, the response from the service must include either of the following headers:

Access-Control-Allow-Origin: http://floatfanweb.cloudapp.net

Access-Control-Allow-Origin: *

In other words, to enable CORS for your REST service, you only need to return the Access-Control-Allow-Origin header with the appropriate value. In our Float Fan Service implementation, we simply add the header within the MVC action that returns the parade data, as shown in Figure 3.

public JsonResult Index()
{
    Response.AddHeader("Access-Control-Allow-Origin", "*");

    var parades = new[] {
        new { name = "Rose Parade 2012",
date="1/2/2012", city= "Pasadena",
state = "CA",
route= "From Orange Grove, down Colorado to Sierra Madre.", image="Megazord.png" },
        new { name = "Thanksgiving Day 2011",
date="11/24/2011",
city= "New York City",
state = "NY",
route= "From 77th St, down Central Park West, then ...",
image="Bug.png"}
    };

    return Json(parades, JsonRequestBehavior.AllowGet);
}

There are a few important points worth noting in the code in Figure 3. First, our action ultimately needs to return the collection of parades as a JSON array, so it returns a value of type JsonResult. Second, notice that parades is an array of anonymous types, and it is the Json() method that takes care of serializing it to a JSON array in the response body. Finally, by default GET requests that return JSON are not permitted by ASP.NET, and that is why we need to specify the JsonRequestBehavior.AllowGet.

So we are covered on the server side, but what does the client JavaScript need to make this GET request? The process is actually fairly straightforward, and I've generalized the JavaScript code, shown in Figure 4, to something you can reuse in your projects.

function corsGet(url, data, callback, returnType) {
        var jqxhr = $.get(url, data, callback, returnType)
        .error(function (jqXhHR, status, errorThrown) {
                if ($.browser.msie && window.XDomainRequest) {
                    var xdr = new XDomainRequest();
                    xdr.open("get", url);
                    xdr.onload = function () {
                        callback(JSON.parse(this.responseText), 'success');
                    };
                    xdr.send();
                } else {
                    alert("CORS is not supported in this browser or from this origin.");
                }
            });
        }


For most browsers except IE, we just use the jQuery.Get method (which uses an XmlHttpRequest behind the scenes). This method takes in the URL of the service that is the target of the GET request, a usually NULL value for data, a function callback that is invoked when the data is successfully retrieved, and a string return type indicating whether the response data should be interpreted by the browser as JSON, XML, script, or HTML.

The bulk of the method shown in Figure 4 defines an error handler. The primary purpose of the error handler is to address the case when a cross-origin request is made within IE. What you need to be aware of is that jQuery.Get will work fine within IE for same-origin requests but will fail for cross-origin requests, which must be made using IE's XDomainRequest (XDR) object. This is why in the error handler, we check first whether the browser is IE, and if so, we configure an instance of XDR and make the request using the XDR instance. Obviously, if an error occurred and it is not because of a cross-origin request in IE, then we simply alert the user that the CORS request failed.

To give you an idea of how to invoke corsGet, Figure 5 shows how we invoke it to get the parades in Float Fan.

var floatFanServiceUrl = "http://floatfanservice.cloudapp.net";

getParades(floatFanServiceUrl);

function getParades(url) {
    corsGet(url, null, function (parades) { updateParades(parades); }, "json");
}

Notice in Figure 5 that for the success callback, we simply call another method that takes the parades object (deserialized from JSON) and passes it to our updateParades method, which handles updating the UI via the browser Document Object Model (DOM). Because our service returns JSON, providing the "json" value for the returnType parameter ensures that the browser correctly deserializes the response into an array of parade objects.

POSTing from Another Realm

The previous solution works for GET, but what if you want to handle the transmission of form data via a POST? CORS handles verbs such as POST in a special way because these verbs are understood to have side effects, particularly when such requests are made multiple times. CORS defines a special "pre-flight" phase in which the browser must first ask the server application for its cross-origin policy by conducting a request with the OPTIONS verb, and only if allowed does the browser perform the actual POST. The policy retrieved with this pre-flight operation can be cached for a period of time, so that subsequent operations can occur without the pre-flight request.

Looking at the Float Fan solution, say we want to support adding a new parade. We can do this on the client by doing a POST of the form data. However, for this to work, the action accepting the POST in the FloatFanService must follow the approach defined by CORS. Figure 6 shows how we handle a POST of a new parade entry.

[AcceptVerbs("POST", "OPTIONS")]
public JsonResult AddParade(string name, string date, string city, string state, string route, string image)
{
    Response.AddHeader("Access-Control-Allow-Origin", "*");

    //This is a preflight request
    if (Request.RequestType.Equals("OPTIONS",
 	 StringComparison.InvariantCultureIgnoreCase))
    {
        Response.AddHeader("Access-Control-Allow-Methods", "POST, PUT");

	 //Allow jQuery requests which include the X-Requested-With header
Response.AddHeader("Access-Control-Allow-Headers", "X-Requested-With");

//Allow caching this policy for 1 day
Response.AddHeader("Access-Control-Max-Age", "86400");
        return null;
    }

    //Code handling POST data from XDomainRequest from Internet Explorer goes here...

    ...code to add parade...

    return Json(parades);
}


Notice in the implementation that we have decorated the action with the AcceptVerbs attribute to accept both POST and OPTIONS verbs. This lets us handle both the pre-flight request and subsequent post requests using the same action. The routine starts off in the same way as for a GET, adding in the Access-Control-Allow-Origin header into the response headers. If the request is an OPTIONS request, then according to the CORS spec we must at least return the Access-Control-Allow-Methods headers with a value containing a comma-separated list of allowed verbs. We can optionally specify which custom headers the browser is allowed to send along with the actual request by adding the Access-Control-Allow-Headers header, and we can control the length of time the policy (consisting in this case of the origin, allowed methods, and allowed headers) can be cached by the browser.

In order to post to this action from our website, we have defined the helper function shown in Figure 7.

function corsPost(url, data, callback, returnType) {
    var jqxhr = $.post(url, data, callback, returnType)
    .error(function (jqXhHR, status, errorThrown) {
        if ($.browser.msie && window.XDomainRequest) {
            var xdr = new XDomainRequest();
            xdr.open("post", url);
            xdr.onload = function () {
                callback(JSON.parse(this.responseText), 'success');
            };
            xdr.send(data);
        } else {
            alert("CORS is not supported in this browser or from this origin.");
        }
    });
}


Notice that it is very similar to what was used in the GET scenario -- the main difference in the jQuery case is the switch-over to using jQuery.post() instead of jQuery.get(). The error handler that switches over to using IE's XDR in the case of a cross-origin post from IE is also similar. The differences are the "post" verb used in the open() method and that we pass the serialized form data as an argument to the send() method.

With the method in Figure 7 available to our client script, we can call it from our web page, as shown in Figure 8. Notice that for the data parameter, we grab the form being posted and serialize the form data by applying the jQuery serialize() method to it.

function addParade(url, form) {
    corsPost(url+"/AddParade",
$(form).serialize(),
function (parades) { updateParades(parades); },
"json");
}

Now there are some specific issues to consider when using IE's XDR for handling POST. First of all, XDR only supports GET and POST requests and nothing else. Second, it does not support the transmission of custom headers. This last point has an unfortunate ramification. When using XDR to POST to an ASP.NET MVC action as we did, XDR does not send the "Content-Type: application/x-www-form-urlencoded" header that typically accompanies a form post. This means that ASP.NET does not know to parse the payload for form fields and so assigns null values to all of the action's arguments. You need to process the request stream and parse out the fields manually. Figure 9 shows an example of how to manually pull out the form field values within the AddParades action (this is the code that was truncated in Figure 6).

//This code handles POST data from XDomainRequest from Internet Explorer
if (string.IsNullOrEmpty(name))
{
    var postBody = new StreamReader(Request.InputStream).ReadToEnd();
    string[] pairs = postBody.Split('&');
    Dictionary values = new Dictionary();
    for (int i = 0; i < pairs.Length; i++)
    {
        string[] pair = pairs[i].Split('=');
        values.Add(pair[0], Server.UrlDecode( pair[1]));
    }

    name = values.ContainsKey("name") ? values["name"] : "null";
    date = values.ContainsKey("date") ? values["date"] : "null";
    city = values.ContainsKey("city") ? values["city"] : "null";
    state = values.ContainsKey("state") ? values["state"] : "null";
    route = values.ContainsKey("route") ? values["route"] : "null";
    image = values.ContainsKey("image") ? values["image"] : "null";
}

Why Not JSONP?

You may have heard of JSONP, and after reading about CORS you might think these are equivalent approaches. In a nutshell, JSONP uses client-side JavaScript to inject a new SCRIPT element into the web page DOM, where the src attribute of the script element points to a cross-origin server. As browsers do not enforce the SOP for such requests, the script can be downloaded. In JSONP, that script includes the data returned from the service operation serialized in JSON form into a global variable in the script, along with code that invokes a callback method, defined within the client web page. When the script element is loaded, the data is deserialized into an object and passed to your callback function. On the server side, when a request comes in for that script from the browser, it does not return an existing *.js file, but rather the web application code must craft a response that contains that script.

Today, JSONP is the accepted "hack," and though it works, it has a few issues that create a demand for a better solution. For starters, JSONP does not enable you to restrict cross-domain access or protect browser-based clients against cross-domain hijacking because JSONP itself depends on cross-domain script injection.

Additionally, JSONP is difficult to use from clients that are not web browsers because they return a JavaScript script that must be executed in order to get at the returned data. CORS is usable from clients (such as the WebClient class in .NET) that are not web browsers because the return payload contains only data (e.g., JSON serialized).

CORS, while still in working draft form, has become the accepted spec for enabling and controlling cross-origin requests and is supported across all the major browsers (with minor differences for IE). It has gained such acceptance because it enforces a contract between both the browser and the server application that must agree to allow the cross-origin request.

Why Not Use a Proxy?

A common solution to cross-origin issues with SOP is to make all service requests flow through the web application and have the web application make the requests of the service on the server side, then integrate the response back to the client. Although this solution is valid, it creates extra, often unnecessary load on the web application, which is forced to become a proxy in addition to its other duties. With CORS you enable better load distribution because the web server application does not have to get involved in requests for more data -- the browser gets its data directly from the services. In the case of Azure, using CORS this way also means you can scale the web roles hosting your ASP.NET or MVC site independently of the roles hosting your service, as well as perform deployment upgrades on the tiers independently.

Origin Reached

Having seen CORS in action for an Azure-hosted website, you will probably agree that it offers an elegant solution to providing public or semi-restricted cross-origin-friendly REST APIs. I encourage you to download the code provided with this article to explore the Float Fan solution in greater detail and get your hands dirty with CORS on Azure!

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish