Implementing CORS support in ASP.NET Web APIs

This post was written for the Beta version of the ASP.NET MVC 4. The updates needed to make them run in the latest bits (Release Candidate) are listed in this new post .

The code in this post is published on the MSDN Code Gallery.

By default, a web page cannot make calls to services (APIs) on a domain other than the one where the page came from. This is a security measure against a group of cross-site forgery attacks in which browsing to a bad site could potentially use the browser cookies to get data from a good site which the user had previously logged on (think your bank). There are many situations, however, where getting data from different sites is a perfectly valid scenario, such as mash-up applications which need data from different sources.

There are a few ways to overcome this limitation. JSONP (JSON with Padding) uses the fact that <script> tags don’t have the cross-domain restriction, and if the server is willing to send the data back differently (i.e., padding the JSON response by wrapping it inside a function call), then the client can create a named function, and add a new <script> tag to the page DOM and that function will be called with the result from the service. It’s a very simple protocol, and has been used successfully in many applications.

The main problem (or limitation) of JSONP is that it only supports GET requests – <script> tags will cause the browser to issue a HTTP GET request for the script referenced by its “src” attribute. In situations where other HTTP verbs are required, JSONP simply doesn’t work. There are some alternatives to this, such as proxying the requests through a service (the cross-domain restrictions are imposed by the XmlHttpRequest object in the browsers, and don’t apply to “general”, server-side, code), but they usually introduce a new component in the system which is error-prone.

CORS (Cross-Origin Resource Sharing) is a new specification which defines a set of headers which can be exchanged between the client and the server which allow the server to relax the cross-domain restrictions for all HTTP verbs, not only GET. Also, since CORS is implemented in the same XmlHttpRequest as “normal” AJAX calls (in Firefox 3.5 and above, Safari 4 and above, Chrome 3 and above, IE 10 and above – in IE8/9, the code needs to use the XDomainRequest object instead), the JavaScript code doesn’t need to worry about “un-padding” responses or adding dummy functions. The error handling is also improved with CORS, since services can use the full range of the HTTP response codes (instead of 200, which is required by JSONP) and the code also has access to the full response instead of only its body.

Cross-domain calls in ASP.NET Web APIs

Note: If you are completely new to ASP.NET Web API then I recommend that you check out some of the tutorials.

As of its Beta release, there is no native support for cross-domain calls in ASP.NET Web APIs. It’s fairly simple, though, to implement those – the comments sample has a simple JSONP formatter. This post shows one way to implement CORS in a Web API project, using a message handler. Message handlers can intercept the requests as they go through the pipeline and either modify them as they go through or respond to the request immediately, bypassing the rest of the pipeline, so they make a good candidate for this scenario.

A very quick CORS intro before diving into the code: requests for cross-domain resources have an additional HTTP header, “Origin”, which identifies the domain where the page was loaded from. If the server supports CORS, it should respond to such requests with an additional response header, “Access-Control-Allow-Origin”. There is also a special request, the preflight request, which is sent prior to the actual request, which uses the HTTP “OPTIONS” verb, which asks the server which HTTP methods and request headers it supports in cross-domain requests (using the “Access-Control-Request-Method” and “Access-Control-Request-Headers” request headers, respectively), and the server must respond with the appropriate headers (“Access-Control-Allow-Methods” and “Access-Control-Allow-Headers” response headers, respectively).

Let’s start with a new ASP.NET MVC 4 / Web API project (add a solution folder, since we’ll add a new project to do the cross-domain calls). In order to get something useful out of the controller from the template, I changed it a little to be able to test it, by storing the “values” in an in memory list.

  1. public class ValuesController : ApiController
  2. {
  3.     static List<string> allValues = new List<string> { "value1", "value2" };
  4.  
  5.     // GET /api/values
  6.     public IEnumerable<string> Get()
  7.     {
  8.         return allValues;
  9.     }
  10.  
  11.     // GET /api/values/5
  12.     public string Get(int id)
  13.     {
  14.         if (id < allValues.Count)
  15.         {
  16.             return allValues[id];
  17.         }
  18.         else
  19.         {
  20.             throw new HttpResponseException(HttpStatusCode.NotFound);
  21.         }
  22.     }
  23.  
  24.     // POST /api/values
  25.     public HttpResponseMessage Post(string value)
  26.     {
  27.         allValues.Add(value);
  28.         return new HttpResponseMessage<int>(allValues.Count - 1, HttpStatusCode.Created);
  29.     }
  30.  
  31.     // PUT /api/values/5
  32.     public void Put(int id, string value)
  33.     {
  34.         if (id < allValues.Count)
  35.         {
  36.             allValues[id] = value;
  37.         }
  38.         else
  39.         {
  40.             throw new HttpResponseException(HttpStatusCode.NotFound);
  41.         }
  42.     }
  43.  
  44.     // DELETE /api/values/5
  45.     public void Delete(int id)
  46.     {
  47.         if (id < allValues.Count)
  48.         {
  49.             allValues.RemoveAt(id);
  50.         }
  51.         else
  52.         {
  53.             throw new HttpResponseException(HttpStatusCode.NotFound);
  54.         }
  55.     }
  56. }

Next, let’s add a new project to the solution, and ASP.NET Empty Web Application, and add a NuGet reference to the jQuery project. With that, we can add an HTML page and add some controls to test the controller.

image

With that ready, we can bind the “click” events from the buttons to make calls to the controller, as shown below (this isn’t beautiful code by any means, with a little more time I’d have combined many of the functions, but for a quick sample, this works)

  1. <script type="text/javascript">
  2.     var valuesAddress = "https://localhost:3227/api/Values";
  3.     $("#getAll").click(function () {
  4.         $.ajax({
  5.             url: valuesAddress,
  6.             type: "GET",
  7.             success: function (result) {
  8.                 var text = "";
  9.                 for (var i = 0; i < result.length; i++) {
  10.                     if (i > 0) text = text + ", ";
  11.                     text = text + result[i];
  12.                 }
  13.  
  14.                 $("#result").text(text);
  15.             },
  16.             error: function (jqXHR, textStatus, errorThrown) {
  17.                 $("#result").text(textStatus);
  18.             }
  19.         });
  20.     });
  21.  
  22.     $("#getOne").click(function () {
  23.         var id = "/" + $("#id").val();
  24.         $.ajax({
  25.             url: valuesAddress + id,
  26.             type: "GET",
  27.             success: function (result) {
  28.                 $("#result").text(result);
  29.             },
  30.             error: function (jqXHR, textStatus, errorThrown) {
  31.                 $("#result").text(textStatus);
  32.             }
  33.         });
  34.     });
  35.  
  36.     $("#post").click(function () {
  37.         var data = "\"" + $("#value").val() + "\"";
  38.         $.ajax({
  39.             url: valuesAddress,
  40.             type: "POST",
  41.             contentType: "application/json",
  42.             data: data,
  43.             success: function (result) {
  44.                 $("#result").text(result);
  45.             },
  46.             error: function (jqXHR, textStatus, errorThrown) {
  47.                 $("#result").text(textStatus);
  48.             }
  49.         });
  50.     });
  51.  
  52.     $("#put").click(function () {
  53.         var id = "/" + $("#id").val();
  54.         var data = "\"" + $("#value").val() + "\"";
  55.         $.ajax({
  56.             url: valuesAddress + id,
  57.             type: "PUT",
  58.             contentType: "application/json",
  59.             data: data,
  60.             success: function (result) {
  61.                 $("#result").text(result);
  62.             },
  63.             error: function (jqXHR, textStatus, errorThrown) {
  64.                 $("#result").text(textStatus);
  65.             }
  66.         });
  67.     });
  68.  
  69.     $("#delete").click(function () {
  70.         var id = "/" + $("#id").val();
  71.         $.ajax({
  72.             url: valuesAddress + id,
  73.             type: "DELETE",
  74.             success: function (result) {
  75.                 $("#result").text(result);
  76.             },
  77.             error: function (jqXHR, textStatus, errorThrown) {
  78.                 $("#result").text(textStatus);
  79.             }
  80.         });
  81.     });
  82. </script>

Time to test the page. After setting both projects as the startup projects in the Visual Studio solution, we can F5 and start clicking on the buttons. Oops, nothing works – they all print the error on the result <div> element.

image

That’s where CORS enters the picture. We need to get the controller to return the appropriate headers, otherwise no cross-domain requests will succeed. Let’s add a new message handler to deal with those requests. The code is shown below. If the request contain the “Origin” header, we’ll treat it as a CORS request, and after dispatching the message through the pipeline (base.SendAsync), we’ll add the “Access-Control-Allow-Origin” header to let the browser know that we’re fine with that request.

  1. protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  2. {
  3.     bool isCorsRequest = request.Headers.Contains(Origin);
  4.     if (isCorsRequest)
  5.     {
  6.         return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>(t =>
  7.         {
  8.             HttpResponseMessage resp = t.Result;
  9.             resp.Headers.Add(AccessControlAllowOrigin, request.Headers.GetValues(Origin).First());
  10.             return resp;
  11.         });
  12.     }
  13.     else
  14.     {
  15.         return base.SendAsync(request, cancellationToken);
  16.     }
  17. }

We also must deal with preflight requests (OPTIONS). If this is the case, we don’t need to send the message through the pipeline, we can respond to it right away, by creating a response message and returning it at the handler itself.

  1. bool isPreflightRequest = request.Method == HttpMethod.Options;
  2. if (isCorsRequest)
  3. {
  4.     if (isPreflightRequest)
  5.     {
  6.         return Task.Factory.StartNew<HttpResponseMessage>(() =>
  7.         {
  8.             HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
  9.             response.Headers.Add(AccessControlAllowOrigin, request.Headers.GetValues(Origin).First());
  10.  
  11.             string accessControlRequestMethod = request.Headers.GetValues(AccessControlRequestMethod).FirstOrDefault();
  12.             if (accessControlRequestMethod != null)
  13.             {
  14.                 response.Headers.Add(AccessControlAllowMethods, accessControlRequestMethod);
  15.             }
  16.  
  17.             string requestedHeaders = string.Join(", ", request.Headers.GetValues(AccessControlRequestHeaders));
  18.             if (!string.IsNullOrEmpty(requestedHeaders))
  19.             {
  20.                 response.Headers.Add(AccessControlAllowHeaders, requestedHeaders);
  21.             }
  22.  
  23.             return response;
  24.         }, cancellationToken);
  25.     }

Finally, we add the CORS handler to the list of message handlers in the global configuration of the application (in global.asax.cs):

  1. protected void Application_Start()
  2. {
  3.     AreaRegistration.RegisterAllAreas();
  4.  
  5.     RegisterGlobalFilters(GlobalFilters.Filters);
  6.     RegisterRoutes(RouteTable.Routes);
  7.  
  8.     BundleTable.Bundles.RegisterTemplateBundles();
  9.  
  10.     GlobalConfiguration.Configuration.MessageHandlers.Add(new CorsHandler());
  11. }

That’s it. We can now call in any CORS-capable browser, as shown below.

image

Security considerations

The cross-domain restrictions in many browsers exist for a reason, so please understand the security implications of enabling such requests with CORS (or JSONP) before doing so in production systems.

Also, the solution shown in this post enables CORS for all actions in all controllers. There is a way to enable them selectively, and you can find more about it in the next post.

[Code in this post]