Implementando CORS en ASP.NET Web API

Seguramente, los que leeis geeks ya estaréis al tanto de la salida de ASP.NET WebAPI para la siguiente versión de ASP.NET  MVC, de hecho, en esta misma comunidad, gente como Jose Maria Aguilar y Eduard Tomás ya han hecho unos cuentos posts interesantísimos sobre el tema, no dejéis pasar la oportunidad de leerlos y aprender de ellos, os lo recomiendo encarecidamente, si me lo permitís. A mi en este post, me gustaría tratar un tema importante cuando nos enfrentamos a hacer un API REST que pueda ser utilizado por otros sitios sin tener que recurrir a JSON padding, por comodidad y por la limitación del verbo a utilizar. Cross-Origin Resource Sharing, usualmente conocido como CORS,  es una especificación que permite habilitar de forma confiable el acceso across-domain-boundaries de forma que no tengamos que utilizar jsonp y soportando cualquier tipo de verbo para la operación.

Antes de intentar explicar detenidamente como funciona y como implementar CORS haremos un pequeño ejemplo con Web API que trataremos de consumir desde otro sitio y veremos como este no puede ser consumido directamente, para, posteriormente ir implementando CORS directamente. La idea del ejemplo es extremadamente simple, un método GET y un POST para un supuesto producto.

 

 

public class ProductController

    :ApiController

{

 

    public IEnumerable<Product> GetProducts()

    {

        var products = new List<Product>()

        {

            new Product(){Id  =1, Name ="Windows Power Shell in Action",Description ="Second Edition. Bruce Payette"},

            new Product(){Id  =1, Name ="Git in Action",Description =""}

        };

 

        return products;

    }

    

    public HttpResponseMessage<Product> PostProduct(Product product)

    {

return new HttpResponseMessage<Product>(product, HttpStatusCode.OK);

    }

}

 

Una vez hecho esto, y el consiguiente mapeo de la ruta, podríamos desde nuestro sitio web utilizar este API, por ejemplo, el código de la siguiente vista nos mostraría como recuperar y mostrar la lista de productos de este API.

 

<div id="products">

Products...

</div>

 

<script type="text/javascript">

    $.ajax({

        url: "http://localhost:1042/api/product/",

        type: "GET",

        success: function (data) {

            $.each(data, function (index, item) {

                $("#products").after("<p>" + item.Name + "</p>");

            });

 

        },

        error: function (jqXHR, settings, exception) {

            $("#products").html("error loading products...");

        }

    });

</script>

Sin embargo, como muchos ya sabéis, si intentamos hacer esto mismo desde otro sitio no tendremos el resultado esperado, puesto que esa llamada está fuera del dominio del cliente y los navegadores actuales restringen esta funcionalidad. Tal y como comentamos anteriormente, podríamos utilizar JSONP para solventar este problema, sin embargo, aunque es una medida que funcionan tiene varios inconvenientes. En primer lugar implica un trabajo de cliente adicional y en segundo lugar solamente estaría soportado el verbo GET. Para solventar este problema, veremos como funciona CORS y posteriormente como implementarlo en ASP.NET Web API

Como funciona CORS?

CORS se basa en dos peticiones fundamentales,  un cross-orgin-request y un preflight-request, los cuales expondremos de una forma sencilla a continuación.

  • cross-origin-request consiste básicamente en el envío por parte de quien solicita un recurso compartido de una cabecer Origin, con el valor del cliente que desea consumir ese recurso. Por parte del servidor,  se hace la lectura de esa cabecera y se comprueba que puede utilizar un determinado recurso. Una vez comprobado, el servidor en la respuesta, envía una cabecera Access-Control-Allow-Origin con el valor del cliente admitido, valor no case sensitive que tienen que coincidir con el emisor. En realidad, este proceso se podría complicar un poco más incluyendo credenciales, no obstante, nosotros no lo utilizaremos, si alguien quiere ampliar la información puede hacerlo aquí.
  • preflight-request es un request utilizado para, a mayores de garantizar el origen , ver que métodos pueden utilizarse.

 

Implementando CORS en Web API

 

Todos los que habéis leido a Eduard y José Maria sabréis que entre las bondades de ASP.NET Web API está la extensibilidad, pués bien, una de las características de esta extensibilidad es la de los MessageHandler. Estos elementos, expuestos en WebAPI generalmente por medio de DelegatingHandler nos permiten introducirnos en el pipeline del proceso de una petición HTTP de una forma muy sencilla.

En nuestro caso, empezaremos por crearnos nuestro handler tal y como vemos.

 

public class CORSMessageHandler

    :DelegatingHandler

{

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

    {

        return base.SendAsync(request, cancellationToken);

    }

}

 

Si registramos este handler, tal y como vemos a continuación en nuestro Application_Start, y ponemos un punto de ruptura en el método SendAsync veremos como ya tenemos una manera de “enchufarnos” al procesamiento de una petición a nuestro servicio de Web API.

 

GlobalConfiguration.Configuration.MessageHandlers.Add(new CORSMessageHandler());

 

Para empezar intentaremos resolver el cross-origin-request, si ha leído la especificación, básicamente lo que haría el navegador es incluir en nuestra petición una cabecera Origin, cabecera con el valor del dominio que intenta llamar al API. Para otorgar al dominio como dominio válido, la petición, además de la respuesta debería incluir una cabecera llamada Access-Control-Allow-Origin con el valor de ese dominio válido. Modificando nuestro handler anterior como sigue, ya tenemos, sin ninguna tarea adicional en el cliente este trabajo resuelto, simple y sencillo. Haga la prueba y fíjese como, automáticamente se hace la inclusión de esta cabecera

 

public class CORSMessageHandler

   :DelegatingHandler

 

   protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

   {

       var corsEnabled = request.Headers

                                .Where(h => h.Key == "Origin")

                                .Any();

       if (corsEnabled)

       {

           return base.SendAsync(request, cancellationToken)

                       .ContinueWith<HttpResponseMessage>(t =>

                       {

                           var response = t.Result;

                           response.Headers.Add("Access-Control-Allow-Origin", request.Headers.GetValues("Origin"));

 

                           return response;

                       });

       }

       else

           return base.SendAsync(request, cancellationToken);

   }

 

El segundo de los request especiales de CORS es el preflight, request que en nuestro caso no se produce puesto que no tenemos ninguna operación que no sea GET. Si hubiéramos implementado una operación con un verbo distinto, por ejemplo el POST de nuestra API Rest la primera petición que llegaría al servidor se corresponde con el request de preflight, que si ha visto la especificación se corresponde con un request con el verbo OPTIONS. La respuesta a esta petición  debería incluir un pare de cabeceras como especifica la documentación con los métodos y valores especiales de headers aceptados. Una vez hecho el preflight, la llamada se realizará de forma normal. Pues bien, una vez terminada, más o menos, la explicación, la implementación ( un poco ASM ) sería la siguiente:

 

public class CORSMessageHandler

    :DelegatingHandler

{

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

    {

        var corsEnabled = request.Headers

                                 .Where(h => h.Key == "Origin")

                                 .Any();

        bool preflight = request.Method == HttpMethod.Options;

 

        if (corsEnabled)

        {

            if (preflight)

            {

                return Task.Factory.StartNew<HttpResponseMessage>(() =>

                {

                    var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK);

 

                    response.Headers.Add("Access-Control-Allow-Origin", request.Headers.GetValues("Origin"));

                    response.Headers.Add("Access-Control-Allow-Headers", request.Headers.GetValues("Access-Control-Request-Headers"));

                    response.Headers.Add("Access-Control-Allow-Methods", "POST");

 

                    return response;

                });

            }

            else

            {

                return base.SendAsync(request, cancellationToken)

                            .ContinueWith<HttpResponseMessage>(t =>

                            {

                                var response = t.Result;

                                response.Headers.Add("Access-Control-Allow-Origin", request.Headers.GetValues("Origin"));

 

                                return response;

                            });

            }

        }

        else

            return base.SendAsync(request, cancellationToken);

    }

}

 

NOTA: El código anterior es una implementación muy simple y sin refactoring alguno, no debería utilizarse sin trabajarla un poco. Fíjese en el hardcoded de los métodos soportados etc etc…

Bueno, esto parece que ha sido todo, espero que os haya gustado…

 

Saludos

Unai

5 comentarios sobre “Implementando CORS en ASP.NET Web API”

  1. Muy buen artículo Unai! No conocía mucho sobre CORS y me has dado algunas ideas para un proyecto.

    La verdad es que cada día es más habitual saltarse las restricciones de Cross Domain Calls, tanto en intranets de clientes como el poder montar un api REST en Azure y ser llamada desde otros dominios :)

    Un saludo

  2. Gran post, Unai!

    Desde luego CORS tiene una pinta estupenda, es una forma mucho más simple para saltarse las limitaciones cross-domain que las opciones actuales.

    Lo que aún no he conseguido es echarlo a andar desde un cliente IE9, ni siquiera tocándole las opciones de seguridad. Tendré por aquí alguna configuración en “modo paranoico” que me impide conectar 😉 Pero con Chrome va estupendo.

    Ah, y gracias por la referencia! :-)

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *