De NuGet y la gestión de paquetes

Ya hace bastante tiempo que NuGet salió y desde entonces se ha convertido en un compañero inseparable de todos nosotros. Y más que va a serlo cuando vNext salga de forma definitiva. En este post doy por supuesto que conoces NuGet y que lo has usado alguna vez (si no… ¡debes aprender a usarlo ya!). En este post quiero comentar los tres modos de funcionamiento que tiene NuGet y algunas cosillas más con las que me he encontrado.

Funcione NuGet en el modo en que funcione, cuando agregamos un paquete siempre ocurre lo mismo:

  • Se crea (si no existe) un directorio packages a nivel de la solución (localizado en el mismo directorio que el .sln).
  • Se descarga el paquete en dicho directorio
  • Se crea (si no existe) un fichero packages.config en el proyecto al cual se haya agregado el paquete y se añade una línea indicando el paquete agregado.
  • Se añade una referencia en el proyecto que apunta al ensamblado del paquete que se encuentra en el directorio packages. Y si el paquete tiene scripts de instalación adicionales, pues se ejecutan.

Recuerda que NuGet es básicamente un automatizador de agregar referencias en VS. Hace todo aquello que harías tu manualmente (descargar el ensamblado, guardarlo en algún sitio, agregar la referencia y cosas extra como editar web.config) y nada más (o nada menos, depende de como se mire).

Vamos a configurar NuGet para que funcione en el primero de los modos. Para ello abre VS2013 y en Tools –> Options –> NuGet Package Manager desmarca las dos checkboxes que están bajo el título de “Package Restore”. De esta manera NuGet funciona de la forma en que funcionaba originalmente. Y dicha forma consiste en que NuGet no hará nada más que lo que hemos descrito hasta ahora. Eso significa que cuando subas a tu repositorio de control de código fuente el proyecto debes incluir el directorio packages que contiene los binarios de los paquetes instalados por NuGet. Si no lo haces, cuando otra persona quiera descargarse el código fuente el proyecto no le compilará porque no encontrará las referencias a los paquetes NuGet:

image

¿Está todo perdido? Pues no, porque NuGet detectará que hay paquetes referenciados (eso lo sabe mirando el packages.config) que no existen en el directorio packages. Así mostrará el siguiente mensaje en la “Package Manager Console”:

image

Este mensaje aparece porque estamos en una versión nueva de NuGet. Cuando apareció NuGet este mensaje no aparecía y honestamente no sé cual era la solución entonces porque ese era un escenario no soportado: en la primera versión de NuGet los paquetes descargados debían subirse al control de código fuente.

Una versión de NuGet posterior habilitó el segundo modo de funcionamiento de NuGet. Para activarlo se debe pulsar sobre la solución en el “solution explorer” y seleccionar la opción “Enable NuGet Package Restore”:

image

Cuando se pulsa esta opción se crea una carpeta .nuget en la raíz de la solución que contiene tres ficheros (NuGet.config, NuGet.exe y NuGet.targets). Y además se modificarán los ficheros de la solución para agregarles por un lado una entrada nueva dentro de <PropertyGroup>:

  1. <RestorePackages>true</RestorePackages>

Y la ejecución de la tarea para que NuGet descargue los paquetes al compilar la solución:

  1. <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
  2. <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
  3.   <PropertyGroup>
  4.     <ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
  5.   </PropertyGroup>
  6.   <Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
  7. </Target>

De este modo al compilar la solución NuGet se descargará los paquetes que no existan de forma automática. Habilitar esta opción te marcará automáticamente la primera de las checkboxes que habíamos desmarcado antes en Tools –> Options –> NuGet Package Manager.

Así, ahora, una de las preguntas más recurrentes sobre NuGet (¿Tengo que subir los paquetes a mi sistema de control de código fuente?) se respondía ahora diciendo que si los querías subir podías hacerlo sin problemas pero que si no, no era necesario siempre y cuando la solución tuviese habilitada la opción de “Package Restore”. En este último caso la carpeta .nuget si que debías subirla.

En principio con esos dos modos cubrimos la totalidad de los escenarios pero con la versión 2.7 de NuGet agregaron un tercer modo de funcionamiento. Dicho tercer modo es básicamente el modo anterior que acabamos de describir pero automatizado. Ya no tenemos que hacer nada, excepto marcar la segunda de las checkboxes que hemos desmarcado al principio (y es que este es, a partir de la versión 2.7, el modo por defecto de NuGet).

Para verla en acción marca la segunda checkbox, y luego borra la carpeta .nuget. Luego recompila la solución y recibirás un error: “This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is XXX.nugetNuget.targets”. Este error se da porque a pesar de que hemos borrado la carpeta .nuget, tenemos los proyectos todavía configurados para que la usen. Así, que no toca otra: abrir en modo texto los ficheros de proyecto y eliminar las líneas que se nos añadieron antes.

Una vez hecho esto, recargas los proyectos y al recompilar automáticamente NuGet descargará los paquetes. La ventaja de este modo de funcionamiento respecto al anterior es que no es intrusivo: no requiere modificar los ficheros de proyecto.

Si actualmente usas NuGet 2.7 o superior (que es de esperar que sí) y tienes la carpeta .nuget en tu repositorio de código fuente lo mejor que puedes hacer es eliminarla. Y luego modificar los proyectos para quitar las líneas indicadas anteriormente. Y con la segunda checkbox marcada (que es como está por defecto) ya tienes la descarga de paquetes automatizada. Por si tienes alguna duda sobre lo que tienes que eliminar en los ficheros de proyecto (aunque son las líneas mencionadas antes), todo el proceso está documentado en la propia web de NuGet. Si usas team build también son necesarias pequeñas modificaciones en TFS2012 o anterior (en TFS2013 así como Visual Studio Online o bien deploys en Azure web sites el proceso está ya integrado). De nuevo tienes toda la documentación en la web de NuGet sobre como configurar el team build.

De todos modos que tengas habilitada la descarga de paquetes automatizada no te impide colocar los paquetes (la carpeta packages) en el repositorio de control de código fuente: es una opción personal. Colocarlos en el sistema de control de código fuente te evita una dependencia con el propio NuGet (que aunque se cae pocas veces, a veces lo hace). Hace tiempo Juanma escribió en su blog un post sobre los peligros de depender del gestor de paquetes. Hay soluciones más elaboradas como no tener los paquetes en el control de código fuente pero usar un servidor de NuGet corporativo. Aquí ya, cada caso es un mundo.

Proyectos en varias soluciones

Vale, eso es un poco más frustrante y es un aviso más que otra cosa: si tienes un proyecto con paquetes gestionados por NuGet y este proyecto lo tienes en varias soluciones, asegúrate de que todas las soluciones (los ficheros .sln) están en el mismo directorio. En caso contrario puedes tener problemas. Es lógico una vez se entiende que hace NuGet y realmente es difícil que pueda hacer otra cosa que la que hace, así que bueno… es algo a tener en cuenta.

Vamos a reproducirlo paso a paso, para entender que ocurre. Para ello crea un directorio, yo lo he llamado nuroot. Luego crea otra carpeta (yo la he llamado folder1) dentro de nuroot:

image

Ahora crea una solución de VS (una aplicación de consola) dentro de folder1 (yo la he llamado DemoProject). Una vez hecho esto agrega un paquete de NuGet a la solución (p. ej. DotNetZip). Ahora la estructura de paquete debe ser como sigue:

image

El directorio packages está al nivel de la solución, y si miras en el proyecto verás que la referencia al ensamblado (en el caso de DotNetZip el ensamblado se llama Ionic.Zip) apunta al directorio packages. De hecho la referencia se guarda relativa al fichero de proyecto (si abres el .csproj en modo texto lo verás):

  1. <Reference Include="Ionic.Zip">
  2.   <HintPath>..\packages\DotNetZip.1.9.3\lib\net20\Ionic.Zip.dll</HintPath>
  3. </Reference>

Perfecto. Ahora crea otra solución vacía (New Project –> Other Project Types –> Visual Studio Solution –> Empty Solution) y dale el nombre que quieras. Yo la he llamado SecondSolution. Lo importante es que la crees en nuroot, no en folder1. Por defecto VS crea un directorio para la solución, pero vamos a eliminarlo. Ve a nurootSecondSolution y mueve el fichero SecondSolution.sln a nuroot. Luego borra el directorio SecondSoution. En este punto la estructura de directorios es pues la misma de antes, con la salvedad de que en la carpeta nuroot hay el fichero SecondSolution.sln.

Finalmente agrega el proyecto existente (DemoProject) a la solución SecondSolution. ¡Una vez cargues el proyecto verás el mensaje de que faltan paquetes de NuGet! Dale a Restore para que NuGet se descargue los paquetes faltantes y la estructura de directorios será la siguiente:

image

Ten presente que NuGet funciona a nivel de solución. Cuando hemos agregado el proyecto DemoProject a la segunda solución, NuGet ha examinado el fichero packages.config del proyecto y ha visto una referencia a DotNetZip. Luego ha examinado el directorio packages de la solución. Y al estar esta otra solución en otro directorio que la anterior, NuGet no encuentra el directorio packages, y por lo tanto asume que debe descargarse los paquetes. Y al descargarlos es cuando nos aparece el otro directorio packages ahora colgando de nuroot (el directorio donde tenemos SecondSolution.sln).

Si compilas el proyecto todo funcionará pero hay un tema importante ahí. El proyecto DemoProject ya contenía una referencia al ensamblado de DotNetZip y al existir dicha referencia NuGet no la modificará. Es decir, la referencia sigue apuntando donde apuntaba inicialmente (nurootfolder1DemoProjectPackages).

Cierra VS y borra los dos directorios packages. Con esto simulas lo que le ocurriría a alguien que se descargase el código fuente (suponiendo que los paquetes no están subidos en él). Ahora carga SecondSolution.sln otra vez y de nuevo verás que NuGet dice que faltan paquetes (obvio, pues los hemos borrado todos). Restaura de nuevo y verás como NuGet crea otra vez el directorio nurootpackages. Pero el fichero csproj sigue teniendo la referencia a nurootfolder1DemoProject y por lo tanto no encontrará la referencia. Es decir, el código no compilará. Para que te compile debes abirlo con la solución DemoProject.sln, restaurar paquetes (o compilar simplemente, recuerda que al compilar se restauran automáticamente) y entonces ya te compilará (desde ambas soluciones).

En este escenario quizá no te parezca tan grave porque total, haces un readme.txt y que diga “Abrir primero DemoProject.sln” y listos. Hombre, es feo y un poco chapuza pero bueno…

Pero el problema lo tienes si luego añades otro paquete de NuGet al proyecto cuando lo tienes abierto con la solución SecondSolution.sln. P. ej. yo he instalado el CommandLineParser. Por supuesto este paquete está instalado en nurootpackages y la referencia del proyecto apunta a este directorio. Observa como han quedado las referencias del proyecto:

  1. <Reference Include="CommandLine">
  2.   <HintPath>..\..\..\packages\CommandLineParser.1.9.71\lib\net45\CommandLine.dll</HintPath>
  3. </Reference>
  4. <Reference Include="Ionic.Zip">
  5.   <HintPath>..\packages\DotNetZip.1.9.3\lib\net20\Ionic.Zip.dll</HintPath>
  6. </Reference>

Por lo tanto ahora si borras los directorios packages, debes abrir el proyecto en ambas soluciones y compilarlas en ambas (para forzar la restauración de paquetes). La primera solución que compiles te dará un error (le faltará el paquete que se añadió a través de la otra solución), da igual el orden en que lo hagas. La segunda que compiles si que compilará bien.

Al final terminarás con ambos paquetes instalados en ambos directorios packages pero solo se usará uno de cada (el referenciado por el proyecto).

Igual no te parece muy grave pero si tienes una build que compila una de esas soluciones dala por perdida: cada vez que se ejecuta la build se parte d un entorno nuevo, por lo que la build no compilará, está rota.

La mejor solución para ello es simplemente tenerlo presente: evita que un mismo proyecto esté en varias soluciones localizadas en directorios distintos (si las soluciones, los ficheros .sln, están todos en la misma carpeta no hay problema porque el directorio packages de todas ellas es el mismo).

Espero que el post os haya resultado interesante!

Saludos!

ASP.NET Historia de una optimización. ¡Cuidado con la Sesión!

En un cliente en el que he estado últimamente tenían un problema de rendimiento en su aplicación ASP.NET. El problema era más o menos que “cuando un usuario está buscando algo, entonces la aplicación se queda bloqueada”. Por supuesto el primer paso fue determinar que significaba “bloqueada” ya que es una palabra un tanto ambigua… Uno de los aspectos que conoce todo el mundo que trata con problemas reportados por usuarios finales es que muchas veces (por no decir casi siempre) el problema está descrito entre mal y peor.

Al final que la aplicación se bloqueaba significaba que el usuario no podía navegar a ningún otro sitio. Debo contextualizar que es una aplicación web que consta de una pagina principal con un menú a modo de “escritorio” y cada opción que selecciona el usuario se abre en una ventana nueva de navegador. Así los usuarios terminan teniendo varias ventanas del navegador abiertas y van haciendo cosillas (búsquedas, ediciones, lo que sea que hagan) en cada una de ellas. Pues bien lo que ocurría es que mientras en alguna ventana se estaban buscando datos, cuando se pulsaba en cualquier otra opción, esta se quedaba esperando y no cargaba hasta que finalizaba la búsqueda de la otra ventana.

Lo primero que hice fue verificar que no estuviesen haciendo ellos un bloqueo por código (tenían algunos singletons donde se guardaban ciertos datos) pero no vi nada sospechoso. Había descartado cualquier bloqueo de BBDD porque un análisis previo realizado había confirmado que no habían bloqueos, por lo que en este aspecto estaba tranquilo. El siguiente punto fue ver en que momento se bloqueaban las otras peticiones. Ahí tuve que invertir un poco de tiempo ya que el proyecto consta de varias soluciones de VS y es un proyecto en webforms bastante complejo. Al final pude configurar mi sistema para depurar parte de la web y observé que ni llegaba al Page_Load del formulario. Eso era interesante pero como el ciclo de vida de Webforms es inescrutable tenía que asegurarme que no se quedase en algún punto anterior al Page_Load (lease algún evento Init o PreInit perdidos por ahí) de cualquier formulario base que pudiese haber. Después de navegar un poco por el código fuente (es una aplicación con una jerarquía de formularios bastante… interesante) llegué a la conclusión de que no. De que las peticiones ni tan siquiera habían entrado en el pipeline de webforms. Se quedaban atascadas antes.

Finalmente me dio por monitorizar el estado de peticiones del proceso de trabajo desde la consola de IIS y llegué a ver lo siguiente:

Captura

Tenía dos peticiones en marcha y una tercera que estaba bloqueada esperando acceso a la sesión. Y es que en asp.net solo se permite una petición concurrente por usuario que tenga acceso de escritura a la sesión. Si la petición requiere solo de acceso a lectura no hay problema, pero nunca entraran dos peticiones concurrentes que tengan permisos de escritura en la sesión (por usuario). Esto es muy importante porque no se trata de si realmente se lee o se escribe en la sesión. Se trata de si se tienen permisos para escribir o leer en la sesión.

De las dos peticiones en marcha que tenía una era a un servicio web que no usaba sesión y la otra era la del buscador. El problema básico de la aplicación es que todas las peticiones tenían permisos de lectura y escritura en la sesión, por lo que todas las peticiones se encolaban una tras otra y nunca había dos peticiones (a dos formularios .aspx) en paralelo. Eso en la operativa normal de la aplicación no se notaba y pasaba desapercibido, pero cuando había un buscador en marcha se notaba simplemente porque el buscador podía tardar bastante tiempo en responder (del orden de segundos). Por lo tanto si un usuario mientras esperaba que el buscador le devolviese resultados (recuerda que el buscador está en otra ventana) volvía a la ventana principal y intentaba lanzar otra operación, esta operación se quedaba sin poder cargarse hasta que finalizaba el buscador. Además al abrirse en ventana nueva, la sensación del usuario era de una ventana nueva en blanco que no hacía nada.

Un workaround rápido fue declarar que los buscadores tuviesen solo acceso de lectura a la sesión. Como todos los buscadores derivaban de un formulario padre para búsquedas fue fácil y rápido añadir el atributo EnableSessionState=”ReadOnly” en la directiva @Page de dicho formulario. Y problema solucionado…

…o más bien parche aplicado, porque la solución real pasaría por la inversa: declarar que el acceso habitual a la sesión es de “solo lectura” (añadiendo <pages enableSessionState="ReadOnly" /> en el web.config) y declarar acceso de lectura y escritura solo en aquellos formularios que quieran escribir en la sesión (usando EnableSessionState=”true” en la directiva @Page). De hecho sería incluso mejor deshabilitar en el web.config el acceso a sesión (colocando false en el enableSessionState) y colocar explícitamente los valores ReadOnly y true a cada formulario que requiera acceder a la sesión (en modo de solo lectura o con permisos totales). Pero ese es un refactoring mucho más complejo, claro.

Ten presente pues que si tus peticiones declaran que pueden acceder en modo lectura y escritura a la sesión nunca se ejecutarán de forma concurrente (para un mismo usuario). Quizá no te des por aludido porque tu aplicación no abre varias ventanas pero… ¿haces varias llamadas AJAX de forma simultánea? Si es así… ¿qué permisos tienen al respecto de la sesión?.

Si en lugar de webforms usas ASP.NET MVC recuerda que puedes aplicar el atributo [SessionState] para indicar que un controlador requiere un acceso a la sesión distinto del que esté indicado por defecto en el web.config (o que no requiere acceso en absoluto).

¿Mi recomendación? Evita usar la sesión en todo lo que puedas. Recuerda que en ASP.NET los datos de autenticación no se guardan en la sesión (tienen su propia cookie separada). Pero si al final te decides a usarla, desactívala por defecto en el web.config y actívala explícitamente en todas aquellas páginas/controladores que la requieran. Y cuando la actives actívala siempre en modo ReadOnly a no ser que realmente debas escribir en ella, claro 😉

Con esos sencillos pasos conseguirás evitar que se te queden peticiones enganchadas esperando por una sesión que a lo mejor ni necesitan!

Saludos!

Securiza tu WebApi con tokens autogenerados

El escenario que vamos a abordar en este post es el siguiente: tienes una API creada con ASP.NET WebApi y quieres que sea accesible a través de un token. Pero en este caso quieres ser tu quien proporcione el token y no un tercero como facebook, twitter o Azure Mobile Services (como p. ej. en el escenario que cubrimos en este post). Para ello nuestra API expondrá un endpoint en el cual se le pasarán unas credenciales de usuario (login y password) para obtener a cambio un token. A partir de ese momento todo el resto de llamadas de la API se realizarán usando este token y las credenciales del usuario no seran necesarias más.

Para empezar crea un proyecto ASP.NET con el template “Empty” pero asegúrate de marcar la checkbox de “Web API” para que nos la incluya por defecto. Luego agregamos los paquetes para hospedar OWIN, ya que vamos a usar componentes OWIN tanto para la creación de los tokens oAuth como su posterior validación. Así pues debes incluir los paquetes “Microsoft.AspNet.WebApi.Owin” y “Microsoft.Owin.Host.SystemWeb”.

El siguiente paso será crear una clase de inicialización de Owin (la famosa Startup). Para ello puedes hacer click con el botón derecho sobre el proyecto en el solution explorer y seleccionar “Add –> OWIN Startup class” o bien crear una clase normal y corriente llamada Startup. El código inicial es el siguiente:

  1. [assembly: OwinStartup(typeof(OauthProviderTest.Startup))]
  2.  
  3. namespace OauthProviderTest
  4. {
  5.     public class Startup
  6.     {
  7.         public void Configuration(IAppBuilder app)
  8.         {
  9.             var config = new HttpConfiguration();
  10.             WebApiConfig.Register(config);
  11.             ConfigureOAuth(app);
  12.             app.UseWebApi(config);
  13.         }
  14.     }
  15. }

La clase WebApiConfig es la que configura WebApi y la generó VS al crear el proyecto (está en la carpeta App_Start). Nos falta ver el método ConfigureOAuth que veremos ahora mismo. Observa que el método ConfigureOAuth se llama antes del app.UseWebApi, ya que vamos a añadir middleware OWIN en el pipeline http y debemos hacerlo antes de que se ejecute WebApi. Y por cierto, dado que ahora inicializamos nuestra aplicación usando OWIN puedes eliminar el fichero Global.asax, ya que no lo necesitamos para nada.

Veamos ahora el método ConfigureOAuth. En este método debemos añadir el middleware OWIN para la creación de tokens OAuth. Para ello podemos usar el siguiente código:

  1. public void ConfigureOAuth(IAppBuilder app)
  2. {
  3.     var oAuthServerOptions = new OAuthAuthorizationServerOptions()
  4.     {
  5.         AllowInsecureHttp = true,
  6.         TokenEndpointPath = new PathString("/token"),
  7.         AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
  8.         Provider = new CredentialsAuthorizationServerProvider(),
  9.     };
  10.     app.UseOAuthAuthorizationServer(oAuthServerOptions);
  11. }

Con ello habilitamos un endpoint (/token) para generar los tokens oAuth. El proveedor de dichos tokens es la clase CredentialsAuthorizationServerProvider (que veremos a continuación). Esta clase será la que recibirá las credenciales (login y password), las validará y generará un token oAuth.

Por supuesto nos falta ver el código para validar las credenciales y aquí es donde entra la clase CredentialsAuthorizationServerProvider. Esa clase es la que recibe el login y el password del usuario, los valida y crea el token oAuth:

  1. public class CredentialsAuthorizationServerProvider : OAuthAuthorizationServerProvider
  2. {
  3.     public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
  4.     {
  5.         context.Validated();
  6.     }
  7.  
  8.     public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
  9.     {
  10.  
  11.         context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
  12.  
  13.         using (TestContext db = new TestContext())
  14.         {
  15.             var user = db.Users.FirstOrDefault(u => u.Login == context.UserName && u.Password == context.Password);
  16.             if (user == null)
  17.             {
  18.                 context.SetError("invalid_grant", "The user name or password is incorrect.");
  19.                 return;
  20.             }
  21.         }
  22.  
  23.         var identity = new ClaimsIdentity(context.Options.AuthenticationType);
  24.         identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
  25.         identity.AddClaim(new Claim(ClaimTypes.Role, "user"));
  26.         context.Validated(identity);
  27.     }
  28. }

Lo único remarcable es la primera línea del método GrantResourceOwnerCredentials que se encarga de habilitar CORS. WebApi tiene soporte para CORS, pero el endpoint /token no está gestionado por WebApi si no por el middleware OWIN así que debemos asegurarnos de que mandamos las cabeceras para habilitar CORS. El resto del código es un acceso a la BBDD usando un contexto de EF para encontrar el usuario con el login y el password correcto. Por supuesto en un entorno de producción eso no se haría así. Este código no tiene en cuenta que las passwords deben guardarse como un hash en la BBDD. Si quieres acceder a BBDD directamente debes generar el hash de los passwords aunque si una mejor opción es usar Identity (y la clase UserManager) para acceder a los datos de los usuarios. Una vez validado que las credenciales son correctas creamos la ClaimsIdentity y generamos el token correspondiente.

Para probarlo podéis hacer un POST a /token con las siguientes características:

  • Content-type: application/x-www-form-urlencoded
  • En el cuerpo de la petición añadir los campos:
    • grant_type = “password”
    • username = “Login del usuario”
    • password = “Password del usuario”

Es decir como si tuvieses un formulario con esos campos y lo enviaras via POST al servidor. Os pongo una captura de la petición usando postman:

image

Se puede ver que la respuesta es el token, el tiempo de expiración (que se corresponde con el valor de la propiedad AccessTokenExpireTimeSpan) y el tipo de autenticación que es bearer (ese es el valor que deberemos colocar en la cabecera Authorization).

A partir de ese punto tenemos un token válido y nos olvidamos de las credenciales del usuario. Al menos hasta que el token no caduque. Cuando el token caduque, se deberá generar uno nuevo con otro POST al endpoint /token.

El siguiente punto es habilitar WebApi para que tenga en cuenta esos tokens. Hasta ahora nos hemos limitado a generar tokens, pero WebApi no hace nada con ellos. De hecho no habilitamos WebApi si no que añadimos otro módulo OWIN para autenticarnos en base a esos tokens. El proceso ocurre antes y es transparente a WebApi. Para ello debemos añadir las siguientes líneas a la clase Startup al final del método ConfigureOAuth:

  1. var authOptions = new OAuthBearerAuthenticationOptions()
  2. {
  3.     AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active
  4. };
  5. app.UseOAuthBearerAuthentication(authOptions);

Así añadimos el módulo de OWIN que autentica en base a esos tokens. Para hacer la prueba vamos a crear un controlador de WebApi y vamos a indicar que es solo para usuarios autenticados:

  1. [Authorize]
  2. public class SecureController : ApiController
  3. {
  4.     public IHttpActionResult Get()
  5.     {
  6.         return Ok("Welcome " + User.Identity.Name);
  7.     }
  8. }

Y ahora para probarlo hacemos un GET a la URL del controlador (/api/secure) y tenemos que pasar la cabecera Authorization. El valor de dicha cabecera es “Bearer <token>”:

image

Y con esto deberíamos obtener la respuesta del controlador. En caso de que no pasar la cabecera o que el token fuese incorrecto el resultado sería un HTTP 401 (no autorizado).

Unas palabras sobre los tokens

Fíjate que en ningún momento guardamos en BBDD los tokens de los usuarios y esos tokens son válidos durante todo su tiempo de vida incluso en caso de que el servidor se reinicie. Eso es así porque el token realmente es un ticket de autenticación encriptado. El middleware de OWIN cuando recibe un token se limita a desencriptarlo y en caso de que sea válido, extrae los datos (los claims de la ClaimIdentity creada al generar el token) y coloca dicha ClaimIdentity como identidad de la petición. Es por eso que en el controlador podemos usar User.Identity.Name y recibimos el nombre del usuario que entramos.

Por lo tanto cualquier persona que intercepte el token podrá ejecutar llamadas “en nombre de” el usuario mientras este token sea válido. A todos los efectos poseer el token de autenticación equivale a poseer las credenciales del usuario, al menos mientras el token sea válido. Tenlo presente si optas por ese mecanismo de autenticación: si alguien roba el token, la seguridad se ve comprometida mientras el token sea válido. Por supuesto eso no es distinto al caso de usar una cookie, donde el hecho de robar la cookie compromete la seguridad de igual forma. Y los tokens son más manejables que las cookies y dan menos problemas, en especial en llamadas entre orígenes web distintos.

¡Espero que este post os resulte interesante!