Middlewares de autenticación en asp.net core

La autenticación y autorización de peticiones es una de las funcionalidades que más quebraderos da en el desarrollo de aplicaciones en ASP.NET. Además es que ha ido cambiando con el tiempo… En un escenario de internet, en ASP.NET clásico, ya fuese Webforms o MVC usábamos FormsAuthentication. Por otra parte cuando apareció WebApi, incorporó sus propios mecanismos de autenticación y autorización, generalmente basados en la implementación de MessageHandlers.

Con la aparición OWIN y Katana, FormsAuthentication fue reemplazado por el módulo de autenticación por cookies de Katana. A pesar de que MVC no era un módulo OWIN, era posible usar Katana para autenticar las peticiones ya fuesen para MVC o para WebApi (que sí que era un módulo OWIN). Posteriormente fueron surgiendo nuevos módulos de autenticación implementados como módulos OWIN para otros escenarios, como el uso de tokens. Esos módulos fueron reemplazando el uso de MessageHandlers ya que a diferencia de estos, podían securizar cualquier componente del pipeline OWIN (p. ej. una api realizada con NancyFx) y no solo WebApi.

Finalmente tenemos asp.net core que sigue el modelo de OWIN basado en middlewares de autenticación que securizan todo el pipeline. Da igual lo que tengas “por detrás”, ya sea MVC6 o cualquier otro middleware, la autenticación y autorización es común.

Responsabilidades de un middleware de autenticación

¿Qué debe hacer un middleware de autenticación? La respuesta rápida, por supuesto, es “autenticar la petición”. Pero esto, exactamente… ¿qué significa?

Desde un punto de vista técnico, autenticar una petición es dejar en el contexto un IPrincipal que tenga una identidad con la propiedad IsAuthenticated a true:

image

Para obtener esta identidad (en general un objeto ClaimsIdentity) el middleware debe tener determinados datos en la petición que mande el cliente. Esos datos dependen del middleware: una cookie, un token o lo que sea. Si existe este dato y es correcto el middleware puede crear la identidad y autenticarla.

Pero autenticar va un paso más allá. Imagina que te autenticas via Azure Active Directory (AAD). Es evidente que es necesario un paso adicional para poder autenticar al usuario: mostrar la página de login de AAD. Y no solo eso, si no gestionar la respuesta que AAD nos da (a través de una URL de callback definida en nuestra aplicación). ¿Quien se debe encargar de mostrar la página de login de AAD? Pues de nuevo, es el middleware de autenticación el que lo hace. Y el que gestiona la URL de callback y procesa la respuesta de AAD. Lo mismo, por supuesto, ocurre si en lugar de AAD, usas facebook, google o cualquier otro proveedor oAuth o OpenID Connect.

Por lo tanto hemos inferido al menos dos acciones que el middleware de autenticación debe hacer:

  1. Leer algún elemento de la petición (cookie, token, etc) y establecer una identidad autenticada en el contexto en base a la existencia y validez de dicho elemento.
  2. Iniciar un proceso de autenticación externo si es necesario (mostrar la página de login de AAD, de facebook, …)

Pero todavía queda una pregunta… ¿Debe realizar el middleware esas dos acciones automáticamente o no? Para entender la pregunta imagina que un cliente hace una petición a una acción de un controlador que está protegida mediante [Authorize]. Authorize, básicamente, lo que hace es mirar si existe una identidad autenticada en el contexto (la da igual como se haya autenticado). Si no la encuentra Authorize cortocircuita la petición y envía una respuesta HTTP 401.

Imagina ahora un pipeline compuesto de un middleware de autenticación y MVC6 con un controlador con una acción decorada con [Authorize]. Imaginemos los siguientes escenarios:

  1. La petición no está autenticada (no hay cookie, ni token, ni nada). El middleware de autenticación no hace nada y la petición llega al Authorize. Este comprueba que no hay identidad autenticada en el contexto y devuelve un 401. Este 401 pasa de vuelta por el middleware de autenticación, quien no hace nada. El usuario recibe un 401.
  2. La petición no está autenticada (no hay cookie, ni token, ni nada). El middleware de autenticación no hace nada y la petición llega al Authorize. Este comprueba que no hay identidad autenticada en el contexto y devuelve un 401. Este 401 pasa de vuelta por el middleware de autenticación, quien inicia un proceso de autenticación. Este proceso puede ser redirigir el usuario a una página de login interno, o bien a la página de facebook o de AAD o cualquier otra cosa.
  3. La petición está autenticada (hay cookie, o token o lo que sea necesario). El middleware de autenticación lee la información de la petición, crea la identidad y la coloca en el contexto. La petición llega al Authorize quien comprueba la existencia de la identidad autenticada y deja proseguir la petición hacia la acción del controlador. La acción se invoca y el resultado se devuelve.
  4. La petición está autenticada (hay cookie, o token o lo que sea necesario). El middleware de autenticación NO lee la información (a pesar de que existe) y por lo tanto no crea la identidad en el contexto. La petición llega a Authorize quien, al no encontrar identidad autenticada, devuelve un 401. El middleware de autenticación recibe el 401 y no hace nunca nada (no tiene sentido que inicie un proceso de autenticación porque la petición está ya autenticada).

Esos cuatro escenarios surgen de la posibilidad de que el middleware haga o no de forma automática las dos acciones que habíamos comentado antes:

  • Leer la información de la request y crear la identidad en el contexto
  • Iniciar un proceso de autenticación al recibir un 401

A la primera de las dos acciones la llamamos “Autenticar la petición” y si el middleware debe hacerlo de forma automática o no, se controla mediante la propiedad AutomaticAuthenticate.

A la segunda de las dos acciones la llamamos “Iniciar el challenge de autenticación” y si el middleware debe hacerlo de forma automática o no, se controla mediante la propiedad AutomaticChallenge.

Esas dos propiedades las establecemos cuando registramos el middleware, en el método Configure de la clase Startup, como p. ej. en el siguiente código:

  1. app.UseCookieAuthentication(opt =>
  2. {
  3.     opt.AutomaticAuthenticate = true;
  4.     opt.AutomaticChallenge = false;
  5. });

En este caso registramos el middleware de cookies. Si la cookie existe la petición se autenticará automáticamente (AutomaticAuthenticate vale true). Si la cookie NO existe, el usuario recibirá un 401 (asumiendo que accede a una acción protegida por [Authorize]). Si AutomaticChallenge fuese true, en lugar de recibir un 401, el usuario sería redirigido a una página de Login.

Autenticar peticiones de forma manual

De los cuatros escenarios mencionados, igual piensas que el último no tiene sentido: Si ponemos AutomaticAuthenticate a false, la petición no se autenticará a pesar de tener los datos. ¿Qué sentido tiene eso? Pues eso nos permite autenticar la petición cuando lo deseemos. Por supuesto, el escenario no tiene sentido si lo combinamos con Authorize (porque Authorize siempre espera una petición ya autenticada), pero si que lo tiene en otros escenarios. Veamos un ejemplo.

Para ello tenemos registrado el middleware de autenticación por cookies de la siguiente manera:

  1. app.UseCookieAuthentication(opt =>
  2. {
  3.     opt.AutomaticAuthenticate = false;
  4.     opt.AutomaticChallenge = false;
  5.     opt.AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme;
  6. });

En esta configuración el middleware ni autenticará peticiones ni iniciará ningún challenge. Supongamos ahora que tenemos en un controlador (HomeController) una acción para crear la cookie de autenticación:

  1. public async Task<IActionResult> Signin()
  2. {
  3.     var principal = new ClaimsPrincipal(
  4.         new ClaimsIdentity(
  5.             new Claim[] { new Claim(ClaimTypes.NameIdentifier, «Eiximenis») },
  6.             CookieAuthenticationDefaults.AuthenticationScheme
  7.          )
  8.     );
  9.     var authManager = HttpContext.Authentication;
  10.     await authManager.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
  11.     return Json(new { Data = «Cookie set» });
  12. }

En este código usamos el AuthorizationManager para “hacer un signin”, es decir para crear la cookie que necesitamos para que la petición esté autenticada. Observa que le indicamos que middleware de autenticación debe “hacer el signin” (recuerda que hacer un signin puede ser crear una cookie u otra cosa). Para ello el parámetro que le pasamos a SignInAsync debe ser el mismo valor que la propiedad AuthenticationScheme de alguno de los middlewares de autenticación registrados.

Ahora imagina otra acción como la siguiente:

  1. [Authorize]
  2. public IActionResult Secure()
  3. {
  4.     return Json(new { Ok = true });
  5. }

Si el usuario navega primero a /Home/Signin (que establece la cookie) vemos que efectivamente la cookie se establece:

image

Y si ahora navegamos a /Home/Secure vemos que recibimos un 401, a pesar de mandar la cookie:

image

Eso es porque la propiedad AutomaticAuthenticate la hemos puesto a false, por lo que el middleware no coloca la identidad en el contexto y por lo tanto Authenticate devuelve el 401.

Ahora bien, podemos autenticar la petición manualmente, es decir obtener la identidad autenticada si la hay:

  1. public async Task<IActionResult> SecureManual()
  2. {
  3.     var authManager = HttpContext.Authentication;
  4.     var principal = await authManager.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
  5.     if (principal != null &&  principal.Identity.IsAuthenticated)
  6.     {
  7.         return Json(new { Ok = true });
  8.     }
  9.     else
  10.     {
  11.         return HttpUnauthorized();
  12.     }
  13. }

En este código usamos, de nuevo, el AuthorizationManager para autenticar la petición a través del método AuthenticateAsync. En este caso “autenticar la petición” significa, leer la cookie y obtener el principal con la identidad autenticada. Observa como la acción no está decorada con [Authorize] y también que a AuthenticateAsync le indicamos qué middleware de autenticación debe autenticar la petición. El valor pasado a AuthenticateAsync (una cadena) debe ser el mismo que el valor de la propiedad AuthenticationScheme de alguno de los middlewares de autenticación registrados.

En resumen: Los middlewares de autenticación gestionan todo el proceso de “autenticar una petición”, leyendo sus datos y iniciando el proceso de challenge  si es necesario. Debemos tener presente que tanto leer los datos de la petición como inciar el challenge pueden hacerlo automáticamente o de forma manual.

Podemos tener varios mecanismos de autenticación (y en muchas aplicaciones es así), p. ej. uno via cookies que proteja la aplicación y otro via bearer token para accesos externos a la api REST. Y otro para acceso via OpenId Connect. Y otro para… en fin, los escenarios son muchísimos.

Espero que este post os haya ayudado a entender un poco más los middlewares de autenticación. Aunqué seguiré hablando de ellos, no puedo menos que recomendaros la charla que Hugo Biarge dio en la DotNet Conference donde desgranó este y otros muchos aspectos de la autenticación y autorización en .NET.

Saludos!

12 comentarios sobre “Middlewares de autenticación en asp.net core”

  1. Hola, estoy trabajando en un WEB API con asp core 1.0 RC que sera consumido por clientes tando mobiles como website, este post se podria adaptar a mi requerimiento para la autenticacion? o nesecito algo adicional, gracias.

    1. Hola Isidro,
      El post es bastante genérico y todo lo dicho aplica. En el caso de trabajar con una API pensada para ser consumida desde dispositivos móviles, la autenticación por cookies (la que uso en los ejemplos del post) no es buena idea. Hay maneras mejores de autenticar una API, como usar un flujo oAuth o Open ID Connect. Hablaré de estas maneras en un futuro post 🙂

      Gracias por el comentario!

        1. Pues para securizar una web, no una webapi.
          Si, se da el caso, de que la webapi forma parte de la web (en el sentido de que va a usarse solo desde la web mediante llamadas ajax) entonces lo ideal es que las páginas (la web) estén protegidas por cookies y la webapi lo esté mediante bearer token o similar.

          Saludos!

          1. Hola Eduard, como siempre un muy buen post, gracias. Pero con respecto a tu respuesta me surge una duda.
            ¿Porque las páginas (Web) protegerlas con cookie y la WebAPI con bearer token?

            Muchas gracias.

          2. Buenas Jorge!
            Las páginas web se protegen con cookies porque básicamente, los navegadores entonces se encargan de todo: envían la cookie automáticamente y solo cuando se navega hacia el orígen (dominio) de dicha página web. Si se protegieran mediante token deberíamos mandar el token a mano y no sería nada cómodo.
            Las APIs se protegen por token por dos motivos. El primero es que usualmente su cliente no es un navegador. Si protegiéramos una API por cookie los clientes no-navegadores (la mayoría) deberían enviar la cookie «a mano». Perdemos la ventaja principal de las cookies (que se envían automáticamente). Pero el segundo motivo es por seguridad. Las APIs protegidas por cookies son vulnerables a un ataque XSRF. Los ataques XSRF se basan en el hecho de que el navegador envía la cookie automáticamente a cualquier petición al orígen web, incluídas peticiones Ajax, realizadas DESDE OTRA página de otro orígen.
            Así imagina la situación siguiente:

            1. Entras en tu banco y te autenticas.
            2. Estando autenticado, abres otra pestaña y navegas a una página «malvada» (p. ej. a través de un correo que hayas recibido).
            3. La página «malvada» usando Ajax hace una petición a la API del banco para que transfiera 1000€ a una cuenta de las Islas Caimán.
            4. Dado que estás autenticado en el banco (en la OTRA pestaña), el navegador enviará la cookie del banco en la petición Ajax (a pesar de que esté hecha desde otro domínio)
            5. La API del banco recibirá la cookie y la validará (¡es una cookie válida!)
            6. Ya te han robado 1000€.

            Así es como funciona XSRF. El uso de tokens lo evita, porque el navegador NO los envía automáticamente y de este modo la petición Ajax que realizara la página malvada estaría sin autenticar.

            Saludos!

  2. Por qué si soy un desarrollador web con experiencia y he trabajdo todo este año con .net core, por qué me cuesta tanto entender tu post, porque hay tantos términos que no me son familiares, por qué es tan difícil realizar una autenticación ?
    No se supone que la autenticación es un True o un False que indica si puedo utilizar el sistema o si no lo puedo utilizar y ya ? 🙁
    Probablemente me falten muchas cosas por aprender

    1. 😉
      Una autenticación es ahora mucho más que un «true» o un «false», porque tenemos escenarios más avanzados. Además recuerda que siempre tenemos «autenticación» y «autorización» que son dos cosas separadas. Además la autenticación es más complicada de lo que parece porque hay muchos tipos de credenciales (login/pwd, una cookie, un token, un certificado,…). Y además puede ser que quien nos autentique sea un servidor y quien nos autorice otro distinto.

      Esa es la razón de toda esa «complejidad» 🙂

  3. Que tal, me encantó el post, es excelente!
    Tengo una consulta o mas que consulta, es pedir una opinión.
    Estoy por realizar una app mobile con Xamarin, la cual se va a conectar a una DB existente que es utilizada por una app win form. La idea es que el back-end sea una web api con ASP.NET Core. Voy a tener un formulario de login con user y password, el cual lo valido contra la DB. Mi duda es….cual es la arquitectura recomendada en cuanto a Autenticación se refiere. Es decir….que middleware coloco en el medio (Entre la App Mobile y la Web Api)? Espero haber sido claro.
    Desde ya muchisimas gracias.
    Saludos!

    1. Lo ideal sería:

      1. Pedir el login/password y mandarlo por canal seguro a la API
      2. La API (NO EL CLIENTE) valida y si es correcto genera un token JWT y lo manda al cliente
      3. El cliente llama a la API con este token JWT. La API valida el token en cada llamada.

  4. Que buen Post

    Tengo una duda, como podría configurar un Identiy sin base de datos, para que me valide los datos que se consumen de una API, es decir, tengo un servicio que consume datos de una API, tengo una URL que me devuelve datos de un cliente, como podría configurar ese identity para que valide ese dato que traigo con la URL.

    Yo lo hago en el contexto de datos, pero mi aplicación no crea otra instancia en el navegador, si ejecuto la aplicación y lleno el contexto de datos con esa URL, cuando la ejecuto en otro navegador o otra IP me aparecen los datos que cargue en el primer navegador.

Deja un comentario

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