CORS and IIS

July 17, 2011, 8:05 p.m.

Last time, I wrote about Cross-Origin Resource Sharing (CORS). This time I'll show how it can be implemented with Microsoft's Internet Information Services (IIS), ASP.Net and jQuery.

Let's assume we have two very separate web applications:

  • http://cooking/, a website that is full of delicous cooking recipes
  • http://shopping/, a website that allows users to make shopping lists

First, we implement http://shopping/ by adding 127.0.0.1 shopping to the hostfile and creating an empty ASP.NET web application with only one file (list.aspx):

<%@ Page Language="C#" ContentType="application/json" %>
[
    { "amount": "2", "name": "Apples" },
    { "amount": "4", "name": "Bananas" },
    { "amount": "8", "name": "Cookies" }
]

That was fast.

Now the guys from http://cooking/ would like to show shopping lists on their site and allow users to add ingredients for the recipe they're looking at to their list. They add the following Javascript to their recipe-detail page to retrieve a user's current shopping list:

$.ajax({
    url: 'http://shopping/list.aspx',
    dataType: 'json',
    success: function(data) {
        // render shopping list items to page
    }
});

Unfortunately this call is blocked due to the same origin policy. Luckily the girls from http://shopping/ want the whole world to use their shopping lists so they add an HTTP Header Access-Control-Allow-Origin: * to their response. This can be done in the IIS Manager

  • IIS6: in the Web's properties dialog under 'HTTP Headers'
  • IIS7: in the 'HTTP Response Headers' section of the 'Features View', or web.config

Here is what the response from http://shopping/list.aspx looks like after the change:

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/7.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Access-Control-Allow-Origin: *
Date: Sun, 17 Jul 2011 14:21:59 GMT
Content-Length: 124

[
    { "amount": "2", "name": "Apples" },
    { "amount": "4", "name": "Bananas" },
    { "amount": "8", "name": "Cookies" }
]

Now everybody, including the script on http://cooking, is allowed to retrieve the shopping list.

Additionally it should be possible for users on http://cooking to click a link 'add to shopping list' while they browse the ingredients for a recipe. The cooking-guys add the following Javascript to their site and have it run when users click on these links.

$.ajax({
    type: 'POST',
    url: 'http://shopping/list.aspx',
    contentType: 'json',
    dataType: 'json',
    data: JSON.stringify({
        amount: 16,
        name: 'Dark Chocolates'
    }),
    success: function (data) {
        // render shopping list items to page
    },
});

This will POST the new item (16 Dark Chocolates) to the list but since it is a different domain, the browser will first query Access-Control with an OPTIONS request:

OPTIONS http://shopping/list.aspx HTTP/1.1
Host: shopping
Origin: http://cooking
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

In particular it checks for the permission to POST with a header content-type. Fortunately the http://shopping site responds with

HTTP/1.1 200 OK
Allow: OPTIONS, TRACE, GET, HEAD, POST
Server: Microsoft-IIS/7.5
Public: OPTIONS, TRACE, GET, HEAD, POST
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Headers: Content-Type
Date: Sun, 17 Jul 2011 17:49:22 GMT
Content-Length: 0

so the browser goes ahead with the original POST:

POST http://shopping/list.aspx HTTP/1.1
Host: shopping
Content-Type: json; charset=UTF-8
Referer: http://cooking/
Content-Length: 38
Origin: http://cooking

{"amount":16,"name":"Dark Chocolates"}

It is possible that everything already works for you after configuring the two additional Access-Control- headers (-Allow-Methods and -Allow-Headers). IIS7 has an OPTIONSVerbHandler configured by default that will respond with all configured custom headers, including the Access-Control- headers. In IIS6, there is no such mapping and depending on your setup it is likely that you need to explicitly allow the OPTIONS verb. In IIS6 this setting is certainly not obvious but you can find it in the website's Properties -> Home Directory -> Configuration... -> Mappings -> .aspx -> Verbs -> Limit to. If you're using a different handler (e.g. WCF) you'll have to add it to the appropriate mapping (e.g. '.svc'). This will allow your ASP.Net application to receive OPTIONS requests. One way to handle these requests is to simply end the response in the Global.asax's Application_BeginRequest method, optionally also setting the Access-Control- headers:

void Application_BeginRequest(object sender, EventArgs e)
{
    var ctx = HttpContext.Current;
    if (ctx.Request.HttpMethod == "OPTIONS")
    {
        //ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
        ctx.Response.End();
    }
}

Some other things to look out for when it doesn't work:

  • Don't just rely on your browser plugins (Firebug etc.) to find out what happened. Use a debugging proxy (e.g. Fiddler, Charles, etc.) or network tools (e.g. Wireshark). There is a really fine line between request-blocked-by-browser (i.e. no request actually happened), OPTIONS request problems and GET or POST problems. The plugins' error messages are not always helpful (e.g. '405 Method not allowed')
  • Depending on your ajax-call setup and content type (json, xml, etc.) you might have to add the Accept header to the list of -Allow-Headers.
  • If there is a problem with the OPTIONS request, check the <system.webServer><handlers> section, there may be handlers with verb limits configured (IIS7 only)
  • If there are still problems with the OPTIONS request, check the <system.web><httpHandlers> section for custom handlers
  • Don't forget about any firewall or security tools that are filtering requests with specific (or unknown) verbs or headers (e.g. UrlScan)

Here is the full sample code: cors-and-iis.zip

about

subscribe