Blog dedicado a la programación .NET y la informática en general

Entradas etiquetadas como ‘ApiKey’

Seguridad de Web API – APIKey (Parte I de IV)

En el cliente en el que estoy ahora mismo ha surgido una necesidad de propuesta para la securización asegurar los accesos a las plataformas web API.

Había implementado algo de seguridad en ASP.Net anteriormente, pero ahora con la plataforma .Net Core se amplían las posibilidades.

La intención es poder autorizar el acceso a las APIs de la forma más sencilla posible, sin tener que construir “arcos de iglesia”, y para que nos lleve unas pocas horas. De lo que he encontrado me quedo con cuatro formas de hacerlo:

  • Comprobación de ApiKey.
  • Autenticación con Jason Web Token (JWT).
  • Azure Active Directory mediante los secretos del registro de “Aplicaciones Empresariales”.
  • Autenticación y Autorización con “Identity Server”.

Para cada uno de ellos iré utilizando proyectos en .Net Framework o .Net Core en la medida que yo considere oportuno.

Pero ¿porqué no utilizar la autenticación mediante ASP.NET Identity mediante el tipo “Forms”? pues porque para ello usa el transporte de cookies, las cuales se gestionan de forma automática mediante los exploradores de internet, pero en WebApi hay que gestionarlos de forma manual y se transportan tanto en las solicitudes como las respuestas.

¿Y la autenticación HTTP Básica mediante los filtros de autorización? no queremos que una clave de usuario y su contraseña personales viajen de un sitio a otro de forma permanente, y en los filtros de autorización hay que generar un atributo personalizado para decorar los controladores. Además hablamos de APIs que normalmente serán consumidas por aplicaciones y no por personas.

Descripción de la autentición con API Key

Hoy comenzamos con la que creo que es la más sencilla “API Key”, consiste en guardar localmente en el proyecto una clave única para autorizar el acceso a los distintos servicios.

Pero antes un poco de teoría para comprender su funcionamiento. En este enlace podéis comprobar cual es el flujo de una petición WebApi. Para esta implementación nosotros nos centraremos en dos sitios, la autenticación en “Message Handlers” y la autorización en “Controller”.

Existe otra capa de seguridad en “HttpModule”, pero está fuertemente ligada a IIS, y prefiero no tener que depender del servidor web.

Usamos “Message Handler” (DelegatingHandler) que se ejecuta antes de los filtros de autorización, eso sí sólo ve las peticiones de WebApi, la intención no es rechazar las peticiones no autenticadas, sino autorizar aquellas que sí lo están para que luego los controladores con Authorize deniguen la petición si no está autorizada. En el código creo que queda más claro.

Codificación

En este caso el proyecto que usaré será una aplicación WebApi hecha con .Net Framework que podéis encontrar en mi repositorio de GitHub. Lo primero que vamos a hacer es lo más común en estos casos, decorar nuestro controlador con [Authorize] ( System.Web.Http ), lo que implica que cualquier petición que no se haya autorizado previamente no podrá acceder al controlador.

[Authorize]
public class ValuesController : ApiController
{
    // GET api/values
    public IEnumerable<string&gt; Get()
    {
        return new string[] { "value1", "value2" };
    }

¿Y dónde y cómo se autoriza? en nuestro caso se comprueba la autenticación de la petición en “Message Handlers”, hacia dónde quiere acceder, y concediéndole el permiso si lo cumple. Cuando llega al controlador, si está asignado el permiso, le autoriza o lo rechaza. He creado una clase ApiKeyAuthMessageHandler que hereda de DelegatingHandler perteneciente al contexto de “Message Handlers”.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Dispatcher;

namespace ApiKeyNetFwk.Seguridad
{
    public class ApiKeyAuthMessageHandler : DelegatingHandler
    {
        /// <summary&gt;
        /// Listado de API Keys autorizadas
        /// </summary&gt;
        private readonly Dictionary<string, Guid&gt; ApiKeys = GestorApiKeys.GetKeys();


        /// <summary&gt;
        /// Cabecera http que indica que contiene la Key de la API
        /// </summary&gt;
        private const string API_KEY = "API_KEY";

        protected override Task<HttpResponseMessage&gt; SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
        {
            // Para obtener la API Key
            IEnumerable<string&gt; listaCabeceras;
            var existeCabeceraApiKey = request.Headers.TryGetValues(API_KEY, out listaCabeceras);
            if (existeCabeceraApiKey &amp;&amp; listaCabeceras.Any())
            {
                // El descriptor aquí contendrá información sobre el controlador al que se enrutará la solicitud.
                // Si es nulo (es decir, no se encontró el controlador), se lanzará una excepción
                var config = GlobalConfiguration.Configuration;
                var controllerSelector = new DefaultHttpControllerSelector(config);
                var controller = controllerSelector.SelectController(request);

                if (controller != null &amp;&amp; ApiKeys.ContainsKey(controller.ControllerName))
                {
                    // Recupera el Guid de la API para comprobar su autenticación
                    Guid apiKey = ApiKeys[controller.ControllerName];
                    if (listaCabeceras.First().Equals(apiKey.ToString()))
                    {
                        // Valida el acceso a la API
                        var principal = new GenericPrincipal(new GenericIdentity("Auth_" + controller.ControllerName), null);
                        AutorizarAccesoApi(principal);
                    }
                }
            }

            return base.SendAsync(request, cancellationToken);
        }

        private void AutorizarAccesoApi(IPrincipal principal)
        {
            Thread.CurrentPrincipal = principal;
            if (HttpContext.Current != null)
            {
                HttpContext.Current.User = principal;
            }
        }
    }
}

En el código se puede ver que he creado un diccionario de ApiKeys (Dictionary<string, Guid> ApiKeys) para que el acceso a cada controlador tenga una key distinta, esto puede hacerse de mil maneras. Al principio del código, en SendAsync, se comprueba las cabeceras de la solicitud http para comprobar si está la que corresponde a ApiKey, se extrae la información y se contrasta con el diccionario. Si la autenticación es correcta se pasa a dar permiso a la solicitud en el contexto http.

private void AutorizarAccesoApi(IPrincipal principal)
{
    Thread.CurrentPrincipal = principal;
    if (HttpContext.Current != null)
    {
        HttpContext.Current.User = principal;
    }
}

Una vez creado el gestor ahora hay que integrarlo en la aplicación, por lo que se invoca en el inicio de la aplicación, en este caso en el Global.asax

using ApiKeyNetFwk.Seguridad;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace ApiKeyNetFwk
{
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            // Autenticación por Message Handler
            GlobalConfiguration.Configuration.MessageHandlers.Add(new ApiKeyAuthMessageHandler());
        }
    }
}

Si se entra por el navegador de internet se comprobará que devuelve un error de no autorización. Para probarlo voy a usar la aplicación PostMan, dejo una captura de la prueba.

También voy a poner cómo probarlo mediante la codificación en C#:

HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("API_KEY", "5b7a521a-ccf5-483e-b250-447d5d5cbb81");
var response = await client.GetAsync("http://localhost:44331/api/Values");

if (!response.IsSuccessStatusCode)
{
    throw new ApplicationException("Hubo algún error");
}

var content = await response.Content.ReadAsStringAsync();

Y listo, ya lo tenemos montado.

Conclusiones

Esta es una forma muy rápida de hacer “segura” una WebApi, pero lo entrecomillo porque si no se pasa por un canal Https cualquier sniffer puede realizar una captura de la cabecera y la seguridad serviría de poco.

Referencias
Burbujas en .NET : Como hacer seguros tus servicios WebApi
Sovit Poudel : Securing ASP.NET Web API

Anuncios
A %d blogueros les gusta esto: