Autorización en WebApi… ¿y si fuera como la de Asp.Net Core 1.0?

[Actualizado] El paquete de NuGet depende ahora de Microsoft.AspNet.Authorization (actualmente en versión 1.0.0-rc1-final) y esto implica algunos cambios. He actualizado el post para reflejarlos.

Una de las muchas novedades que nos ofrece Asp.Net Core 1.0 es el rediseño de los filtros de autenticación. Se deja atrás el concepto de autorización por roles o usuarios y se introduce un nuevo concepto de autorización por políticas. Cada una de estas políticas define que elementos quiere tener en cuenta para autorizar al usuario. Puede ser la presencia de un determinado claim, que un claim tenga uno o varios valores específicos, o incluso que el nombre del usuario o el rol tengan unos valores determinados (no olvidemos que los roles o el nombre del usuario siguen siendo claims ). Además se introduce también el concepto de un servicio de autorización (IAuthorizationService) que podremos inyectar en nuestros controladores para validar si un usuario está autorizado o no a realizar una acción, no sólo teniendo en cuenta las características del usuario, sino el propio estado del recurso al que se quiere acceder.

Estas políticas las definiremos de esta forma:

// Add Authorization policies
services.AddAuthorization(options =>
{
    options.AddPolicy("MiPolitica", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("department", "sales");
    });
    options.AddPolicy("Mayor18", policy =>
    {
        policy.Requirements.Add(new MinimumAgeRequirement(18));
    });
});

En Asp.Net Core 1.0, esta configuración se hace en la clase Startup, dentro del método ConfigureServices. Como seguramente ya sabrás, este método lo que hace es configurar y registrar una serie de clases para que estén disponibles para el mecanismo integrado de inyección de dependencias, y puedan ser utilizadas por la propia infraestructura e incluso por el propio desarrollador.

Una vez definidas las políticas, se define con el atributo Authorize la política que se quiere aplicar a nivel de controlador o acción. De hecho, se ha creado una sobrecarga en el constructor de este conocido atributo para poder definir el nombre de la política que se va a utilizar para validar la autorización.

[Authorize("MiPolitica")]

La verdad es que toda esta nueva infraestructura nos permite abordar escenarios realmente complejos que en las versiones actuales de WebApi y Mvc exigirían mucho código personalizado.

¿Lo puedo utilizar en WebApi hoy?

Pero toda esta infraestructura no está disponible en los proyectos de WebApi en los que estamos trabajando hoy. Tenemos que seguir utilizando el atributo Authorize de toda la vida en el que sólo podemos especificar los roles o los usuarios que están autorizados.

¿O no?

Gracias al paquete de NuGet Acheve.Web.Http.Authorization podemos tener a nuestra disposición en los proyectos de WebApi toda la potencia de la autorización basada en políticas que nos ofrece Asp.Net Core 1.0.

Sólo hay que tener en cuenta una cosa importante, y es que, por defecto, WebApi no nos proporciona un mecanismo de inyección de dependencias integrado en el framework, y, por lo tanto, para que funcione todo esto estamos obligados a configurar nuestro contenedor de inyección de dependencias preferido con WebApi y a registrar en el los tipos que hacen posible que todo esto funcione.

Configurar un contenedor de inyección de dependencias externo en WebApi es sencillo. Sólo tenemos que asignar el Adapter para nuestro contenedor a la propiedad DependencyResolver de la configuración de WebApi.

Por ejemplo con Autofac, la configuración sería algo parecida a esto:

public class Startup
{
  public void Configuration(IAppBuilder app)
  {
    var builder = new ContainerBuilder();

    // STANDARD WEB API SETUP:

    // Get your HttpConfiguration. In OWIN, you'll create one
    // rather than using GlobalConfiguration.
    var config = new HttpConfiguration();

    // Register your Web API controllers.
    builder.RegisterApiControllers(Assembly.GetExecutingAssembly());

    // Run other optional steps, like registering filters,
    // per-controller-type services, etc., then set the dependency resolver
    // to be Autofac.
    var container = builder.Build();
    config.DependencyResolver = new AutofacWebApiDependencyResolver(container);

    // OWIN WEB API SETUP:

    // Register the Autofac middleware FIRST, then the Autofac Web API middleware,
    // and finally the standard Web API middleware.
    app.UseAutofacMiddleware(container);
    app.UseAutofacWebApi(config);
    app.UseWebApi(config);
  }
}

A partir de este momento, nuestros controladores pueden definir dependencias en el constructor que serán resueltas por el contenedor.

Si instalamos el paquete de NuGet Acheve.Web.Http.Authorization.Autofac, podremos utilizar un método de extensión sobre el ContainerBuilder de Autofac que nos permitirá registrar nuestras políticas exactamente de la misma manera que en Asp.Net Core 1.0.

builder.UsePolicyAuthorization(options =>
{
    options.AddPolicy(Policies.Sales, policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("department", "sales");
    });
    options.AddPolicy(Policies.Over18Years, policy =>
    {
        policy.Requirements.Add(new MinimumAgeRequirement(18));   
    });
});

[Actualizado]
A partir de este momento, podremos usar los atributos definidos en el ensamblado Microsoft.Asp.Net.Authorization (este ya es Asp.Net Core!) para definir las políticas de autorización de nuestros controladores y métodos de acción. De hecho, el atributo se llama igual que el definido por WebApi, por lo que tendremos que utilizarlo definiendo el nombre completo del tipo.

    [Microsoft.AspNet.Authorization.Authorize]
    [RoutePrefix("products")]
    public class ProductsController : ApiController

Pero todavía nos falta una cosa. El nuevo modelo de Asp.Net Core permite que estos atributos no sean atributos de autorización del framework (lo que por otra parte nos permite reutilizarlos en una plataforma diferente). Así que si no hacemos nada más, realmente WebApi no sabría qué hacer con ellos. Para solucionarlo, configuraremos un filtro global en la configuración de WebApi que será el que se encargue de descubrir estos atributos e integrarlos en la autorización de WebApi.

config.Filters.Add(new UseAspNetCoreAuthorizationModelAttribute());

[Final actualizado]

Voy a ir escribiendo una serie de posts sobre el uso de toda esta nueva infraestructura de autorización para descubrir toda la potencia que nos ofrece. Pero lo más importante es que todo lo que veamos, lo vamos a poder utilizar tanto en WebApi 2 como en Asp.Net Core 1.0.

El código fuente de los paquetes de NuGet está aquí: https://github.com/hbiarge/Acheve.Web.Http.Authorization e incluye ejemplos de uso de este mecanismo de autorización en un proyecto de WebApi 2.

WebApi: Tests de integración con diferentes identidades

tldr;
Acabo de publicar un pequeño paquete de NuGet para facilitar los test de integración de WebApi cuando incluyen peticiones autenticadas y autorización por claims.
Lo puedes encontrar aqui: Acheve.Owin.Testing.Security
Y el código fuente aquí: Github

 

En general, no me gusta hacer test unitarios de mis controladores de WebApi. Me da la sensación que me aportan muy poco valor. Hay que mockear un montón de infraestructura, haciendo que los tests sean farragosos de configurar (por muchos métodos de extensión que creemos para facilitarnos el trabajo).

Una de las soluciones que más me gusta en estos escenarios es la de hacer test de integración del api. Juanma (aka @gulnor) lo comentaba por aquí (post de lectura muy recomendada) hace un tiempo, y yo no puedo estar más de acuerdo con esta visión.

“La complejidad reside en la configuración de los componentes”

El componente que suelo utilizar para estos test de integración es el paquete de NuGet Microsoft.Owin.Testing. Básicamente me ofrece un host basado en Owin para el api. Muy sencillo de configurar con su clase Startup y muy rápido de ejecutar.

Pero el problema es cuando el api, no sólo requiere peticiones autenticadas, sino que además incluye algún mecanismo de autorización basado en roles claims y hay determinados endpoints que tienen un comportamiento u otro en base a esta información. Desde luego, este es el escenario perfecto en el que los test de integración aportan muchísimo. Pero no es fácil de configurar. ¿Cómo puedo incluir de forma sencilla en mis test de integración los claims del usuario que quiero suplantar al realizar la petición?

La verdad es que he utilizado diferentes aproximaciones en diferentes proyectos, pero aquí te voy a contar la que más me ha gustado.

¿Dónde configuro el mecanismo de autenticación de mi api?

Antes de entrar en faena, déjame que haga una pequeña reflexión sobre este punto. Creo que es importante.

Con WebApi voy a poder “hostear” mi api de diferentes maneras. El propio framework ya permitía el concepto de Self Host cuando se publicó su versión 1. Esto quiere decir que no necesito un IIS para exponer el api, sino que lo puedo hacer en cualquier tipo de aplicación: consola, servicio de Windows, etc. Además, con la aparición de OWIN y “katana”, es todavía más fácil utilizar diferentes “hosts” para el api.

La idea es que mi api debe incluir la lógica de autorización, para lo que seguramente, necesitará un usuario autenticado para poder saber quién es y que permisos tiene sobre el api. Sobre todo en el escenario que planteábamos en el que hay que discriminar si el usuario que hace la petición está autorizado o le tenemos que devolver un 401 o un 403.

Pero en realidad es el host el que debe decidir qué mecanismo concreto de autenticación  se ha de utilizar. Es decir, qué elemento de la petición Http se debe utilizar para identificar al usuario y conocer cuales son sus credenciales (en el sentido de claims). En un host podré utilizar tokens bearer en la cabecera estándar “Authorization”, en otro una cabecera personalizada, en otro autenticación integrada de Windows o en otro autenticación básica. O incluso una combinación de ellas. Pero en cualquier caso, mi api, una vez autenticado el usuario por el mecanismo que considere el host debería responder de forma consistente a los requisitos de autorización.

Creando un middleware de autenticación personalizado

Por lo tanto, la aproximación está clara. El TestServer que me ofrece el paquete de NuGet Microsoft.Owin.Testing  no es más que un host de mi api en el que voy a configurar un mecanismo de autenticación personalizado que me permita, de forma sencilla, establecer las credenciales del usuario que quiero suplantar en la petición.

No voy a entrar en este momento en los detalles de cómo implementar un middleware de autenticación (lo dejamos para otro post), pero no es complicado. Básicamente nos vale con saber que, para el escenario de los test de integración, la información de los claims del usuario va a viajar codificada en Base64 en la cabecera estandar de autorización. Nada complicado. Pero cuidado. Nada seguro tampoco. !Ni se te ocurra utilizar este middleware en producción!

Los test de integración

La configuración del TestServer es sencilla. Lo único que tengo que hacer es crear la clase Startup que va a definir el comportamiento del servidor y allí, configurar el middleware de autenticación personalizada justo antes de usar mi api. Por ejemplo, así:

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseTestServerAuthentication();

            var config = new HttpConfiguration();
            Sample.Api.WebApiConfiguration.Configure(config);
            app.UseWebApi(config);
        }
    }

En los ejemplos, voy usar xUnit. Cada test que se ejecuta, crea una nueva instancia de la clase que lo contiene. Además utiliza mecanismos estándar (nada de atributos) para la inicialización de los tests (en el constructor de la clase) y para la limpieza (implementar la interfaz IDisposable).

Por lo tanto, en el contexto de mis test, crearé la instancia del TestServer en el constructor de la clase de test de la siguiente forma:

        private readonly TestServer _server;
        private readonly HttpClient _userHttpCient;

        public VauesWithDefaultUserTests()
        {
            _server = TestServer.Create<Startup>();
            _userHttpCient = _server.HttpClient
                .WithDefaultIdentity(Identities.User);
        }

La magia ocurre en ese método de extensión WithDefaultIdentity . En este caso, añade una cabecera por defecto a todas las peticiones que se hagan con el HttpClient que configura. En esta cabecera, viajará la información necesaria para que el middleware de autenticación que hemos configurado cree la instancia del ClaimsPrincipal que representará la petición.

De la misma forma, hay otro método de extensión sobre la clase RequestBuilder que nos permitirá configurar las credenciales de una única petición:

        [Fact]
        public async Task WithRequestBuilder()
        {
            var response = await _server.CreateRequest("api/values")
                .WithIdentity(Identities.User)
                .GetAsync();

            response.EnsureSuccessStatusCode();
        }

Los dos métodos de extensión aceptan el mismo tipo de parámetro: un IEnumerable<Claim> en el que definiremos la información que tenga nuestro usuario en el contexto de las peticiones al api. Por ejemplo, en los ejemplos de antes, he creado una clase estática que definirá todas las identidades que quiera utilizar en el contexto de los tests:

    public static class Identities
    {
        public static readonly IEnumerable User = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, "1"),
            new Claim(ClaimTypes.Name, "User"),
        };
    }

Con estos dos métodos de extensión y con el middleware configurado en la clase Startup que utiliza el servidor de test, es todo lo que necesitamos para poder realizar peticiones autenticadas a cualquier api.

Sencillo, ¿no? Desde luego, ya no tienes excusa para no tener unos bonitos test de integración de tu WebApi.

¡Que lo disfrutes!

Si tenéis curiosidad, el código fuente está aquí: https://github.com/hbiarge/Acheve.Owin.Testing.Security. En la carpeta samples podrás encontrar un ejemplo completo de uso de la librería.