Cómo aplicar seguridad a tu API mediante Json Web Token (JWT)
Hacer un API es genial, pero es más genial aún hacerlo de forma segura 😉, hoy no se puede tomar el asunto de la seguridad a la ligera

Naturalmente comenzaremos por dar una breve pero potente definición de qué es JWT: Se trata de un estándar definido en los RFC (Request for comments) # 7519, es abierto y enfocado en la seguridad de las transferencias de información o requests/responses que se hacen a un API, de forma que tu información no esté accesible a cualquier persona allá afuera, sino que mediante un token el usuario peticionario pueda identificarse y así poder obtener lo que el backend le permita según los privilegios o claims que este tenga.

Imaginemos esta situación por unos instantes, ya tenemos nuestra API desarrollada, es decir nuestro backend, como este que hicimos por ejemplo, muy bonito todo, pero si lo publicásemos cualquier persona con postman o cualquier otro cliente puede usarlo, hacer las peticiones get, post, etc que quisiera, ya que no existe ninguna autenticación, es aquí donde entra a tallar JWT, el cual mediante algunas configuraciones a nuestro proyecto nos permitirá restringir accesos a aquellos que tengan las credenciales correctas.

Qué lo caracteriza

Entre las características que JWT tiene destacan:

  • Provee una autenticación sin estado
  • Su medio de comunicación para la autenticación es por una cadena de texto también llamada Token
  • Sigue la arquitectura RestFul
  • Al no tener estado, es agnóstica de la tecnología usada
  • Este token va en las cabeceras de las peticiones (headers)
  • Se debe usar en una conexión segura mediante HTTPS

A trabajar!...

Para el ejemplo de esta entrada estaremos aplicando jwt a un web api hecho con net core, el repositorio está disponible en https://github.com/GeaSmart/BibliotecaAPI:

Configurando

Instalar desde Nuget

Microsoft.AspNetCore.Authentication.JwtBearer

Añadir las configuraciones de autenticación en appsettings.Development.json :

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "connectionStrings": {
    "default": "server=.;initial catalog=test1308;integrated security=true"
  },
  "AuthenticationSettings": {
    "Issuer": "Peticionario",
    "Audience": "Public",
    "SigningKey": "G3VF4C6KFV43JH6GKCDFGJH45V36JHGV3H4C6F3GJC63HG45GH6V345GHHJ4623FJL3HCVMO1P23PZ07W8" //esta cadena tu la eliges, sólo asegúrate que sea bien larga
  }
}

Ahora configuraremos la clase startup.cs

En el método ConfigureServices añadir al final:

		public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            //...

			//***** Configuracion de servicios para JWT *****
            //autorizacion
            services.AddAuthorization(options =>
                options.DefaultPolicy = 
                new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build()
            );
            //estos valores los obtenemos de nuestro appsettings
			var issuer = Configuration["AuthenticationSettings:Issuer"];
            var audience = Configuration["AuthenticationSettings:Audience"];
            var signinKey = Configuration["AuthenticationSettings:SigningKey"];
            //autenticacion
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
                {
                    options.Audience = audience;
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        ValidIssuer = issuer,
                        ValidateIssuerSigningKey = true,
                        ValidateLifetime = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(signinKey))
                    };
                }                
            );
        }

Configuramos el método Configure añadiendo esta linea, siempre antes de todo lo demás:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            //Configurando la aplicación para JWT
            app.UseAuthentication();

            //...
        }

Configuramos la anotación Authorize en los controladores que queramos añadirle seguridad, lo podemos hacer a nivel de todo el controlador lo que añadiría seguridad a todos sus métodos, o a nivel del método, quedaría por ejemplo así:

namespace BibliotecaAPI.Controllers
{
    [ApiController]
    [Route("api/autores")]
    [Authorize] //aquí estaríamos agregando a nivel de todos los métodos del controlador
    public class AutoresController : ControllerBase
    {
	   //...
	}
    //...
}

Por si acaso crack, si ahora mismo corremos nuestra API nos daría error debido a que ya está configurada nuestra seguridad, sin embargo no tenemos ningún permiso para acceder 😄 eso lo veremos a continuación...

Generando los Tokens

Creamos el servicio

Para ello y para manejar un orden crearemos las carpetas Services y dentro de ella Contracts, entonces en contracts crearemos la interfaz IAuthService y en la carpeta Services la clase AuthService

	public interface IAuthService
    {
        public bool ValidateLogin(string username, string password);
        string GenerateToken(DateTime fechaActual, string username, TimeSpan tiempoValidez);
    }

Y nuestra AuthService quedaría así:

    public class AuthService:IAuthService
    {
        public bool ValidateLogin(string username, string password)
        {
            //aqui haríamos la validación, de momento simulamos validación login
            if(username.Equals("usuario") && password.Equals("123456"))
                return true;
            return false;
        }

        public string GenerateToken(DateTime fechaActual, string username, TimeSpan tiempoValidez)
        {
            var fechaExpiracion = fechaActual.Add(tiempoValidez);
            //Configuramos las claims
            var claims = new Claim[]
            {
                new Claim(JwtRegisteredClaimNames.Sub,username),
                new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.Iat,
                    new DateTimeOffset(fechaActual).ToUniversalTime().ToUnixTimeSeconds().ToString(),
                    ClaimValueTypes.Integer64
                ),
                new Claim("roles","Cliente"),
                new Claim("roles","Administrador"),
            };
            
            //Añadimos las credenciales
            var signingCredentials = new SigningCredentials(
                    new SymmetricSecurityKey(Encoding.ASCII.GetBytes("G3VF4C6KFV43JH6GKCDFGJH45V36JHGV3H4C6F3GJC63HG45GH6V345GHHJ4623FJL3HCVMO1P23PZ07W8")),
                    SecurityAlgorithms.HmacSha256Signature
            );//luego se debe configurar para obtener estos valores, así como el issuer y audience desde el appsetings.json

            //Configuracion del jwt token
            var jwt = new JwtSecurityToken(
                issuer: "Peticionario",
                audience: "Public",
                claims:claims,
                notBefore:fechaActual,
                expires:fechaExpiracion,
                signingCredentials:signingCredentials
            );

            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
            return encodedJwt;
        }
    }

Nuestra estructura de carpetas al final quedaría algo así:

Para poder hacer uso de AuthService con inyección de dependencias, lo inyectamos en ConfigureServices

		public void ConfigureServices(IServiceCollection services)
        {            
            services.AddScoped(typeof(IAuthService), typeof(AuthService));//inyectando servicio
						//...
				}

Podemos hacer estas inyecciones desde una clase especializada destinada sólo a esa función, es más profesional pero de momento lo haremos simple 😉

Creamos la entidad UserLogin para poder enviar peticiones de inicio de sesión

    public class UserLogin
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }

Podemos hacer esto usando el patrón DTO, es más profesional, pero será motivo de otro post, nuevamente lo mantendremos simple, pero ya te voy tirando los datitos para que investigues crack, pronto estará este tema aquí también en el blog

Creamos nuestro controlador de login

Añadimos un controlador para este fin, el LoginController quedaría como sigue:

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

        public LoginController(IAuthService authService)
        {
            this.authService = authService;
        }

        [HttpPost]
        public ActionResult Token(UserLogin credenciales)
        {
            if(authService.ValidateLogin(credenciales.Username, credenciales.Password))
            {
                var fechaActual = DateTime.UtcNow;
                var validez = TimeSpan.FromHours(5);
                var fechaExpiracion = fechaActual.Add(validez);

                var token = authService.GenerateToken(fechaActual, credenciales.Username, validez);
                return Ok(new
                {
                    Token = token,
                    ExpireAt = fechaExpiracion
                });
            }
            return StatusCode(401);
        }
    }

Pruebas

Como en el proyecto tenemos configurado Swagger (si no sabes que es esto, es para documentar y tiene una pintaza, en este post te enseño a usarlo) vamos a tener esta hermosa vista:

Nuestro API documentadito con Swagger

Probaremos ejecutar el método get del controlador Autores /api/autores, recordemos que está restringido con Authorize

Yeah! 🤟 Obtenemos un error 401 que quiere decir Unauthorized, no tenemos permisos y eso es justamente lo que esperamos ya que no hemos hecho ningún login, eso quiere decir que ya está funcionando nuestra seguridad configurada! 🥳

Nuestro API no nos devuelve la información y nos dice que no tenemos autorización

Ahora haremos una prueba con postman pero con un usuario permitido, veamos:

Lo primero será hacer una petición post al controlador Login, lo que nos devolverá un token, luego ese token lo enviaremos en la cabecera de la misma petición que hicimos antes y nos debería esta vez, devolver los valores, veamos:

Con las credenciales correctas nos devolvió el token! 😊

Ahora ejecutemos la petición get api/autores

Antes, sin token sólo para confirmar:

Nuestro API nos dice fuera de acá

Ahora probamos con token en la cabecera

Recuerda que en Postman para agregar el token vamos a la pestaña Authorization, seleccionamos Bearer token y pegamos el token

Ahora nos devuelve lo solicitado, bien!

Hemos culminado, felicitaciones genio, haz configurado tu primer JWT para añadir seguridad a tu API, en una siguiente entrada continuaremos con JWT aplicando otro ejemplo que consolide nuestros conocimientos.

En conclusión

Como hemos visto, aplicar seguridad no es precisamente difícil, aunque sí requiere su curva de aprendizaje moderada, vale totalmente la pena, deja de manejar tu seguridad a la ligera cuanto antes o sino esa brecha de seguridad que estás teniendo tarde o temprano te jugará muy en contra estimado developer.

Hasta la vista crack y ya sabes comparte para que el conocimiento se esparza y tu bravedeveloper siga creciendo 😊

Deja una respuesta

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