Cross-Domain Calls for a WCF Service

As I develop more services for mobile devices (and mobile web in particular), cross-domain calls are becoming more frequent. Essentially, a cross-domain call happens when a web page uses JavaScript to access resources at a different URL. This can be as complex as hosting services completely separate from the app in another web site, or in another subdomain or on another port, and also can happen simply because the web page is calling a service over SSL.

The problem occurs because the browser only trusts calls from the exact same domain, unless the server says that it trusts your domain (or all domains). This is by design.

So the resolution is to have the server tell the clients what domains it trusts for cross-site calls. Seems simple, but of course there is more to it than that.

When the browser detects a cross-domain call, it behaves differently if your service is configured correctly. If you were expecting your JavaScript code to perform a POST to a REST URL, that is no longer what happens, at least initially. Instead of performing the POST, the browser actually sends an OPTIONS verb call to your REST URL. This is called a preflight request. Your service needs to respond to the preflight request containing the OPTIONS verb and let the browser know which parameters are acceptable on a cross-site call. If everything matches up and your client is in an allowed domain, then the browser performs the original POST that you expected and everything continues on normally.

If your service is not configured correctly, the browser doesn’t cooperate and you don’t see anything at all. Again, this is by design.

Configuring The Service

From an HTTP point of view, what we need to do in code is add the following name/value headers to our HTTP responses:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Max-Age

The first, Access-Control-Allow-Origin, needs to be added to any request so the browser knows to use the OPTIONS verb for cross-site calls, which just error out otherwise with an XmlHttpReqeust Error 101 in JavaScript. The other three need to be added in response to the OPTIONS verb request. 

The Access-Control-Allow-Origin header can be added with one or more values. If you expect any client anywhere to call your services, like Google Maps does, you want to add * for the value, allowing all callers. Otherwise, the most secure way to use Access-Control-Allow-Origin is to set it for the specific domains you want to accept. Say your services are at https://service.yoursitehere.com, and your client site is http://www.yousitehere.com, you want to set the value to http://www.yoursitehere.com.

The code adds the Access-Control-Allow-Methods header to tell the browser which HTTP verbs your calls will accept as cross-domain.

By adding the Access-Control-Allow-Headers header, you control exactly which HTTP headers are allowed in the actual request which follows the preflight request.

The Access-Control-Max-Age header tells the browser how long to cache the preflight call result before doing it again during this browser session. I set this long to avoid multiple preflight requests.

In my case, I’m using configuration-less WCF services as described here. In my experience much of the pain with WCF in the past has been with the complex configurations, and this option circumvents that problem substantially.

The C# Code

So this is how it’s done programmatically, in Global.asax in my case:

 protected void Application_BeginRequest(object sender, EventArgs e)
 {
  HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin" , ”http://www.yoursitehere.com”);

 if (HttpContext.Current.Request.HttpMethod == "OPTIONS" )
 {
 //These headers are handling the "pre-flight" OPTIONS call sent by the browser
 HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods" , "GET, POST" );
 HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers" , "Content-Type, Accept" );
 
 
 HttpContext.Current.Response.AddHeader("Access-Control-Max-Age" "1728000" );
 HttpContext.Current.Response.End();
 }
 }
 

I put it Global.asax in order to avoid having each request handle multiple verbs and calling the handler in each service call. The downside to this is that any non-service request also gets the additional header and processing as well. For me, there are only service requests in this app so this is not a big deal.

The Client

I’ve regularly used jQuery to access services from client HTML code. It’s not complicated, the important parts are formatting the call to send JSON properly in response to our preflight request, which includes the dataType, contentType and data parameters to the $.ajax call.

 $.ajax({
 url: "https://www.yoursitehere.com/person/add",
 type: "POST" ,
 dataType: "json" ,
 contentType: "application/json" ,
 data: JSON.stringify(personObject),
 success: function (data) {
 //Process success here 
 },
 error: function (jqXHR, textStatus, errorThrown) {
 //process errors here 
 }
 });
 

Tools

If you are debugging REST services, you need some good tools. I have used Fiddler, the Chrome Developer Tools (the Network panel in particular) and the Chrome app Advanced REST Client Application successfully.

References

This post is the basis of the fix I’m describing

W3C description of how the HTTP process works

FireFox and CORS, also a lot of good info on HTTP headers

Posted on: 2 Comments

2 Responses

  1. Ron says:

    Hey,

    Nice article, I’ve written a similar thing which didn’t work and tried your sln. (i.e. copy the relevant code) and still get same origin policy error on chrome, with configuration-less WCF service and a website that use this service, what do I miss ???
    Thanks,

    Ron

    • davetrux says:

      Have you tried setting Access-Control-Allow-Origin to * instead of a specific site name? Cross-domain policy is tricky, and even the slightest differences can cause it to kick in. Using * is much more permissive but still an option. Access-Control-Allow-Origin has to already be in the header before the OPTIONS request will be made. That’s why it’s outside the OPTIONS check in the method above. Are you seeing the OPTIONS request happening in the Chrome Dev tools?

      Make sure you are looking at the headers in the Chrome Dev tools as well, usually that’s where you will find the discrepancy. Create a client function that calls something cross-domain that works, like Google Maps, and examine the headers, comparing what is returned by their service to your results and see if you can find a difference.