Servicios REST con .NET Core
  • Servicios REST con ASP.NET Core y Entity Framework Core
  • 1. Introducción
    • 1.1 Instalación Visual Studio Community
    • 1.2 Instalación de SQL Server en Mac
    • 1.3 Extensión Intellicode
    • 1.4 Aplicación de ejemplo
  • 2. Explicación de Conceptos
    • 2.1 Servicios REST
      • 2.1.1 OData
      • 2.1.2 GraphQL
    • 2.2 Entity Framework para versiones de tu base de datos
    • 2.3 Paquetes Nuget
    • 2.4 Archivo de recursos
    • 2.5 Estructura de los Proyectos .Net Core
    • 2.6 Código Limpio
    • 2.7 Programación asíncrona
    • 2.8 Git
    • 2.9 ¿Qué es Scrum?
      • 2.9.1 Personas
      • 2.9.2 Roles Scrum
      • 2.9.3 Backlog
      • 2.9.4 Reuniones del Scrum
  • 3. Control de código fuente y Scrum con Azure DevOps
    • 3.1 Introducción a Azure DevOps
      • 3.1.1 Crear un nuevo Proyecto en Azure DevOps
      • 3.1.2 Agregando personas al equipo de trabajo
      • 3.1.3 Agregando los sprints y la capacidad de trabajo
      • 3.1.4 Crear el Backlog y asignar User Stories al Sprint
      • 3.1.5 Crear Prototipos
      • 3.1.6 Conectar a Azure DevOps desde Visual Studio
        • 3.1.6.1 Crear tu proyecto con Visual Studio Community y sincronizarlo a Azure DevOps
      • 3.1.7 Consultar tus tareas pendientes
        • 3.1.7.1 Tareas y Dashboards con Azure DevOps
        • 3.1.7.2 Consultar tus tareas desde Visual Studio Community
      • 3.1.8 Trabajando con Ramas (Branches)
        • 3.1.8.1 Crear la rama desarrollo desde Azure DevOps
          • 3.1.8.1 Crear una rama(branch) desde tu tarea en Azure DevOps
        • 3.1.8.2 Como trabajar con ramas (branches) desde Visual Studio Community
        • 3.1.8.3 Crear el Pull Request con Azure DevOps
      • 3.1.9 Retrospectiva del Sprint
      • 3.1.10 Agregando una Wiki
    • 3.2 Integrando tu código fuente a GitHub
      • 3.2.1 Trabajando con ramas en GitHub
      • 3.2.2 Sincronizar los cambios del código con GitHub y Visual Studio
      • 3.3 Trabajando con Branches (Ramas) con Visual Studio
        • 3.3.1 Branches con Visual Studio
        • 3.3.2 Creando un template para tus PR (Pull Request)
        • 3.3.3 Protegiendo tu branch
  • 4. Creando tu primer servicio
    • 4.1 Crear las base de datos y los usuarios en MySQL
    • 4.2 Crear la tabla Categoría y sus validaciones
    • 4.3 Creando el servicio Categorias
    • 4.4 Probando tus servicios con POSTMAN
    • 4.5 Documentar y Probar tus servicios con Swagger
      • 4.5.1 Configurar Swagger
      • 4.5.2 Comentarios XML
      • 4.5.3 Generando la página de documentación
    • 4.6 Agregando índices
    • 4.7 Mejorando tu código
      • 4.7.1 Creando Objetos de Accesos a Datos
      • 4.7.2 Creando tus mensajes de error en diferentes idiomas
      • 4.7.3 Cambiando el formato del Json de los servicios
  • 5. Agregando el servicio para los productos
    • 5.1 Crear la tabla de Productos
    • 5.2 Formas de cargar información de tablas relacionadas
    • 5.3 Crear llaves fóraneas e índices
    • 5.4 Creando el servicio Productos
    • 5.5 Validar Reglas Mejorando tu código
      • 5.5.1 Agregando una excepción a todos nuestros servicios
      • 5.5.2 Agregando clases genéricas para validar y/o consultar información
      • 5.5.3 Alternativa para validar reglas con ef core
  • 6. Cambiar de base de datos a SQL Server
    • 6.1 Cambiar la base de datos a SQL Server
    • 6.2 Cambiar a SQL Server en Azure
  • 7. Crear servicios con OData
    • 7.1.1 Creando el modelo Clientes
    • 7.1.2 Creando el modelo ClienteCategoría
    • 7.1.3 Agregando paquete Nuget para OData
    • 7.1.4 Configurar el EDM Model
    • 7.1.5 Configurar el servicio OData y llaves foráneas
    • 7.1.6 Creando el Controller para clientes
    • 7.1.7 Configurar y probar los servicios con OData
    • 7.1.8 Práctica Crear el servicio para ClientesCategorias
    • 7.1.9 Recomendaciones de seguridad y rendimiento a tomar en cuenta con OData
  • 8. GraphQL
    • 8.1.1 Creando la tabla Caducidad
    • 8.1.2 Creando el query
    • 8.1.3 Configurando y probando graphQL
    • 8.1.4 Creando la Mutation
    • 8.1.5 Probando nuestros servicios con Postman
  • 9. Seguridad
    • 9.1 Json Web Tokens
    • 9.2 Seguridad basada en roles y usuarios
      • 9.2.1 Creando nuestra tabla roles e insertando los roles principales
      • 9.2.2 Consideraciones de seguridad para almacenar tus passwords
      • 9.2.3 Creando las tablas para manejar la seguridad
      • 9.2.4 Agregando usuarios y roles
    • 9.3 Agregando seguridad a nuestros servicios
    • 9.4 Creando nuestro servicio de login y generar el token
    • 9.5 ¿Cómo agregar seguridad basada en roles a los Servicios REST?
      • 9.5.1 Seguridad basada en claims
      • 9.5.2 Creando las tablas para validar permisos por cada tabla
      • 9.5.3 Seguridad basada en directivas
      • 9.5.4 Seguridad con Action Filters
    • 9.6 Guardando el historial de cambios
    • 9.7 Refrescando tu token
    • 9.8 Seguridad Mejorando tu código
      • 9.8.1 ¿Cómo limitar el número de intentos incorrectos en el login?
      • 9.8.2 ¿Cómo obtener la ciudad del usuario por medio de la IP?
      • 9.8.3 Habilitando CORS
  • 10. Pruebas Unitarias
    • 10.1 Agregando el proyecto de pruebas unitarias
    • 10.2 Crear una prueba unitaria
      • 10.2.1 Ejecutando las pruebas unitarias
    • 10.3 Agregando una base de datos en memoria para nuestras pruebas unitarias
    • 10.4 Agregando la referencia de nuestro proyecto CaducaRest
      • 10.4.1 Agregando paquetes nuget necesarios
    • 10.5 Configurando Clases para Objetos Sustitutos
      • 10.5.1 Configurando el Contexto para utilizar la base de datos en Memoria
      • 10.5.2 Configurando el objeto para sustituir mensajes de Error por idioma
    • 10.6 Agregando pruebas para las Categorías
  • 11. Integración continua
    • 11.1 ¿Qué es la integración continua?
    • 11.2 Subir tu código fuente a BitBucket
      • 11.2.1 Integración continua y pruebas automáticas con Bitbucket
    • 11.3 Integración continua y pruebas automáticas en Azure DevOps
  • 12. Pruebas de integración
    • 12.1 ¿Qué es SpecFlow?
    • 12.2 Agregando el proyecto de pruebas de integración
    • 12.3 Configurando Specflow
    • 12.4 Creando pruebas para el login
    • 12.5 Agregando las pruebas de Integración a Azure Devops
    • 12.6 Specflow Mejorando tu código
      • 12.6.1 Cambiando las pruebas a español
      • 12.6.2 Pasando tablas a nuestras pruebas
      • 12.6.3 Agregar los passwords como variables de ambiente
      • 12.6.4 Probando con SQLite
      • 12.6.5 Agregando diferentes parámetros con MSTest
      • 12.6.6 Generando el reporte living doc de specflow
  • 13. Integración continua con Postman
    • 13.1 Recomendaciones para probar tus servicios
    • 13.2 Instrucciones básicas para probar con Postman
    • 13.3 Crear colecciones en Postman
    • 13.4 Agregar pruebas a tus servicios
    • 13.5 Crear environments
    • 13.6 Agregando datos de prueba con archivos .csv
    • 13.7 Exportando tus colecciones y ejecutarlas con Newman.
    • 13.8 Agregando las colecciones de postman al pipeline
  • 14. Pruebas de usuario
    • 14.1 Page Object Model
    • 14.2 ¿Qué es Selenium?
      • 14.2.1 Selenium Instrucciones básicas
      • 14.2.2 Agregando el proyecto de pruebas de usuario
    • 14.3 ¿Qué es Cypress?
      • 14.3.1 Cypress Instrucciones básicas
    • 14.4 ¿Qué es Playwright?
      • 14.4.1 Playwright Instrucciones básicas
  • 15. Despliegue Continuo con Azure DevOps y Azure
    • 15.1 Crear un App Service en Azure
    • 15.2 Generando Artifacts en Azure Pipelines
    • 15.3 Generando el Release en Azure Pipelines al App Service de Azure
    • 15.4 Ejecutando las colecciones de Postman después del release
    • 15.5 Agregando las pruebas de usuario en Azure Pipelines
  • 16. Instalación en Windows Server e IIS
    • 16.1 Instalar IIS en Windows Server
    • 16.2 Instalación del ASP.NET Core Module/Hosting Bundle
    • 16.3 Crea el Sitio Web en IIS
  • 17. Instalación en Linux
    • 17.1 Creando una máquina virtual linux en Azure
    • 17.2 Habilitando el acceso remoto
    • 17.3 Configura linux para .Net Core
    • 17.4 Instalando mysql
    • 17.5 Instalando Nginx y configurando tu servicio
    • 17.6 Instalando un certificado SSL gratuito con CertBot
    • 17.7 Agregando diferentes subdominios
  • 18. Docker
Powered by GitBook
On this page

Was this helpful?

  1. 9. Seguridad
  2. 9.8 Seguridad Mejorando tu código

9.8.1 ¿Cómo limitar el número de intentos incorrectos en el login?

Previous9.8 Seguridad Mejorando tu códigoNext9.8.2 ¿Cómo obtener la ciudad del usuario por medio de la IP?

Last updated 2 years ago

Was this helpful?

Para evitar que un hacker intente adivinar el usuario y password, lo que suelen hacer es generar scrpits para combinar muchas combinaciones de usuario y contraseñas, algunas de las sugerencias que se recomienda es limitar el número de intentos incorrectos, para que por ejemplo después de 5 intentos incorrectos se envíe un email al usuario indicando que debido a tantos intentos incorrectos ahora debe teclear un código.

El código sería de la siguiente manera

  1. Se revisa si existe un código de bloqueo

    1. Si existe se regresa el error de que la cuenta esta bloqueada

    2. No existe código de bloqueo, se revisa que el usuario/password sea correcto

      1. Si es incorrecto se revisa que el número de intentos sea menor a 5

        1. Si es menor a 5 se regresa un error 400 indicando que el usuario/password es incorrecto

        2. Si no se envía el código de bloqueo al correo del usuario y se regresa un error 423 indicado que el usuario ha sido bloqueado. El código será un número aleatorio de 6 cifras.

      2. Si no es incorrecto se guarda en 0 el número de intentos incorrectos y el código

Agregamos una carpeta Templates aquí vamos a guardar código html para enviar los correos de cuenta bloqueada.

Agregamos un archivo IntentosIncorrectos.html y agregamos entre llaves {{}} los datos a remplazar, en esta caso vamos a reemplazar el campo usuario y el campo codigo

IntentosIncorrectos.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Intentos Incorrectos</title>
    <style>
        .codigo {
            font-weight: bold;
            font-size: 1.5rem;
            text-align: center;
        }
    </style>
</head>
<body>
    <p>
        ¡Hola {{usuario}}!. Se detectaron varios intentos de acceso 
        incorrectos a tu cuenta. Para poder entrar a tu cuenta teclea
        el siguiente código de verificación:
    </p>
    <span class="codigo">{{codigo}}</span>
    <p>
        Si no has solicitado este código, puede que alguien esté 
        intentado acceder a tu cuenta. No reenvíes este correo
        electrónico ni des el código a nadie.
        Si deseas mas información por favor comunícate al servicio 
        de soporte.
    </p>
</body>
</html>

Agregamos una nueva clase para enviar correos llamada Correo en nuestra carpeta Core

Correo.cs
public class Correo
{
    /// <summary>
    /// Mensaje del correo
    /// </summary>
    public string Mensaje;

    /// <summary>
    /// Correos a quien se enviara el correo
    /// </summary>
    public string Para;
    
    /// <summary>
    /// Asunto del correo
    /// </summary>
    public string Asunto;

    /// <summary>
    /// Permite enviar un correo
    /// </summary>
    public void Enviar()
    {
        string smtpAddress, usuarioCorreo, passwordCorreo;
        int puerto = 587;            
        smtpAddress = "smtp.gmail.com";
        usuarioCorreo = "corrego@gmail.com";
        passwordCorreo = "tupassword";

        SmtpClient client = new SmtpClient(smtpAddress, puerto)
        {
            Credentials = new NetworkCredential(usuarioCorreo, 
                                                    passwordCorreo),
            EnableSsl = true,
        };
        MailMessage mailMessage = new MailMessage
        {
           From = new MailAddress(usuarioCorreo)
        };
        mailMessage.To.Add(Para);
        mailMessage.IsBodyHtml = true;
        mailMessage.Body = Mensaje;
        mailMessage.Subject = Asunto;
        try
        {
            client.Send(mailMessage);
        }
        catch (Exception ex)
       {
           Console.WriteLine(ex.InnerException);
       }
}

En nuestra clase UsuarioDAO agregamos el método para enviar el correo de cuenta bloqueada

UsuarioDAO.cs
public class UsuarioDAO
{
   private void EnviaCorreoIntentosIncorrectos(string path,  
                              string usuario, string email, int codigo)
   {
       string body = System.IO.File.ReadAllText(
           Path.Combine(path,"Templates", "IntentosIncorrectos.html"));
       body = body.Replace("{{usuario}}", usuario);
       body = body.Replace("{{codigo}}", codigo.ToString());
       Correo mail = new Correo()
       {
           Para = email,
           Mensaje = body,
           Asunto = "Tu cuenta ha sido bloqueada"
       };
       try
       {
           mail.Enviar();
       }
       catch (Exception ex)
       {
                Console.WriteLine(ex.InnerException);
       }
}

Cambios nuestro archivo LoginDTO para agregar el campo código, el cual utilizará el usuario para desbloquear su cuenta

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

    /// <summary>
    /// Password del usuario
    /// </summary>
    [Required(ErrorMessage = "Required")]
    [StringLength(255)]
    public string Password { get; set; }

    /// <summary>
    /// Código para desbloquear el usuario
    /// </summary>
    public int Codigo { get; set; }
}

Por último cambiamos nuestro método Login:

  1. Revisamos que el usuario no tenga previamente generado un código

    1. Si tiene un código

      1. Revisamos que se haya enviado el código en el login y que el password enviado coincida con el password del usuario

      2. Si coincide

        1. Borramos el código y el número de intentos incorrectos lo regresamos a 0.

UsuarioDAO.cs
public class UsuarioDAO
{
    public async Task<TokenDTO> LoginAsync(LoginDTO loginDTO, 
                                           IConfiguration config)
    {
        Seguridad seguridad = new Seguridad();           
        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;
        }
        //Si el usuario tiene un código mayor a 0, el usuario
        // ha sido bloqueado
        if (usuario.Codigo > 0 )
        {
            if (usuario.Password == seguridad
                      .GetHash(usuario.Adicional1 + loginDTO.Password)
                && usuario.Codigo == loginDTO.Codigo)
            {
                //Reiniciamos el número de intentos y el código
                // para iniciar sesión
                usuario.Intentos = 0;
                usuario.Codigo = 0;
                contexto.SaveChanges();
            }
            else
            {
                customError = new CustomError(423,
                           this.localizacion
                           .GetLocalizedHtmlString("PasswordLocked"));
            }               
        }
        if (usuario.Password != seguridad.GetHash
                           (usuario.Adicional1 + loginDTO.Password))
        {
            usuario.Intentos = usuario.Intentos + 1;
            if (usuario.Intentos > 5)
            {
                Random r = new Random();
                int codigo = r.Next(0, 999999);
                usuario.Codigo = codigo;
                customError = new CustomError(423,
                      this.localizacion.
                      GetLocalizedHtmlString("PasswordLocked"));
                EnviaCorreoIntentosIncorrectos(_path,usuario.Clave, 
                                                usuario.Email, codigo);
            }
            else
            {
                customError = new CustomError(400,
                    this.localizacion
                      .GetLocalizedHtmlString("PasswordIncorrecto"));
            }
            contexto.SaveChanges();
            return tokenDTO;
        }
        if (!usuario.Activo)
        {
            customError = new CustomError(403,
                        this.localizacion
                        .GetLocalizedHtmlString("UsuarioInactivo"));
            return tokenDTO;
        }
        tokenDTO = GenerarToken(config, usuario.Id, usuario.Nombre);
        var usuarioAcceso = new UsuarioAcceso();
        usuarioAcceso.UsuarioId = usuario.Id;       
        usuarioAcceso.Fecha = DateTime.Now;
        usuarioAcceso.Token = tokenDTO.Token;
        usuarioAcceso.Activo = true;
        usuarioAcceso.Ciudad = "Default";
        usuarioAcceso.Estado = "Default";
        usuarioAcceso.SistemaOperativo = "Default";
        usuarioAcceso.RefreshToken = tokenDTO.RefreshToken;
        usuarioAcceso.Navegador = "Default";
        contexto.UsuarioAcceso.Add(usuarioAcceso);
        contexto.SaveChanges();
        return tokenDTO;
    }
}

De esta manera si el usuario teclea 6 veces mal el password se envía un correo con el código al usuario, el cual debe enviarlo en el login para desbloquear su usuario.

El código para el login del usuario ha quedado muy largo y es difícil de probar ya que se realizan demasiadas cosas, vamos a separarlo en funciones que solo realicen una única cosa.

Primero en nuestra clase UsuarioDAO vamos a crear una función que nos regrese los datos de un usuario si le pasamos como parámetro la clave del usuario.

UsuarioDAO.cs
public class UsuarioDAO
{
    public async Task<Usuario> ObtenerPorClave(string clave)
    {
        var usuario = await contexto.Usuario
              .FirstOrDefaultAsync(usu => usu.Clave == clave);
        if (usuario==null)
        {
            customError = new CustomError(400,
                            String.Format(this.localizacion
                            .GetLocalizedHtmlString("GeneralNoExiste"),
                                        "La clave del usuario"));
        }
        return usuario;
    }
}

Agregamos otra función para saber si el usuario es válido, el usuario es válido si esta activo

UsuarioDAO.cs
public class UsuarioDAO
{
    public bool EsUsuarioActivo(Usuario usuario)
    {        
        if (!usuario.Activo)
        {
            customError = new CustomError(403,
                         this.localizacion
                         .GetLocalizedHtmlString("UsuarioInactivo"));
            return false;
        }
        return true;
   }
}

Agregamos otra función para saber si el usuario esta bloqueado o no. Un usuario esta bloqueado si el código es mayor a cero, de esta forma es un poco mas claro saber la regla del usuario bloqueado sin tener que leer comentarios

UsuarioDAO.cs
public class UsuarioDAO
{
    public bool EsUsuarioBloqueado(Usuario usuario)
    {
        return usuario.Codigo > 0;
    }
}

Agregamos otra función para validar que el password del usuario sea el correcto

UsuarioDAO.cs
public class UsuarioDAO
{
    public bool EsPasswordCorrecto(Usuario usuario, string password)
    {
        Seguridad seguridad = new Seguridad();
        return usuario.Password == seguridad
                               .GetHash(usuario.Adicional1 + password);
    }
}

Agregamos otra función para verificar el password que mande llamar a las funciones que creamos.

Agregamos una constante que nos indicara cuantos intentos tiene el usuario antes de que se le bloquee.

UsuarioDAO.cs
public class UsuarioDAO
{
    public const int MAXIMOS_INTENTOS = 5;
    public bool EsPasswordValido(Usuario usuario, string password, int codigo )
    {
        if (EsUsuarioBloqueado(usuario))
        {
            //Si el password es correcto validamos que haya enviado
            //el código correcto
            if (EsPasswordCorrecto(usuario, password) 
                 && usuario.Codigo == codigo)
            {
                //Reiniciamos el número de intentos y 
                // el código para iniciar sesión
                usuario.Intentos = 0;
                usuario.Codigo = 0;
                contexto.SaveChanges();
                return true;
            }
            else
            {
                customError = new CustomError(423,
                    this.localizacion.GetLocalizedHtmlString("PasswordLocked"));
                return false;
            }
        }
        else
        {
            if (!EsPasswordCorrecto(usuario, password))
            {
                usuario.Intentos = usuario.Intentos + 1;
                if (usuario.Intentos > MAXIMOS_INTENTOS)
                {
                        Random r = new Random();
                        codigo = r.Next(0, 999999);
                        usuario.Codigo = codigo;
                        customError = new CustomError(423,
                                            this.localizacion
                                            .GetLocalizedHtmlString
                                            ("PasswordLocked"));
                        EnviaCorreoIntentosIncorrectos(_path, 
                                 usuario.Clave, usuario.Email, codigo);
                }
                else
                {
                    customError = new CustomError(400,
                    this.localizacion.GetLocalizedHtmlString
                         ("PasswordIncorrecto"));
                }
            }
        }
        return true;
    }
}

Cambiamos el código para agregar un nuevo registro de UsuarioAcceso, creamos la clase UsuarioAccesoDAO en nuestra carpeta DAO

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

    public UsuarioAccesoDAO(CaducaContext context,
                            LocService locService)
    {
        this.contexto = context;
        this.localizacion = locService;
    }

    public async System.Threading.Tasks.Task<bool>
             GuardarAccesoAsync(TokenDTO tokenDTO, 
                                int usuarioId)
    {
        var usuarioAcceso = new UsuarioAcceso();
        usuarioAcceso.UsuarioId = usuarioId;
        usuarioAcceso.Fecha = DateTime.Now;
        usuarioAcceso.Token = tokenDTO.Token;
        usuarioAcceso.Activo = true;
        usuarioAcceso.SistemaOperativo = "Default";
        usuarioAcceso.RefreshToken = tokenDTO.RefreshToken;
        usuarioAcceso.Navegador = "Default";
        contexto.UsuarioAcceso.Add(usuarioAcceso);
        contexto.SaveChanges();
        return true;
    }
}

Cambiamos el método LoginAsync para mandar llamar todas nuestras funciones

UsuarioDAO.cs
public class UsuarioDAO
{
    public async Task<TokenDTO> LoginAsync(LoginDTO loginDTO, 
                                IConfiguration config, string ip)
    {
        var usuario = await ObtenerPorClave(loginDTO.Usuario);
        if (usuario == null)
            return tokenDTO;
        if (!EsUsuarioActivo(usuario))
            return tokenDTO;
        if (!EsPasswordValido(usuario, loginDTO.Password, 
                             loginDTO.Codigo))
            return tokenDTO;
        tokenDTO = GenerarToken(config, usuario.Id, usuario.Nombre);
        UsuarioAccesoDAO usuarioAccesoDAO = 
                  new UsuarioAccesoDAO(contexto, localizacion);
        await usuarioAccesoDAO.GuardarAccesoAsync(tokenDTO, usuario.Id, ip);   
        return tokenDTO;
    }
}