Thursday, January 24, 2013

Cross-origin resource sharing (CORS)


Overview

Cross-origin resource sharing (CORS) is a mechanism that allows a web page to make XMLHttpRequests to another domain.[1] Such "cross-domain" requests would otherwise be forbidden by web browsers, per the same origin security policy. CORS defines a way in which the browser and the server can interact to determine whether or not to allow the cross-origin request.[2] It is more powerful than only allowing same-origin requests, but it is more secure than simply allowing all such requests.


Scenario

"Site A" needs to send GET/POST request to "Site B"
When "Site A" send a GET/POST request to "Site B" will automatically cancelled that request.
This type of request traditionally would not be allowed under the browser’s same origin policy.


Enable CORS

In order CORS to allow "Site A" to get the response from "Site B", we need to add Access-Control-Allow-Origin response header "Site B".
For Microsoft IIS7, merge this into the web.config file at the root of your application or site:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <system.webServer>
   <httpProtocol>
     <customHeaders>
       <add name="Access-Control-Allow-Origin" value="*" />
     </customHeaders>
   </httpProtocol>
 </system.webServer>
</configuration>
Check this page for more information [enable-cors.org/server.html]



Challenge 1 - Handling a special request

Cross-origin requests come in two flavors:
  • simple requests
  • "special requests" (a term I just made up)

Simple requests are requests that meet the following criteria:
  • HTTP Method matches (case-sensitive) one of: HEAD, GET, POST
  • HTTP Headers matches (case-insensitive): Accept, Accept-Language, Content-Language, Last-Event-ID, Content-Type
  • Content type :application/x-www-form-urlencoded, multipart/form-data, text/plain

If you send a request outside this simple request criteria which is "not-so-simple requests", The browser first issues a preflight request, which is like asking the server for permission to make the actual request.
preflight request will send a OPTIONS request.
Once permissions have been granted, the browser makes the actual request. The browser handles the details of these two requests transparently. The preflight response can also be cached so that it is not issued on every request.



Sample scenario : If you use [angularJS] v1.0.3 to make a $http request, It will add a custom header "X-Requested-With" into the request. Or we may also need to add custom header such as "X-BLA-*".
This kind of request will trigger preflight request which will ask the server permission to make the actual request.
To enable this request to be passed into the server, we need enable Access-Control-Allow-Headers response header However when we enable this Access-Control-Allow-Headers,
It requires us to provide specific origin in Access-Control-Allow-Origin response header.
<add name="Access-Control-Allow-Origin" value="http://localhost" />
<add name="Access-Control-Allow-Credentials " value="true" />
For more information, please check this site [html5rocks.com/cors]

.


Challenge 2 - Anti Forgery Session Token

Another challenge with CORS is dealing with [Anti Frogery Session Token]
There are 2 things you need to provide inside your POST request in order to satisfy Anti Frogery validation on the server.
  • The incoming request has a Request.Form entry called __RequestVerificationToken
  • Session ID which stored on your Cookie.

The server will use Session ID to get verification token and try to match this token with __RequestVerificationToken.

To solve this problem, both client and server need to allow credential to be shared between request and response.
  • Enable Access-Control-Allow-Credentials on the server.
<add name="Access-Control-Allow-Credentials " value="true" />
  • Enable .withCredentials on your request.
        $.ajax({

            // The 'type' property sets the HTTP method.
            // A value of 'PUT' or 'DELETE' will trigger a preflight request.
            type: 'POST',
            data: serializedData,
            
            // The URL to make the request to.
            url: $scope.url,

            xhrFields: {
                // The 'xhrFields' property sets additional fields on the XMLHttpRequest.
                // This can be used to set the 'withCredentials' property.
                // Set the value to 'true' if you'd like to pass cookies to the server.
                // If this is enabled, your server must respond with the header
                // 'Access-Control-Allow-Credentials: true'.
                withCredentials: true
            },
            ....
        });


Limitations

There are still 2 limitations which CORS can't handle at this moment.
  • 302 (Redirect) Response code with special request (Which initialize preflight request).
In Fiddler it will look like this
1. OPTIONS - http://siteB => 404 (not found)
2. POST - http://siteB => 302 (redirect with location)
3. GET - http://siteB/new location => 200 (OK) 
Even in fiddler, we get 200 response status, the browser keep reject this response.

  • Redirect from HTTP to HTTPS (vice versa)
This issue happen when your HTTP request redirects you to HTTPS protocol.
In Fiddler it will look like this
1. GET - http://siteB => 302 (redirect to HTTPS)
2. GET - https://siteB => 200 (OK)
Again, even in fiddler, we get 200 response status, the browser keep reject this response.



Sample code (end to ends)

  • Enable CORS on server side by adding this response header into web.config
<system.webServer>
    <httpProtocol>
      <customHeaders>
         <add name="Access-Control-Allow-Origin" value="http://localhost:55129" />
  <add name="Access-Control-Allow-Credentials " value="true" />
  <add name="Access-Control-Allow-Headers" value="X-Requested-With" /> 
      </customHeaders>
    </httpProtocol>
</system.webServer>

  • Client request using AJAX
  $.ajax({
            // The 'type' property sets the HTTP method.
            // A value of 'PUT' or 'DELETE' will trigger a preflight request.
            type: 'GET',

            // The URL to make the request to.
            url: 'http://siteB,

            // The 'contentType' property sets the 'Content-Type' header.
            // The JQuery default for this property is
            // 'application/x-www-form-urlencoded; charset=UTF-8', which does not trigger
            // a preflight. If you set this value to anything other than
            // application/x-www-form-urlencoded, multipart/form-data, or text/plain,
            // you will trigger a preflight request.
            contentType: 'text/plain',

            xhrFields: {
                // The 'xhrFields' property sets additional fields on the XMLHttpRequest.
                // This can be used to set the 'withCredentials' property.
                // Set the value to 'true' if you'd like to pass cookies to the server.
                // If this is enabled, your server must respond with the header
                // 'Access-Control-Allow-Credentials: true'.
                withCredentials: true
            },

            headers: {
                // Set any custom headers here.
                // If you set any non-simple headers, your server must include these
                // headers in the 'Access-Control-Allow-Headers' response header.
            },

            success: function (data, textStatus, jqXHR) {
                // Here's where you handle a successful response.
                $scope.data = data;
                $scope.$apply();
            },

            error: function (jqXHR, textStatus, errorThrown) {
                // Here's where you handle an error response.
                // Note that if the error was due to a CORS issue,
                // this function will still fire, but there won't be any additional
                // information about the error.
                alert('Woops, there was an error making the request. - with status ' + errorThrown);
            }
        });

  • Or Client request using $http module from angularJS
    $http.get($scope.url, {withCredentials : true}).
          success(function (data, status) {
              $scope.data = data;
              $scope.$apply();
          }).
          error(function (data, status) {
              $scope.data = data || "Request failed";
              $scope.$apply();
          }
        );

No comments: