RSS

The CORS

12 Oct

Intro
It’s a common practice to use Ajax, Silverlight or Flash to communicate with the server on the same domain. However, at times, you are required to access another domain, and this may be interpreted by browsers as a security issue. This post specifically addresses how to do so using CORS: “Cross-origin resource sharing”.

You might say that the purpose of CORS is to help browsers understand that accessing a resource from a different domain is legit. Consider this example: There are web services available from a certain other domain. You wish to access them from a browser using Ajax, but the browser interprets this a security issue and forbids it. CORS is the now-standard way to perform some sort of a handshake between the browser and the server before the Web Services can be consumed. The browser queries the server whether it’s OK to access a Web Service and acts according to the reply. Note, that although the server is what needs to be configured to allow access to your client, it is the browser that performs the limitation and will stop the request if no such access was granted. A different client such as a .NET application need not worry about CORS, unless for some reason the developer is required to support it, as the server does not enforce clients to use CORS.

Supporting CORS on the server side is merely done by sending several Response Headers to the client browser. On IIS this is done either by adding headers to the outgoing responses or by tweaking web.config as required.

Problem 1: Cross-site scripting
Ingredients:

  • 1 Web Service which needs to be consumed.
  • 1 IIS to host that Web Service on a domain other than the client browser domain.
  • 1 IIS to host a client script trying to consume that Web Service.
  • 1 Firefox browser (tested version is 14.0.1) and 1 Chrome browser (tested version is 20).

Let’s view the problem: our client JavaScript runs on ‘localhost’ and will try to consume a Web Service running in domain ‘OtherServer’. This is the server code:

using System.Web.Services;

[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class MyService : System.Web.Services.WebService
{
    [WebMethod]
    public string HelloWorld(string name)
    {
        return "Hello " + name;
    }
}

web.config setup:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0">
    </compilation>
    <webServices>
      <protocols>
        <add name="HttpGet" />
        <add name="HttpPost" />
      </protocols>
    </webServices>
  </system.web>
</configuration>

The client code (using jQuery 1.7.2):

$.ajax({
    type: 'get',
    url: 'http://OtherServer/MyServer/MyService.asmx/HelloWorld',
    data: { name: "Joe" },
    contentType: 'text/plain',
    success: function (data) {
        alert("Data Loaded: " + data);
    }
});

Note that the client code does not send nor expect a JSON type. this is deliberate and will be explained later on why.

Here is the result of this request:

Running the above client results in somewhat conflicting results on Firefox and Chrome. In FF, exploring Firebug Net tab shows as if the request and response went as expected. As you can see in the screenshot below, the request was sent from localhost to ‘otherserver’, and returned with a 200 (OK). But, as you can also see the response text is empty. Server side debugging proves that the request arrived as expected. However client side debugging shows a somewhat different picture as the status of the request returns as 0 and not 200. In Chrome, the Network tab shows an almost immediate error and sets the status of the request to ‘(canceled’). When reverting to a more “native” use of XMLHttpRequest rather then jQuery, it seems like the status of the request is also returned as 0 instead of 200.

BTW: running this exact client script from within ‘otherserver’ domain will work OK, with the expected xml returned.

Solution 1: Access-Control-Allow-Origin
In order to make this work, the server has to send back the “Access-Control-Allow-Origin” response header, acknowledging the client. The value of this response header can be either ‘*’ or an actual expected client domain, thus allowing a more controlled CORS interaction, if required. You may have noticed in the previous screenshot that the browser automatically sends an ‘Origin’ header when the request is a cross-site request. That is the name of the domain that the server should return so that the request will be allowed.

Sending back the “Access-Control-Allow-Origin” response header can be done either by adding a single line of code, as below:

using System.Web;
using System.Web.Services;

[System.Web.Script.Services.ScriptService]
public class MyService : System.Web.Services.WebService
{
    [WebMethod]
    public string HelloWorld(string name)
    {
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "http://localhost");
        return "Hello " + name;
    }
}

Or by tweaking the web.config to return that response header, for all requests in this sample (IIS 7 or above):

  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>

Note: If you do decide to specify an explicit origin, you must provide the origin as it is sent in the request headers. In this example it is ‘http://localhost&#8217;, but naturally this has to be the actual domain name a consuming script is running from.

Now that we resend the request, we receive the result as expected (I used the web.config tweak in this example):

Problem 2: Sending JSON types
In a previous post I described the “magic” behind sending JSON strings to the server, so that the server will perform auto binding to server side types. Let’s try it now, with a cross domain server request:

$.ajax({
    type: 'POST',
    url: 'http://OtherServer/MyServer/MyService.asmx/HelloWorld',
    data: JSON.stringify({ name: "Joe" }),
    contentType: 'application/json',
    dataType: 'json',
    success: function (data) {
        alert("Data Loaded: " + data);
    }
});

Running this client script fails. Note the traffic:

As you can see, in this case the browser did not send a POST request as expected, but an OPTIONS request. This procedure is called “Preflight”, which means that the browser sends an implicit request to the server, asking whether the request is legit. Only if the server replies that the request is indeed legit, using response headers again, will the browser continue with the original request as planned. So, looking at the screenshot above, you can see several things:

  1. The browser sends an OPTIONS request.
  2. The browser sends two new request headers: “Access-Control-Request-Method” with a value of “POST”, and “Access-Control-Request-Headers” with a value of “content-type”. This quite clearly states that the browser is asking whether it can use POST and whether it can send a “content-type” header in the request.
  3. The server replies that “POST” is indeed allowed (amongst other methods), and maintains the origin as ‘*’ like before.
  4. The server doesn’t tell the browser anything about the content-type header request, which is the problem here.

In this case the browser was not satisfied with the preflight response from the server and therefore the original request was not sent.

Important: As you can see from problem/solution 1, not all requests are preflighted. FF docs sum this as follows. A request will be preflighted in the following cases:

  • It uses methods other than GET or POST. Also, if POST is used to send request data with a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain, e.g. if the POST request sends an XML payload to the server using application/xml or text/xml, then the request is preflighted.
  • It sets custom headers in the request (e.g. the request uses a header such as X-PINGOTHER)

So by sending a content-type of ‘application/json’, we have made this request perform a “preflight” request.

Solution 2: Access-Control-Allow-Headers
The solution to this problem is simply to tweak the server to acknowledge the content-type header, thus causing the browser to understand that the content-type header request is legit. Here goes:

  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Content-Type" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>

When we try our request again it works as expected:

Note: You may wish to limit or explicitly detail which methods of requesting information are supported by using the “Access-Control-Allow-Methods” response header like so:

  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Methods" value="POST,GET,OPTIONS" />
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Content-Type" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>

Problem 3: (Basic) Authentication
If the server resource is secured and requires authentication for accessing it before a client can actually consume it, you’ll have to modify your request accordingly, or you’ll end-up with a 401 authorization response from the server.

For example, if you protect your Web Service with Basic Authentication (and disable Anonymous Authentication, of course), then you have to have your request conform to the Basic Authentication requirements, which means that you have to add a request header of “Authorization” with a value that begins with “Basic” followed by a Base64 string of a combined username:password string (which is by no way encrypted – but that is another matter). So borrowing Wikipedia’s sample, if we have an “Aladdin” user account with a password of “open sesame”, we have to convert to Base64 the string of “Aladdin:open sesame” and add it as an Authorization request header. The request header should be: “Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==”.

UPDATE: Wikipedia’s relevant page was modified till this post was published their excellent Aladdin sample was somewhat changed.

$.ajax({
    type: 'POST',
    url: 'http://OtherServer/MyServer/MyService.asmx/HelloWorld',
    data: JSON.stringify({ name: "Joe" }),
    contentType: 'application/json',
    beforeSend: function ( xhr ) {
        xhr.setRequestHeader("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
    },
    dataType: 'json',
    success: function (data) {
        alert("Data Loaded: " + data);
    }
});

We also have to change the “Access-Control-Allow-Headers” to allow a request header of “Authorization”, or the OPTIONS will fail again:

  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Methods" value="POST,GET,OPTIONS" />
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Content-Type,Authorization" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>

However, this change is not sufficient, at least not for “preflighted” requests. If you change the security settings in your IIS web site to Basic Authentication but require a preflighted request, it turns out that the preflight itself will fail authentication. This is because the preflight request does NOT send the “Authorization” header. IIS automatically responds with a 401 (Unauthorized) as it cannot authenticate the preflight request. If you would test this on a Firefox browser, the preflight will fail and the original request will not be sent:

Interesting enough, on Chrome this works fine, despite the returned 401 from the server:

In order to get this to work on FF, I made several attempts using various combinations of the following (you can read about all of them in the jQuery.ajax documentation):

  • I added withCredentials to the request (if you use this you have to have your server return a Access-Control-Allow-Credentials: true, and have Access-Control-Allow-Origin return an explicit origin).
  • I added username/password to the request.
  • I added the patch designated to fix the known FF bug of getAllResponseHeaders() not returning the response headers correctly.
  • Also added the “Access-Control-Allow-Credentials” to the server response.
  • Changed the “Access-Control-Allow-Origin” from “*” to the “http://localhost&#8221; (request origin) as the FF docs specify.

I also reverted from jQuery to the “native” XMLHttpRequest to no avail. Nothing I tried made it work on FF.

So, which browser is “right”? Is Firefox right to block the request because a 401 was received, or is Chrome right for ignoring the 401 on the preflight? After all, it would make sense that because the Authorization header is not sent on the preflight, the browser will be “clever enough” to ignore the 401. However, as you can read in this reported FF bug it turns out that the CORS spec requires that the server will return a 200 (OK) on the preflight before proceeding with the original request. According to that, Firefox has the correct implementation and Chrome has a bug (if you follow the reported bug you’ll learn that this is actually a webkit bug, although this is yet to be concluded.)

Solution 3: Handle OPTIONS “manually”
Here comes the bad part. While for Integrated Application Pools, you can code a custom module to bypass IIS behavior and return a 200 for the preflight, you cannot do that for a Classic Application Pool. In an Integrated Application Pool, ASP.NET is integrated into IIS’ pipeline, allowing you to customize the authentication mechanism in this particular case. However a Classic Application Pool means that module code will run only after IIS has authorized the request (or rather – failed to authorize in this case).

First, let’s review the Integrated Application Pool patch, in the form of an HttpModule:

public class CORSModule : IHttpModule
{
    public void Dispose() { }

    public void Init(HttpApplication context)
    {
        context.PreSendRequestHeaders += delegate
        {
            if (context.Request.HttpMethod == "OPTIONS")
            {
                var response = context.Response;
                response.StatusCode = (int)HttpStatusCode.OK;
            }
        };
    }
}

The code above is very non-restrictive – it allows all preflights (or other usage of the OPTIONS verb) to get away without authentication. So you better consider revising it to your needs.

The web.config in IIS7 Integrated App Pool now incorporates the following to support the HttpModule:

  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Methods" value="POST,GET,OPTIONS" />
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Content-Type,Authorization" />
      </customHeaders>
    </httpProtocol>
    <modules>
      <add name="CORSModule" type="CORSModule" />
    </modules>
  </system.webServer>

The result is working as expected:

For Classic Application Pool, there isn’t an easy solution. All my attempts to reconfigure IIS to allow OPTIONS through despite the Basic Authentication have failed. If anyone knows how to do this – please comment. A minor attempt to dispute over the decision to reject a preflight based on the authorization issue has failed. In particular to the Classic Application Pool issue (i.e. that older web servers cannot be tweaked to allow the OPTIONS request), the response was that we should wait till these servers are obsoleted (??)

However, you might consider a different solution – you can revert from the default IIS Basic Authentication module back to Anonymous authentication, and handle the authorization yourself (or use an open source like MADAM.) This solution means that preflights will not fail authentication, but you are still able to require credentials for accessing the different resources (explanation can be found below the code):

void Application_AuthenticateRequest(object sender, EventArgs e)
{
    bool authorized = false;
    string authorization = Request.Headers["Authorization"];
    if (!string.IsNullOrWhiteSpace(authorization))
    {
        string[] parts = authorization.Split(' ');
        if (parts[0] == "Basic")//basic authentication
        {
            authorization = UTF8Encoding.UTF8.GetString(Convert.FromBase64String(parts[1]));
            parts = authorization.Split(':');
            string username = parts[0];
            string password = parts[1];

            // TODO: perform authentication
            //authorized = FormsAuthentication.Authenticate(username, password);
            //authorized = Membership.ValidateUser(username, password);
            using (PrincipalContext context = new PrincipalContext(System.DirectoryServices.AccountManagement.ContextType.Machine))
            {
                authorized = context.ValidateCredentials(username, password);
            }

            if (authorized)
            {
                HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(new System.Security.Principal.GenericIdentity(username), null);
            }
        }
    }

    if (!authorized)
    {
Response.AddHeader("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", Request.Url.Host));
        Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized;
        Response.End();
    }
}

As you can see from the code above, Basic Authentication is handled “manually”:

  • Line 1 shows that this code runs upon a request authentication.
  • Line 4 queries whether there’s an Authorization header.
  • Lines 7-8 proceed to authentication only for Basic Authentication (you can customize this to your needs). This particular example demonstrates Basic Authentication.
  • Lines 16-21 demonstrate different methods of authentication. You have to choose what’s best for you or add your own.
  • Line 23-26 are optional and can be used to populate the current context’s User property with an identity that you might need in your Web Service later on.
  • Lines 30-35 return a 401 and a WWW-Authenticate header which indicate to the client which authentication methods are allowed (that is, in case the user was not authenticated).

This solution is a variant of the solution described in a previous post I made about having a “Mixed Authentication” for a website.

Summary

I really can’t tell, but it seems like CORS is something that is here to stay. Unlike JSONP which is a workaround that utilizes a security hole in todays browsers, one that might be dealt with someday, CORS is an attempt to formalize a more secure way to protect the browsing user. The thing is that CORS is not trivial as I would have preferred it to be. True, once you get the hang of it, it makes sense. But it has it’s limitations and complexity to get it right without spending too much time to configure it correctly. As for the IIS Classic App Pool and non-anonymous configuration issue, well, that is something that seems to be like a real problem. You can try to follow this thread and see if something comes up.

Credits

Lame, but this post’s title is credited to The Corrs.

About these ads
 
3 Comments

Posted by on 12/10/2012 in Software Development

 

Tags: ,

3 responses to “The CORS

  1. Ilya

    15/01/2013 at 08:16

    Thanks for this article! It really helped me to solve the problem I had (solution 3).

     
  2. Rajan R.G

    30/06/2013 at 14:00

    Thanks for the article. Using your Solution 3, OPTIONS request successfully passed but the POST request gets Unauthorized error at this line
    if (context.Request.HttpMethod == “OPTIONS”)
    My services is REST WCF with windows authentication.

     

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
Follow

Get every new post delivered to your Inbox.

Join 60 other followers

%d bloggers like this: