9.4 Creando nuestro servicio de login y generar el token

Vamos a crear nuestro servicio de login en el cual además de validar al usuario vamos a regresar el token. Según que tanta seguridad necesite tu aplicación puedes darle un tiempo de vida más largo o más corto. En mi caso como es una aplicación sencilla que no maneja datos muy confidenciales le dejaré un tiempo de 15 días. Para aplicaciones que manejen información más confidencial similar a un banco puedes darle tiempo de una hora. También si quieres probar que los token caducan y el servicio para refrescar el token puedes darle un tiempo más corto para pruebas de 2 minutos, solamente para que no debas esperar a que expiren los tokens.

Vamos a crear una carpeta llamada DTO (Data Transfer Object) en esta carpeta vamos a guardar archivos que necesitamos para nuestros servicios pero que no pertenecen a ninguna tabla, por ejemplo vamos a crear una clase llamada TokenDTO para regresar los datos del Token cuando un usuario inicia sesión correctamente.

TokenDTO
public class TokenDTO
{
    public string Token { get; set; }
    
    public DateTime TokenExpiration { get; set; }
    
    public string Nombre { get; set; }

    public string RefreshToken { get; set; }
}

Agregamos otro clase llamada LoginDTO el cual va a incluir los datos que necesitamos para realizar el login, para empezar solo pediremos usuario y contraseña.

LoginDTO
public class LoginDTO
{
    [Required(ErrorMessage = "Required")]
    [StringLength(15)]
    public string Usuario { get; set; }

    [Required(ErrorMessage = "Required")]
    [StringLength(255)]
    public string Password { get; set; }

}

Crear clases para Generar y el código para Refrescar el Token

En nuestra carpeta Core agregamos una clase Token el cual tendrá 2 métodos uno para generar el Token y otro para generar el método para refrescar el token.

Como generar el token

La clase que genera el Token de parte de .NET es JwtSecurityToken, en nuestro método GenerarToken recibe como parámetros la lista de claims adicionales que deseas regresar en tu token.

Para generar el token vamos a utilizar una llave simétrica la cual nos servirá para hacer el hash al token, similar a como lo hicimos con el password del usuario con el salt, para firmar utilizaremos el algoritmo SHA-256 con la función SingningCredentials, e indicamos que expira en 15 días, agregamos también quien es el issuer y quien es la audiencia.

Por último con el método JwtSecurityTokenHandler() generamos la cadena que contiene el token.

Token.cs
public class Token
{
    protected readonly IConfiguration Config;

    public Token(IConfiguration config)
    {
        Config = config;
    }

    public string GenerarToken(Claim[] claims)
    {
        var key = new SymmetricSecurityKey
                   (Encoding.UTF8.GetBytes(Config["Tokens:Key"]));
        var creds = new SigningCredentials
                   (key, SecurityAlgorithms.HmacSha256);
        JwtSecurityToken jwtToken = new JwtSecurityToken
                   (Config["Tokens:Issuer"],
                    Config["Tokens:Issuer"],
                    claims,
                    expires: DateTime.Now.AddDays(1).ToLocalTime(),
                    signingCredentials: creds);
        string token = new JwtSecurityTokenHandler()
                    .WriteToken(jwtToken);
            return token;
    }
}

Como refrescar el Token

Debido a que nuestro token expira después de 15 días ya no será válido, algunos de los servicios REST incluyen un método para refrescar el token, por seguridad si tu token nunca expira y un hacker obtiene tu token podría acceder siempre a tus datos a menos que desactives tu usuario, para refrescar el token, al momento de que realizas el login se suele regresar una cadena aleatoria, esta cadena aleatoria se debe enviar como parámetro junto con tu token para validar que es un token válido.

Creamos una función RefrescarToken el cual nos va a generar la cadena aleatoria para refrescar el token, para eso obtenemos un número random, luego lo convertimos a una cadena base 64, nuestro método para refrescar token será por medio GET, el convertir a base64 nos genera caracteres con &,?,/,+,- los cuales indican parámetros en nuestros métodos GET, remplazaremos estos caracteres por números

Token.cs
public class Token
{
    public string RefrescarToken()
    {
        var randomNumber = new byte[32];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(randomNumber);
            return Convert.ToBase64String(randomNumber)
                .Replace("$", "1").Replace("/", "2")
                .Replace("&", "3").Replace("+", "4")
                .Replace("-", "5").Replace("?", "6");
        }
    }
}

Crear el método para el login.

Agregamos una clase RolDAO el cual nos regresará los roles que tiene el usuario que se envía como parámetro

RolDAO.cs
public class RolDAO
{
    private readonly CaducaContext contexto;
    private readonly LocService localizacion;

    public RolDAO(CaducaContext context, LocService localize)
    {
        this.contexto = context;
        this.localizacion = localize;
    }

    public List<string> ObtenerRolesPorUsuarios(int usuarioId)
    {
            return (from usuarioRol in contexto.UsuarioRol
                join rol in contexto.Rol
                    on usuarioRol.RolId equals rol.Id
                where usuarioRol.UsuarioId == usuarioId
            select rol.Nombre).ToList();
    }
}

En nuestro archivo UsuarioDAO agregamos el método para el LoginAsync.

Primero realizamos las siguientes validaciones:

  1. Que el usuario exista

  2. Que el password del usuario coincida con el que está guardado, para esto al password enviado por el usuario le agregamos el salt (adicional1)

  3. Que el usuario se encuentre activo

Si cumple todas las condiciones agregamos:

  • El claim con el id del usuario. Utilizaré el claim de tipo Sid

  • El claim con los roles del usuario.

  • El token generado

  • El código para refrescar el token

UsuarioDAO.cs
public class UsuarioDAO
{
   public async Task<TokenDTO> LoginAsync(LoginDTO loginDTO, 
                                          IConfiguration config)
   {
       TokenDTO tokenDTO = new TokenDTO();
       Seguridad seguridad = new Seguridad();
       Token token = new Token(config);
       var usuario = await contexto.Usuario.FirstOrDefaultAsync
                                 (usu => usu.Clave == loginDTO.Usuario);
       if (usuario == null)
       {
            customError = new CustomError(400,
               String.Format(this.localizacion
                    .GetLocalizedHtmlString("GeneralNoExiste"),
                                               "La clave del usuario"));
            return tokenDTO;
       }
       if (usuario.Password != seguridad
                    .GetHash(usuario.Adicional1 + loginDTO.Password ))
       {
            customError = new CustomError(400, 
                  this.localizacion
                       .GetLocalizedHtmlString("PasswordIncorrecto"));
            return tokenDTO;
       }
       if (!usuario.Activo)
       {
            customError = new CustomError(403,
                   this.localizacion
                       .GetLocalizedHtmlString("UsuarioInactivo"));
            return tokenDTO;
       }           
       var claims = new Claim[]
       {
           new Claim(ClaimTypes.Sid, usuario.Id.ToString()),
       };
       RolDAO rolDAO = new RolDAO(contexto, localizacion);
       var roles = rolDAO.ObtenerRolesPorUsuarios(usuario.Id);
       foreach (var rol in roles)
       {
           claims.Add(new Claim(ClaimTypes.Role, rol));
       }
       DateTime fechaExpiracion = DateTime.Now.AddDays(15).ToLocalTime();
       tokenDTO.Token = token.GenerarToken(claims, fechaExpiracion);
       tokenDTO.TokenExpiration = fechaExpiracion;
       tokenDTO.UsuarioId = usuario.Id;
       tokenDTO.RefreshToken = token.RefrescarToken();
       return tokenDTO;
   }
}

Agregamos los mensajes de error a nuestros archivos de recursos de mensajes.

Agregamos un nuevo controller (UsuariosController) para los servicios de los usuarios puedes ver el siguiente link para recordar cómo crear un nuevo controller.

En el método Post ponemos entre paréntesis la palabra Login para que el login se acceda mediante la siguiente url:

POST http://localhost:5000/api/Usuarios/Login

Agregamos entre [] la palabra AllowAnonymous el cual le indica al servicio que permite el acceso a este servicio no requiere token, cualquiera puede acceder a este servicio.

UsuariosController.cs
public class UsuariosController : ControllerBase
{
    public UsuariosController(CaducaContext context,
                                  LocService localize,
                                  IConfiguration config,
                                  IWebHostEnvironment hostingEnvironment,
                                  IHttpContextAccessor accessor) : base(context, localize)
    {           
        _config = config;
        _accessor = accessor;
        _hostingEnvironment = hostingEnvironment;
        usuarioDAO = new UsuarioDAO(context, localize, _hostingEnvironment.ContentRootPath);
    }
        
    [HttpPost("Login")]
    [AllowAnonymous]
    public async Task<IActionResult> PostAsync(
                                           [FromBody] LoginDTO loginDTO)
    {
        var token = await usuarioDAO.LoginAsync(loginDTO, _config);
        if (string.IsNullOrEmpty(token.Token))         
            return StatusCode(usuarioDAO.customError.StatusCode,
                           usuarioDAO.customError.Message);
        return Ok(token);
    }
}

Para acceder a los servicios con Swagger

  1. Da clic en el botón Authorize que se encuentra arriba a la derecha.

  2. Teclea la palabra Bearer seguido de un espacio y el token generado por el login

  3. Dar clic en el botón Authorize de la ventana popup

Ahora puedes consultar cualquier servicio y te regresará la información

Para probar los servicios con Postman

  1. Da clic en la pestaña Authorization

  2. Del lado izquierdo en Type Selecciona Bearer Token

  3. En Token escribe el token generado por el método Login

Last updated