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
Se revisa si existe un código de bloqueo
Si existe se regresa el error de que la cuenta esta bloqueada
No existe código de bloqueo, se revisa que el usuario/password sea correcto
Si es incorrecto se revisa que el número de intentos sea menor a 5
Si es menor a 5 se regresa un error 400 indicando que el usuario/password es incorrecto
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.
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:
Revisamos que el usuario no tenga previamente generado un código
Si tiene un código
Revisamos que se haya enviado el código en el login y que el password enviado coincida con el password del usuario
Si coincide
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;
}
}