次の方法で共有


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]

Comments

  • Anonymous
    February 20, 2012
    Great article Carlos!  Exactly what I was looking for.  Thanks for the clear walkthrough.
  • Anonymous
    February 20, 2012
    Doesn't setting cors support do the trick?jQuery.support.cors = true;
  • Anonymous
    February 20, 2012
    Edmund, that flag tells jQuery to use an alternative object for making cross-domain calls if the browser doesn't natively support it on its XmlHttpRequest implementation. For example, in IE8 that will cause jQuery to switch from XmlHttpRequest to the XDomainRequest object (in IE10 they "fixed" it in a way that XmlHttpRequest can be used for all requests). Regardless of the object on the client, the server still needs to "play the game" and return the appropriate headers so that the browser will allow such requests to be made.
  • Anonymous
    February 20, 2012
    So that allows for wider browser support (IE....). Great article!
  • Anonymous
    February 29, 2012
    One comment: this approach enables CORS for all operations in all controllers. To have a finer granularity for cross-domain access, you can look at my other post at blogs.msdn.com/.../implementing-cors-support-in-asp-net-web-apis-take-2.aspx
  • Anonymous
    March 20, 2012
    The comment has been removed
  • Anonymous
    April 01, 2012
    Thanks for the article. I tried to implement it on MVC3 and WebApi (via nuget). It all compiles great and I can debug the behavior.But when I do a POST, IIS (7.5) throws an 500 error. Do I need to make any settings on the IIS so it can handle the Headers/Options verb?
  • Anonymous
    June 13, 2012
    Hello Sir,I am trying to make calls to MVC Web API from an another web project using JavaScript Ajax.I am getting "No Transport" Error because of Cross-Domain calls. I found "Cors Handler" here and it is working fine in Firefox and Chrome but when I am running the same sample provided by you in Internet Explorer 9, then it is returning error.Can you help me why microsoft itself is not supporting this in IE?What should I do? This sample is not running in IE9 :-(
  • Anonymous
    June 13, 2012
    Yes, unfortunately IE9 does not implement CORS in the default XmlHttpRequest object which is used by most frameworks. jQuery does have a workaround - namely set jQuery.support.cors = true, and it will switch to use the XDomainRequest object instead. Depending on which framework you're using, you may need to use something else. Or if you're using the JS objects directly, you'll first need to check whether you're under IE9- (IE10 works fine) and use XDomainRequest, and use the "normal" object for the other browsers.
  • Anonymous
    June 25, 2012
    Do you have any updated source for the latest ASP .NET Web API RC?
  • Anonymous
    July 01, 2012
    Yes, I just updated the code to run in the ASP.NET Web API Release Candidate. There is a link at the top of the post with the location for the post with the changes.
  • Anonymous
    October 20, 2012
    Hey,when I run your exact solution and click to get all it just write error ?? !!what is the problem ?
  • Anonymous
    November 21, 2012
    Great walk through, exactly what I was looking for.Thanks.
  • Anonymous
    February 26, 2013
    Great article. How i can do for two mvc project running on different port? as HttpRequestMessage is not available into mvc3.
  • Anonymous
    July 25, 2013
    The comment has been removed
  • Anonymous
    September 09, 2013
    I'm having the same trouble other folks have mentioned, namely, that the handler doesn't seem to respond to OPTIONS requests (see stackoverflow.com/.../webapi-delegatinghandler-not-called-for-preflight-cors-requests). Any suggestions? Have other folks found solutions?
  • Anonymous
    March 06, 2014
    Hi The above post helped me to get the results in both IE and Chrome. However it doesn't work in firefox. I use Firefox 27.0.1Any config options I need to enable or handle differently.
  • Anonymous
    March 12, 2014
    CORS is now natively supported in WebAPI, so you don't need to use this solution anymore. The post at msdn.microsoft.com/.../dn532203.aspx has a good description about how to use it.
  • Anonymous
    May 14, 2014
    Hi CarlosFigueira, Thanks for the article . its really good. But i have one problem, i am not able to call any CURD after publishing on IIS. Do i have to change any settings for that, i am gettingXMLHttpRequest cannot load 10.0.0.109/.../student. The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:2617, *', but only one is allowed. Origin 'http://localhost:2617' is therefore not allowed access.
  • Anonymous
    May 21, 2014
    Gitesh, you don't need to use the workaround from this post anymore - CORS is now natively supported in the ASP.NET Web API framework. Take a look at www.asp.net/.../enabling-cross-origin-requests-in-web-api for more information.