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

Entradas etiquetadas como ‘API REST’

Seguridad de Web API – JWT (Parte II de IV)

En la anterior entrada hablamos de la seguridad de WebApi con ApiKey. Hoy nos adentramos en Jason Web Token.

  • 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”.

Breve descripción de JWT

De JWT tenemos bastante información en internet, la definición oficial en español en la Wikipedia es basante completa pero creo que Santi Macías lo hace mejor en su blog. Resumiéndolo, tal vez demasiado, JWT es un estandar JSON para transmitir la identidad de un usuario de forma confiable, es decir, firmada y verificada digitalmente. Además también se incluye los privilegios que demanda, o en inglés “claims”, como por ejemplo que su rol es de administrador, entre otras cosas. El contenido está dividido en tres partes, la cabecera con el tipo de codificación y token, el “payload” con los datos de la identificación e información útil, y la firma para verificar que el token es válido.

Si lo incluyo en los artículos de seguridad de WebApi es porque hoy en día es un estandar muy utilizado por los servicios de autenticación. Mi intención no es crer un servicio de autenticación completo, tan sólo cómo generar el token y cómo utilizarlo en una WebApi

Base del Proyecto

El proyecto va a utilizar las librerías de OWIN… esto me suena ¿pero qué es? “Open Web INterface” es una especificación que define las relaciones entre los servidores web y los componentes de las aplicaciones, reduciendo las interacciones entre éstas a un conjunto pequeño de tipos. Viéndolo desde una perpectiva práctica, utilizaremos la estructura que OWIN nos ofrece para incluir la gestión de la seguridad de la aplicación.

Esto se realiza mediante la clase “Startup.cs”, en proyectos en “.Net Framework” hay que incluir en la raiz del proyecto un nuevo fichero de tipo “Clase de inicio OWIN” que depende de las librerías de NuGet y las instalará automáticamente si no las tiene:

  • Owin
  • Microsoft.Owin

En “.Net Core” esta clase viene por defecto incluida.

Para instalar las librerías que nos permitan trabajar con JWT se utiliza la siguiente línea de comandos:

PM> Install-Package System.IdentityModel.Tokens.Jwt

Que a su vez instalará los siguientes paquetes:

  • Microsoft.IdentityModel.Logging
  • Microsoft.IdentityModel.Tokens
  • Microsoft.IdentityModel.JsonWebTokens
  • System.IdentityModel.Tokens.Jwt

Por último para poder integrar JWT en OWIN se intalará el siguiente paquete de NuGet:

PM> Install-Package Microsoft.Owin.Security.Jwt

Este paquete también instalará:

  • Microsoft.Owin.Security
  • Microsoft.Owin.Security.OAuth

Ahora ya tenemos la base del proyecto preparado para poder codificar la solución.

Contrucción del código para generar JWT

Lo primero es crear una clase que retenga los datos que acreditan al usuario al que se le otorga la autorización. Para ellos genero la clase “Credencial.cs”

public class Credencial
{
    public int IdUsuario { get; set; }
    public string Nombre { get; set; }
    public string CorreoElectronico { get; set; }
    public IEnumerable<string> Roles { get; set; }
}

Antes de ponernos a configurar la seguridad del aplicativo codificaremos una clase que nos proporcione los “tokens”. En la generación del “token” se incluye la firma digital. Para su composición se va a utlizar una clave o llave de seguridad que se encripta mediante un algoritmo simético, en este caso SHA256, para obtener un identificador único llamado “hash” y que nos proporciona la clave pública que será enviada en el “token”. En este caso se crea una clase que extiende al tipo string para poder trabajar con las textos que contengan las claves.

public static class ExtensionesSeguridad
{
    public static SigningCredentials FormarSigningCredentials(this string jwtSecret)
    {
        var claveSimetrica = jwtSecret.FormarClaveSeguridadSimetrica();
        var credencialesFirmadas = new SigningCredentials(claveSimetrica, SecurityAlgorithms.HmacSha256);

        return credencialesFirmadas;
    }

    public static SymmetricSecurityKey FormarClaveSeguridadSimetrica(this string jwtSecret)
    {
        return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret));
    }
}

Ahora se codifica la clase que proporciona los “tokens”.

public class ProveedorJwt
{
    #region Métodos Públicos
    /// <summary>
    /// Genera un token de tipo JSW Bearer
    /// </summary>
    /// <param name="credencial">Credencial de autenticación</param>
    /// <param name="dominio">Dominio web donde se autentica</param>
    /// <param name="secreto">Clave secreta para generar el hash</param>
    /// <param name="caducidad">Tiempo que dura el token como activo</param>
    /// <returns></returns>
    public string GenerarToken(Credencial credencial, string dominio, string secreto, TimeSpan caducidad)
    {
        var jwtSeguro = new JwtSecurityToken
        (
            issuer: dominio,
            audience: dominio,
            claims: GenerarClaims(credencial),
            // Cuando más corto sea el tiempo de caducidad más aumentará la seguridad
            expires: DateTime.UtcNow.Add(caducidad),
            signingCredentials: secreto.FormarSigningCredentials()
        );
        var token = new JwtSecurityTokenHandler().WriteToken(jwtSeguro);

        return token;
    }
    #endregion

    #region Métodos Auxiliares
    /// <summary>
    /// Genera una colección de claims basados en la credencial pasada como parámetro
    /// </summary>
    /// <param name="credencial">Credencial contiene la información del usuario</param>
    /// <returns>Devuleve una colección de Claims para JWT</returns>
    private IEnumerable<Claim> GenerarClaims(Credencial credencial)
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, credencial.IdUsuario.ToString()),
            new Claim(ClaimTypes.Name, credencial.Nombre),
            // Se pueden añadir más claims aquí
        };

        if (credencial.Roles != null)
        {
            foreach (var role in credencial.Roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role));
            }
        }

        return claims;
    }
    #endregion
}

Configuración de la seguridad en el proyecto

Ahora hay que aplicar JWT en la configuración del proyecto. Como anteriormente he dicho nos basaremos en OWIN o más concretamente en la clase Startup.

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
        {
            TokenValidationParameters = new TokenValidationParameters
            {
                // AuthenticationType = "Bearer",
                // Deben coincidir el secreto y el dominio al generar el token para el cliente
                IssuerSigningKey = ConfigurationManager.AppSettings["SecretoJwt"].FormarClaveSeguridadSimetrica(),
                ValidIssuer = ConfigurationManager.AppSettings["DominioJwt"],
                ValidAudience = ConfigurationManager.AppSettings["DominioJwt"],
                ValidateLifetime = true,
                ClockSkew = TimeSpan.FromMinutes(0)
            }
        });
    }
}

Y ahora hay que “decorar” el controlador que se quiera asegurar.

[Authorize]
public class ValuesController : ApiController

Consumiendo el controlador segurizado

Haremos las pruebas de dos formas, primero con la aplicación PostMan y luego con código C# añadiendo un nuevo controlador.

Para generar el “token” de PostMan utilizaré la consola de C# Interactiva. Si no la tenéis a mano podéis encontrarla aquí:

Lo primero es haber compilado el proyecto y luego agregar la librería mediante el comando #r y la ruta del archivo dll. Dejo el ejemplo que he realizado en mi equipo con el “token” generado.

> #r "C:\GitHub\RublenX\Aprendiendo\PoCSeguridadWebApi\JwtNetFrk\bin\JwtNetFrk.dll"
> JwtNetFrk.Seguridad.Credencial credencial = new JwtNetFrk.Seguridad.Credencial { IdUsuario = 1, Nombre = "JuanRu", CorreoElectronico = "inventado@email.es", Roles = new string[] { "Aplicacion" } };
> JwtNetFrk.Seguridad.ProveedorJwt proveedor = new JwtNetFrk.Seguridad.ProveedorJwt();
> string token = proveedor.GenerarToken(credencial, "localhost:44342", "YWPgvOaLJNcubpz4duqCS7KRwgmCVRRLQ2sVRjPVI7ELOFcGHl2", TimeSpan.FromHours(1));
> token
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiSnVhblJ1IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQXBsaWNhY2lvbiIsImV4cCI6MTU2MDk0NjMxNSwiaXNzIjoibG9jYWxob3N0OjQ0MzQyIiwiYXVkIjoibG9jYWxob3N0OjQ0MzQyIn0.jHPhvm__IfufYwlSYuK70LvuUSj7trNMS4uy6swv-q4"
> 

Se configura PostMan con el “token” formado en Visual Studio de la siguiente manera:

Al lanzarlo podremos comprobar que devuelve los valores esperados.

Ahora probemos con C#. Se crea un nuevo controllador que solicitará por código el token, con una caducidad determinada, lo agregará al cliente y devolverá el resultado.

public class ConsumirJwtController : ApiController
{
    public async Task<IHttpActionResult> Get()
    {
        // Se obtiene el token
        ProveedorJwt jwTokenProv = new ProveedorJwt();
        string token = jwTokenProv.GenerarToken(
            new Credencial
            {
                IdUsuario = 1,
                Nombre = "nombre usuario",
                CorreoElectronico = "anonimo@email.es",
                Roles = new List<string> { "apiAccess" }
            },
            ConfigurationManager.AppSettings["DominioJwt"],
            ConfigurationManager.AppSettings["SecretoJwt"],
            TimeSpan.FromHours(1));

        HttpClient client = new HttpClient();

        // Se incluye el token en el header de la petición
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        HttpResponseMessage respuesta = await client.GetAsync("https://localhost:44342/api/Values");

        if (!respuesta.IsSuccessStatusCode)
        {
            return BadRequest();
        }

        // Al estar redireccionándolo, sólo que con la autenticacón incluida, se utiliza ResponseMessage
        return ResponseMessage(respuesta);
    }
}

Conclusión

No me he dedicado a realiar una autenticación completa con gestión de usuarios, contraseñas y roles, tan sólo he mostrado cómo realizar un JWT, la configuración del proyecto para que pueda utilizarlo y cómo consumirlo.

Lo interesante de JWT es que no permite la modificación del mismo sin ser detectado y tiene una caducidad, aún así siempre hay que utilizar el protocolo seguro HTTPS para evitar ataques “Man-in-the-middle” .

Referencias
Openiddict : Using OpenIddict with JWT Bearer Authentication with a .NET Framework 4.6 Resource server
Santi Macias : Construyendo una Web API REST segura con JSON Web Token en .NET Parte I y Parte II
Jon Hilton : Secure your ASP.NET Core 2.0 API
Bit of Technology : ASP.NET Web API 2 external logins with Facebook and Google in AngularJS app

Entradas relacionadas :

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> 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>
        /// Listado de API Keys autorizadas
        /// </summary>
        private readonly Dictionary<string, Guid> ApiKeys = GestorApiKeys.GetKeys();


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

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
        {
            // Para obtener la API Key
            IEnumerable<string> listaCabeceras;
            var existeCabeceraApiKey = request.Headers.TryGetValues(API_KEY, out listaCabeceras);
            if (existeCabeceraApiKey && 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 && 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

A %d blogueros les gusta esto: