JSON Web Token - Seguridad en servicios Web API de .NET Core

La seguridad en los servicios Web API, es un punto fundamental a la hora de implementar este tipo de soluciones en nuestros desarrollos sobre la plataforma .NET.

A partir de la versión 2.1 del Framework ASP.NET Core, Microsoft incluye por defecto un sistema de autenticación de usuarios para servicios Web API RESTful, basado en la tecnología JSON Web Tokens (JWT).

En este artículo veremos cómo crear desde cero un servicio Web API RESTful de .NET Core con seguridad basada en JSON Web Token, para posteriormente explicar con detalle, el proceso de autenticación de los usuarios en el servicio con el gestor de peticiones HTTP Postman.

JWT

Seguramente, si has decidido leer este artículo, es porque ya conoces la tecnología JSON Web Token (JWT en adelante), y lo que te interesa es saber cómo implementar en tus servicios Web API RESTful este sistema de autenticación de usuarios.

Es por esto que pasaremos directamente a desarrollar desde cero nuestro ejemplo de Web API RESTful con seguridad JWT en ASP.NET Core.

Nota: Existe infinidad de información en Internet acerca de JWT que puedes consultar si quieres conocer en profundidad el funcionamiento de este estándar, como por ejemplo este artículo de Wikipedia.

 

Creando nuestro Web API RESTful con Visual Studio

Para este ejemplo, utilizaremos Visual Studio 2017 con todas las actualizaciones necesarias para usar el SDK de .NET Core 2.2.

En primer lugar, crearemos un nuevo proyecto del tipo Aplicación Web ASP.NET Core, y seleccionaremos la plantilla API con plataforma de destino .NET Core y Framework ASP.NET Core 2.2.

Nota: En el caso de querer compilar la aplicación para Windows (IIS), pueden elegir como plataforma de destino .NET Framework (Fulll framework). Así mismo, también pueden utilizar el Framework ASP.NET Core 2.1 si así lo estimaran conveniente.

WebApi JWT

El Modelo de datos

Antes de crear nuestro Web API RESTful, debemos definir el Modelo de datos sobre el cual trabajaremos posteriormente. 

Para esto, crearemos una nueva carpeta en el proyecto llamada Models, y en su interior definiremos una nueva clase llamada Pais.cs de la siguiente manera:

Modelo Pais

    public class Pais
    {
        public Pais()
        {
            this.Id = Guid.NewGuid();
        }

        [Key]
        public Guid Id { get; set; }

        [Required]
        [StringLength(50, MinimumLength = 3)]
        public string Nombre { get; set; }

        [Required]
        public int Habitantes { get; set; }
    }

El Controlador de API

En este momento, ya podremos crear la clase controladora para nuestro Web API RESTful. 

Pera esto, nos situaremos en la carpeta Controllers del proyecto, y con el botón derecho del ratón accederemos al menú contextual (opción Agregar > Controlador ...) para crear un nuevo Controlador del tipo Controlador de API con acciones que usan Entity Framework.

Controlador API EF

Seleccionando esta plantilla, Visual Studio creará por nosotros (Scaffolding) un Controlador API RESTful con Acciones CRUD que usan Entity Framework.

Lo interesante de esta opción, es que simplemente indicándole un Modelo de datos y el nombre de la clase que actuará como Contexto de datos, Visual Studio instalará por nosotros los paquetes NuGet de Entity Framework Core necesarios para la aplicación, y creará un Contexto de datos y cadena de conexión hacia una Base de Datos SQL Server del tipo (localdb)\\mssqllocaldb que posteriormente crearemos desde Entity Famework (Code First).

scaffolding

Si todo ha ido bien, en este punto ya tendremos creado nuestro Controlador API (PaisController.cs) con Acciones CRUD sobre el Modelo de datos Pais.cs

    [Route("api/[controller]")]
    [ApiController]
    public class PaisController : ControllerBase
    {
        private readonly ApplicationDbContext _context;

        public PaisController(ApplicationDbContext context)
        {
            _context = context;
        }

        // GET: api/Pais
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Pais>>> GetPais()
        {
            return await _context.Pais.ToListAsync();
        }

        // GET: api/Pais/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Pais>> GetPais(Guid id)
        {
            var pais = await _context.Pais.FindAsync(id);

            if (pais == null)
            {
                return NotFound();
            }

            return pais;
        }

        // PUT: api/Pais/5
        [HttpPut("{id}")]
        public async Task<IActionResult> PutPais(Guid id, Pais pais)
        {
            if (id != pais.Id)
            {
                return BadRequest();
            }

            _context.Entry(pais).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!PaisExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/Pais
        [HttpPost]
        public async Task<ActionResult<Pais>> PostPais(Pais pais)
        {
            _context.Pais.Add(pais);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetPais", new { id = pais.Id }, pais);
        }

        // DELETE: api/Pais/5
        [HttpDelete("{id}")]
        public async Task<ActionResult<Pais>> DeletePais(Guid id)
        {
            var pais = await _context.Pais.FindAsync(id);
            if (pais == null)
            {
                return NotFound();
            }

            _context.Pais.Remove(pais);
            await _context.SaveChangesAsync();

            return pais;
        }

        private bool PaisExists(Guid id)
        {
            return _context.Pais.Any(e => e.Id == id);
        }
    }

Así como nuestro Contexto de datos de Entity Framework Core ApplicationDbContext.cs.

    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext (DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<Pais> Pais { get; set; }        
    }

Entity Framework Core y la Base de Datos

Durante el proceso de creación del Controlador API PaisController.cs, Visual Studio ha generado por nosotros una cadena de conexión en el archivo de configuración appsettings.json con la siguiente estructura:

"ConnectionStrings": {
    "ApplicationDbContext": "Server=(localdb)\\mssqllocaldb;                             
                             Database=ApplicationDbContext-2dfbdd2b-470c-4bd4-99fe-9b1184c2e631;
                             Trusted_Connection=True;
                             MultipleActiveResultSets=true"
  }

También ha configurado como servicio el Contexto de datos ApplicationDbContext en la clase Startup.cs. Esto es necesario, para poder "inyectar" el Contexto de datos en el Controlador API mediante Inyección de Dependencias.

...

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddDbContext<ApplicationDbContext>(options =>                    
       options.UseSqlServer(Configuration.GetConnectionString("ApplicationDbContext")));
}

...

Llegados a este punto, solo faltaría crear nuestra Base de Datos y comenzar a implementar la seguridad JWT, pero antes, crearemos una serie de datos de prueba que serán insertados en la tabla de Países a la hora de crear la base de datos.

Esto lo haremos desde el método OnModelCreating del Contexto de datos ApplicationDbContext de la siguiente manera:

    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext (DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<Pais> Pais { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<Pais>().HasData(
                new Pais { Nombre = "España", Habitantes = 46000000 },
                new Pais { Nombre = "Alemania", Habitantes = 83000000 },
                new Pais { Nombre = "Francia", Habitantes = 65000000 },
                new Pais { Nombre = "Italia", Habitantes = 61000000 }
                );
        }
    }

Ahora ya podemos crear nuestra base de datos, y esto lo haremos a través de Migraciones de Entity Framework Core (Code First).

En primer lugar abriremos la consola de administración de paquetes NuGet (Herramientas > Administrador de paquetes NuGet > Consola del Administrador de paquetes), y crearemos una Migración inicial de la siguiente manera:

PM> Add-Migration MigracionInicial

Y por último, aplicaremos los cambios:

PM> Update-Database

Si todo ha ido bien, ya tendremos creada la base de datos y la tabla de países con los datos de prueba.

Para probar el correcto funcionamiento del Web API, podemos realizar una petición GET desde nuestro explorador (/api/pais), obteniendo el siguiente resultado:

api-paises

 

Configurando la seguridad con JWT

Antes de comenzar a configurar JWT, debemos proteger nuestro Web API de accesos no autorizados.

Para ello, "decoraremos" las Acciones del Controlador que requieran seguridad con el atributo [Authorize]. Esto hará que de momento, las peticiones dirigidas a los elementos afectados (Acciones) retornen un HTTP 401 Unauthorized (acceso no autorizado).

...

// GET: api/Pais
[HttpGet]
[Authorize] // SOLO USUARIOS AUTENTICADOS
public async Task<ActionResult<IEnumerable<Pais>>> GetPais()
{
     return await _context.Pais.ToListAsync();
}

...

Los parámetros de configuración JWT

En el archivo de configuración appsettings.json, definiremos una serie de parámetros básicos que posteriormente serán utilizados para generar nuestro Token. 

Issuer: Debe indicar quien es un emisor válido para el Token. Normalmente indicaremos el Dominio desde el cual se emite el Token.

Audience: Debe indicar la audiencia o destinatario a los que se dirige el Token. En nuestro caso indicaremos la Url de nuestro Web API.

ClaveSecreta: Obviamente es el parámetro de configuración más importante, ya que será la Clave que utilizaremos tanto para firmar digitalmente el Token al enviarlo, como para comprobar la validez de la firma al recibirlo.

{
  ...

  "JWT": {
    "ClaveSecreta": "OLAh6Yh5KwNFvOqgltw7",
    "Issuer": "www.rafaelacosta.net",
    "Audience": "www.rafaelacosta.net/api/miwebapi"
  }
}

Creando el servicio de autenticación JWT

En el método ConfigureServices() del archivo Startup.cs de la aplicación, añadiremos los servicios de autenticación de ASP.NET Core (services.AddAuthentication()) con la configuración específica para JWT.

        public void ConfigureServices(IServiceCollection services)
        {
            // CONFIGURACIÓN DEL SERVICIO DE AUTENTICACIÓN JWT
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options => 
                {
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuer = Configuration["JWT:Issuer"],
                        ValidAudience = Configuration["JWT:Audience"],
                        IssuerSigningKey = new SymmetricSecurityKey(
                            Encoding.UTF8.GetBytes(Configuration["JWT:ClaveSecreta"])
                        )
                    };
                });

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.AddDbContext<ApplicationDbContext>(options =>
                    options.UseSqlServer(Configuration.GetConnectionString("ApplicationDbContext")));
        }

Una vez configurado el servicio de autenticación JWT, debemos añadir el Middleware de autenticación de ASP.NET Core (app.UseAuthentication()) al Pipeline de la aplicación para que vigile las peticiones entrantes.

Esto lo haremos en el método Configure() del archivo Startup.cs de la aplicación.

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
                app.UseDeveloperExceptionPage();
            else
                app.UseHsts();

            app.UseHttpsRedirection();

            // AÑADIMOS EL MIDDLEWARE DE AUTENTICACIÓN
            // DE USUARIOS AL PIPELINE DE ASP.NET CORE
            app.UseAuthentication();

            app.UseMvc();
        }

 

Generando el Token de autenticación con JWT

Llegados a este punto, ya podemos comenzar a implementar el sistema de generación de Tokens JWT, pero antes, crearemos una clase de Modelo llamada UsuarioInfo.cs, la cual representará la información que identificará de manera única a los usuarios autenticados. Esta clase la utilizaremos posteriormente para construir el Payload de nuestros Tokens.

    public class UsuarioInfo
    {
        public Guid Id { get; set; }
        public string Nombre { get; set; }
        public string Apellidos { get; set; }
        public string Email { get; set; }
        public string Rol { get; set; }
    }

También necesitamos crear una clase de Modelo UsuarioLogin.cs, que represente las credenciales de acceso de un usuario determinado. En nuestro caso definiremos las propiedades Usuario y Password.

    public class UsuarioLogin
    {
        public string Usuario { get; set; }
        public string Password { get; set; }
    }

Por último crearemos un nuevo Controlador de API (LoginController.cs), cuyo cometido será identificar mediante usuario y contraseña (UsuarioLogin.cs) a los potenciales clientes de nuestro Web API, además de generar un Token válido que posteriormente será enviado de vuelta a quien lo solicitó.

    [Route("api/[controller]")]
    [ApiController]
    public class LoginController : ControllerBase
    {
        private readonly IConfiguration configuration;

        // TRAEMOS EL OBJETO DE CONFIGURACIÓN (appsettings.json)
        // MEDIANTE INYECCIÓN DE DEPENDENCIAS.
        public LoginController(IConfiguration configuration)
        {
            this.configuration = configuration;
        }

        // POST: api/Login
        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> Login(UsuarioLogin usuarioLogin)
        {
            var _userInfo = await AutenticarUsuarioAsync(usuarioLogin.Usuario, usuarioLogin.Password);
            if (_userInfo != null)
            {
                return Ok(new { token = GenerarTokenJWT(_userInfo) });
            }
            else
            {
                return Unauthorized();
            }
        }

        // COMPROBAMOS SI EL USUARIO EXISTE EN LA BASE DE DATOS 
        private async Task<UsuarioInfo> AutenticarUsuarioAsync(string usuario, string password)
        {
            // AQUÍ LA LÓGICA DE AUTENTICACIÓN //

            // Supondremos que el Usuario existe en la Base de Datos.
            // Retornamos un objeto del tipo UsuarioInfo, con toda
            // la información del usuario necesaria para el Token.
            return new UsuarioInfo()
            {
                // Id del Usuario en el Sistema de Información (BD)
                Id = new Guid("B5D233F0-6EC2-4950-8CD7-F44D16EC878F"),
                Nombre = "Nombre Usuario",
                Apellidos = "Apellidos Usuario",
                Email = "email.usuario@dominio.com",
                Rol = "Administrador"
            };

            // Supondremos que el Usuario NO existe en la Base de Datos.
            // Retornamos NULL.
            //return null;
        }

        // GENERAMOS EL TOKEN CON LA INFORMACIÓN DEL USUARIO
        private string GenerarTokenJWT(UsuarioInfo usuarioInfo)
        {
            // CREAMOS EL HEADER //
            var _symmetricSecurityKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(configuration["JWT:ClaveSecreta"])
                );
            var _signingCredentials = new SigningCredentials(
                    _symmetricSecurityKey, SecurityAlgorithms.HmacSha256
                );
            var _Header = new JwtHeader(_signingCredentials);

            // CREAMOS LOS CLAIMS //
            var _Claims = new[] {
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.NameId, usuarioInfo.Id.ToString()),
                new Claim("nombre", usuarioInfo.Nombre),
                new Claim("apellidos", usuarioInfo.Apellidos),
                new Claim(JwtRegisteredClaimNames.Email, usuarioInfo.Email),
                new Claim(ClaimTypes.Role, usuarioInfo.Rol)
            };

            // CREAMOS EL PAYLOAD //
            var _Payload = new JwtPayload(
                    issuer: configuration["JWT:Issuer"],
                    audience: configuration["JWT:Audience"],
                    claims: _Claims,
                    notBefore: DateTime.UtcNow,
                    // Exipra a la 24 horas.
                    expires: DateTime.UtcNow.AddHours(24)
                );

            // GENERAMOS EL TOKEN //
            var _Token = new JwtSecurityToken(
                    _Header,
                    _Payload
                );

            return new JwtSecurityTokenHandler().WriteToken(_Token);
        }        
    }

Una vez que el usuario autenticado haya recibido el Token de acceso, ya podrá acceder con total seguridad a aquellas Acciones de nuestro Web API que hayan sido protegidas con el atributo [Authorize].

Este Token recibido, nos identificará como usuario autenticado en las sucesivas peticiones que realicemos al servicio Web API, sin necesidad de volvernos a validar en el sistema de información. Simplemente debemos indicar en la cabecera (Header) de las peticiones HTTP al Web API, un encabezado Authorization: del tipo bearer con el valor del Token obtenido.

 

Comprobando el sistema de seguridad JWT con Postman

Una vez implementada la seguridad JWT sobre nuestro servicio Web API de ASP.NET Core, comprobaremos cómo funciona el proceso de autenticación de usuarios, utilizando la herramienta de "testeo" de APIs REST Postman.

Cómo obtener el Token de acceso

Si en este momento del desarrollo realizáramos desde Postman una petición del tipo GET: api/Pais a nuestro servicio Web API, obtendríamos una respuesta HTTP 401 Unauthorized

401

Por lo tanto, y como requisito inicial, debemos solicitar un Token JWT válido accediendo al recurso POST: api/Login.

Esto lo haremos enviando en el cuerpo de la petición (Body) un objeto JSON del tipo UsuarioLogin, con un usuario y contraseña válidos en nuestro sistema de información. Si todo ha funcionado correctamente, recibiremos en el cuerpo de la respuesta nuestro Token de acceso JWT.

login-token-jwt

Realizando una petición GET al Web API

En este momento ya estamos en disposición de realizar cualquier petición a nuestro servicio Web API PaisController, siempre y cuando indiquemos en la cabecera (Head) de dichas peticiones, nuestro Token de acceso JWT obtenido anteriormente.

Esto lo haremos mediante Postman de la siguiente manera: 

En primer lugar crearemos una nueva petición del tipo GET al recurso GET: api/Pais

Seguidamente, accederemos a la pestaña Authorization y seleccionaremos el tipo Bearer Token

A continuación copiaremos y pegaremos el Token de acceso en el campo indicado y pulsaremos el botón Preview Request.

authorization

 

Esto hará que en la cabecera de la petición se cree un encabezado Authorization de tipo Bearer con el valor del Token de acceso.

GET /api/pais HTTP/1.1
Host: localhost:5001
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJlYzk2MzVmNS04ZTEzLTQ4YTMtOGE5NC1lYmE3NDZiMDdlZjkiLCJuYW1laWQiOiJiNWQyMzNmMC02ZWMyLTQ5NTAtOGNkNy1mNDRkMTZlYzg3OGYiLCJub21icmUiOiJOb21icmUgVXN1YXJpbyIsImFwZWxsaWRvcyI6IkFwZWxsaWRvcyBVc3VhcmlvIiwiZW1haWwiOiJlbWFpbC51c3VhcmlvQGRvbWluaW8uY29tIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW5pc3RyYWRvciIsIm5iZiI6MTU1OTUxNjI0NCwiZXhwIjoxNTU5NjAyNjQ0LCJpc3MiOiJ3d3cucmFmYWVsYWNvc3RhLm5ldCIsImF1ZCI6Ind3dy5yYWZhZWxhY29zdGEubmV0L2FwaS9taXdlYmFwaSJ9.gokqwD7dcKwZUFVcvAcNc52WyUrVTTQjavrp0uBbESA

Por ultimo enviaremos la petición al servidor, y veremos como recibimos una respuesta HTTP 200 Ok, con el contenido de los registros de la base de datos en formato JSON. 

peticion-ge

Importante: Los Tokens JWT caducan, esto quiere decir que a la hora de generarlos, podemos indicar un tiempo de validez mediante el parámetro expires: de la clase JwtPayload(). Por esta razón debemos estar atentos a las respuestas HTTP 401 (acceso no autorizado) del Web API, para volver a solicitar un nuevo Token si fuera necesario.

Enviando una petición POST al Web API

En el caso de una petición POST, los pasos iniciales del proceso serían los mismos.

En primer lugar solicitaríamos el Token de acceso, y lo añadiríamos a la cabecera e la petición mediante el encabezado Authorization.

A continuación, crearíamos en el cuerpo (Body) de la petición, el nuevo objeto JSON del tipo Pais.cs que queremos enviar al servidor para ser insertado en la base de datos. 

Por último, enviaremos la petición al servidor, y veremos como recibimos una respuesta HTTP 201 Created con el nuevo objeto creado en la base de datos. 

peticion-post

 

   EtiquetasJSON Web API Seguridad .NET Core

  Compartir


  Nuevo comentario

El campo Comentario es obligatorio.
El campo Nombre es obligatorio.

Enviando ...

  Comentarios

No hay comentarios para este Post.



  Etiquetas

.NET Core .NET Framework .NET MVC .NET Standard AJAX ASP.NET ASP.NET Core ASP.NET MVC Bootstrap Buenas prácticas C# Cookies Entity Framework JavaScript jQuery JSON PDF Pruebas Unitarias Seguridad SEO Sql Server SqLite Swagger Validación Web API Web Forms

  Nuevos


  Populares















Utilizamos cookies propias y de terceros para mejorar nuestros servicios y ofrecerle una mejor experiencia de navegación. Si continúa navegando consideramos que acepta su uso. Más información   Acepto