[ASP.NET Web API] Autenticación basada en tokens y líos con el CORS

No era la primera vez que implementaba este tipo de autenticación en un proyecto de ASP.NET Web Api + ASP.NET MVC + OWIN y ya sabía (o al menos creo saber) los retos de trabajar con estos pipelines, digo, líneas como config.SuppressDefaultHostAuthentication(); en el  WebApiConfig hacen su magia, conocí la clase OAuthAuthorizationServerProvider y en su momento dejé en mis favoritos entradas como esta como fuente de consulta. Con el tema de CORS creí que todo estaba resuelto con el uso del atributo EnableCors que hizo esto más sencillo.

La semana pasada este requerimiento surgió de nuevo pero con un componente adicional: El token es solicitado en un cliente desde otro dominio. De entrada parece que nada cambia, pero si que lo hace, o al menos si hay que agregar unas cuantas líneas. Sabía que en mis favoritos aún estaban los enlaces a las fuentes que había usado como referencia en implementaciones anteriores, así que volví a consultar una serie de tutoriales muy, muy completos de este blog. Y por primera vez me percaté que existen dos “enable cors”! (o algo así) uno a nivel del pipeline de OWIN y el que siempre uso en Web API. El que siempre uso, el de Web API, viene en el paquete Microsoft.AspNet.WebApi.Cors y el que no conocía, el de Katana, viene en el paquete Microsoft.Owin.Cors.

¿Qué diferencia tienen WebApi.Cors y Owin.Cors?

No encontré nada en la documentación oficial, pero viendo el nombre, entendiendo algo de OWIN y habiendo usado Katana, se puede suponer que el primero es solo para el Framework Web API y lo que hay en el, es decir, los ApiControllers. El segundo es un middleware que agrega soporte para el resto del pipeline, como el middleware para generar los tokens! Igual apliqué un poco de GOD (google oriented development) y encontré una explicación similar en Stack Overflow.

¿Y cómo se usan los dos?

Lo primero que hice, desde el desconocimiento, fue agregar en el Startup la línea app.UseCors(CorsOptions.AllowAll); y en el momento de probar surgió el primer error, error que rompe la parte que ya funcionaba (mensaje en la consola de Chrome):
Control-Allow-Origin' header contains multiple values 'https://foo.com, https://foo.com', but only one is allowed. Origin 'https://foo.com' is therefore not allowed access. ¡Y tiene todo el sentido! pues en dos puntos del pipeline estamos agregando estas cabeceras.

Una solución puede ser quitar la configuración del CORS en el WebAPIConfig, es decir, la línea config.EnableCors(); y dejar que el OWIN se configure global con con Katana. ¡Pero no me gusta! no me gusta por el AllowAll (least privilage), aunque parece claro que se puede cambiar (más sobre esto a continuación), y porque se supone que la configuración del CORS de Web API es para el Framework! digo, por algo existe, no?

Como el único  lugar donde necesito el CORS es en el endpoint que genera el token, pensé que era más fácil y mejor quitar el UseCors() del Startup y agregar las cabeceras directamente en el response, sabía que no podía ser el primero que hacía esto y así fue, la segunda solución la encontré en Stack Overflow y pasa por sobrescribir el método MatchEndpoint del OAuthAuthorizationServerProvider, yo terminé con una implementación como la siguiente:

        private static void SetOriginHeaders(IOwinContext context)
        {
            if (!context.Response.Headers.Keys.Contains("Access-Control-Allow-Origin"))
                context.Response.Headers.AppendCommaSeparatedValues("Access-Control-Allow-Origin", "https://localhost:44369");
            if (!context.Response.Headers.Keys.Contains("Access-Control-Allow-Headers"))
                context.Response.Headers.AppendCommaSeparatedValues("Access-Control-Allow-Headers", "Accept", "Content-Type", "Authorization", "Cache-Control", "Pragma", "Origin");
            if (!context.Response.Headers.Keys.Contains("Access-Control-Allow-Methods"))
                context.Response.Headers.AppendCommaSeparatedValues("Access-Control-Allow-Methods", "GET", "POST", "PUT", "DELETE", "OPTIONS");
        }

        public override Task MatchEndpoint(OAuthMatchEndpointContext context)
        {
            if (context.IsTokenEndpoint &&
                (context.Request.Method.Equals("OPTIONS", StringComparison.InvariantCultureIgnoreCase)
                || context.Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase)))
            {
                SetOriginHeaders(context.OwinContext);
                context.MatchesTokenEndpoint();
                //context.RequestCompleted();
                return Task.CompletedTask;
            }

            return base.MatchEndpoint(context);
        }

        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            context.Validated();
        }

        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {		
            //Se valida el usuario...

            SetOriginHeaders(context.OwinContext);
        }

Con el código anterior funciona sin problemas, pero nuevamente pensé: ¡Esto debe poderse hacer desde el middleware!

Configurar el Owin.Cors

Después de dar muchas vueltas llegué a esta solución, que ahora me parece muy sencilla y obvia. Se puede crear una instancia de CorsOptions y usar un PoliciResolver, que no es más que una función que permite tener acceso a la información del request (IOwinRequest) y retornar un objeto que encapsula los Headers HTTP en cómodas propiedades. El código del Startup se así (el orden de los middlewares SI altera el resultado):

    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseCors(new CorsOptions
            {
                PolicyProvider = new CorsPolicyProvider
                {
                    PolicyResolver = PolicyResolver
                }
            });
            ConfigureAuth(app);
        }

        private static Task<CorsPolicy> PolicyResolver(IOwinRequest request)
        {
            if (request.Path.Value == "/api/token")
            {
                //Estos valores pueden ser recuperados de un archivo de configuracion
                var corsPolicy = new CorsPolicy
                {
                    Headers = { "Accept", "Content-Type", "Authorization", "Cache-Control", "Pragma", "Origin" },
                    Methods = { "POST", "OPTIONS" },
                    Origins = { "https://localhost:44369" }
                };
                return Task.FromResult(corsPolicy);
            }
            return Task.FromResult<CorsPolicy>(null);
        }
    }

Conclusiones

  • Existe un middleware de Katana que habilita CORS en todo el pipeline de OWIN, Microsoft.Owin.Cors
  • Owin.Cors y WebApi.Cors pueden existir en el mismo proyecto
  • El OAuthTokenProvider no hace parte de WebApi, por eso se necesita el Owin.Cors

Referencias y enlaces de interés

Espero les sea de utilidad.

Hasta el próximo post.

Un comentario sobre “[ASP.NET Web API] Autenticación basada en tokens y líos con el CORS

Deja un comentario