Wednesday 8 April 2015

Configuring CORS in MVC Web Api 2

This article will describe how developers can handle the restrictions of the Same-Origin Policy that exists in all browsers today by relaxing these restrictions using CORS, Cross-origin resource sharing. Developers that have worked some with Javascript and Ajax calls, for example using jQuery, know about the limitations that are imposed on browsers for scripts to abide the "same-origin policy". If a script written in Javascript tries to access a remote resource on another host, or even another website and/or port on the same machine, chances are high that the Ajax call will although executed, not be returned to the caller (calling script). Typically if a developer checks the Developer Tools in a browser (usually F12), the following error pops up:

XMLHttpRequest cannot load http://localhost:58289/api/products/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:62741' is therefore not allowed access.

This error is because the remote resource, in this case the remote service which is another ASP.NET Web API 2 site on the same machine (localhost), does not allow this origin because of the missing 'Access-Control-Allow-Origin' header in the response. The origin itself is the requesting address and will be sent in the request as the 'Origin' header. The request in this particular case was:

Accept:*/*
Accept-Encoding:gzip, deflate, sdch
Accept-Language:en-US,en;q=0.8,nb;q=0.6,sv;q=0.4
Cache-Control:max-age=0
Connection:keep-alive
Host:localhost:58289
Origin:http://localhost:62741
Referer:http://localhost:62741/index.html
User-Agent:Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36

Note the origin request header above. I have enabled CORS on the remote site on the same machine but configured it to deny this address. First, to enable CORS in a MVC Web API 2 website/webapplication, install the following nuget package: Microsoft ASP.NET Web API 2.2 Cross-Origin Support 5.2.3

Install-Package Microsoft.AspNet.WebApi.Cors

Next up, edit the class WebApiConfig in the App_Start folder:

   public static void Register(HttpConfiguration config)
   {

            config.EnableCors(); 
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
  }

The line that was added here in addition to the default setup is the line:
config.EnableCors(); 
It is possible to allow all calls to this remote site by setting up the web.config file with the following in <system.webServer><httpProtocol>:

    <customHeaders>
      <add name="Access-Control-Allow-Origin" value="*" />
    </customHeaders>

However, this will actually not always be desired as often one knows which origin that will call this remote resource (site) and it is possible to do this programatically. So for now, commenting out this line in web.config and in the following example we use the [EnableCors] attribute:

using ProductsApp.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Cors;
using System.Web.Http.Description;

namespace ProductsApp.Controllers
{

    [EnableCors(origins: "http://localhost:62742", headers: "*", methods: "*")]
    public class ProductsController : ApiController
    {

        private readonly Product[] products = new Product[]{
            new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 },
            new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M },
            new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M }
        };

        public IEnumerable<Product> GetAllProducts()
        {
            return products; 
        }

      
        public async Task<IHttpActionResult> GetProduct(int id)
        {
            Product product = await GetProductAsync(id);
            if (product == null)
            {
                return NotFound(); 
            }
            return Ok(product); 
        }

        private async Task GetProductAsync(int id)
        {
            await Task.Delay(1000); 
            var product = products.FirstOrDefault(p => p.Id == id);
            return product;
        }      

    }

}

The following CORS setup was applied to the entire Web API v2 controller, which inherits from API Controller:
[EnableCors(origins: "http://localhost:62742", headers: "*", methods: "*")]
Here, the orgins property can list up (comma-separated) the allowed origins. Wildcards can be used here. The headers and methods configure further the setup of CORS. The [EnableCors] attribute can be applied to a controller or to a method inside the Web API v2 controller for fine-grained control. It is possible to do custom policy of CORS by using instead of [EnableCors] attribute a custom class that implements the interface
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
                AllowMultiple = false)]
public class EnableCorsForPaidCustomersAttribute :
  Attribute, ICorsPolicyProvider
{
  public async Task GetCorsPolicyAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
  {
    var corsRequestContext = request.GetCorsRequestContext();
    var originRequested = corsRequestContext.Origin;
    if (await IsOriginFromAPaidCustomer(originRequested))
    {
      // Grant CORS request
      var policy = new CorsPolicy
      {
        AllowAnyHeader = true,
        AllowAnyMethod = true,
      };
      policy.Origins.Add(originRequested);
      return policy;
    }
    else
    {
      // Reject CORS request
      return null;
    }
  }
  private async Task IsOriginFromAPaidCustomer(
    string originRequested)
  {
    // Do database look up here to determine if origin should be allowed
    return true;
  }
}
Note here that we can do a database lookup inside the method IsOriginFromAPaidCustomer above. The class inherits from attribute, so one will use this attribute instead. We can add properties for further customization. Using a database-lookup will let us have control to which callers we allow to do a CORS call from their (Java)scripts without having to recompile, since the EnableCors attribute specifies the allowed origins hardcoded. There are further details of CORS, but this is really what is required to get successful ajax calls like the one below to work and avoiding the CORS violation.

    <script>

        $(document).ready(function () {

            var uri = 'http://localhost:58289/api/products/'; 

            $.ajax({
                url: uri,
                success: function (data) {
                    $.each(data, function (key, item) {
                        $('<li>', { text: formatItem(item) }).appendTo($('#products'));
                    });
                }
            });

            function formatItem(item) {
                return item.Name + ': $' + item.Price + ' ID: ' + item.Id;
            }

        });

    </script>

If it is desired to not use CORS (Cross Origin Resource-Sharing) for a given method or controller, use the [DisableCors] attribute. It is also possible to set up [EnableCors] with setting methods = "", i.e. no methods (HTTP Verbs) should be allowed CORS.