phronCare/phronCare.API/Controllers/AuthenticationController.cs

307 lines
15 KiB
C#
Raw Normal View History

2025-01-24 19:17:26 -03:00
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using phronCare.API.Models;
using Services.Models;
using Services.Interfaces;
using phronCare.API.Models.Authentication.Login;
using phronCare.API.Models.Authentication.SingUp;
using Google.Authenticator;
using QRCoder;
using System.ComponentModel.DataAnnotations;
namespace phronCare.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthenticationController(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IEmailService emailService, IConfiguration configuration, SignInManager<IdentityUser> signInManager, TwoFactorAuthenticator twoFactorAuthenticator) : ControllerBase
{
private const int JWT_TOKEN_VALIDITY_HOURS = 48;
#region Declaration Section
private readonly UserManager<IdentityUser> userManager = userManager;
private readonly RoleManager<IdentityRole> roleManager = roleManager;
private readonly SignInManager<IdentityUser> signInManager = signInManager;
private readonly TwoFactorAuthenticator authenticator = twoFactorAuthenticator;
private readonly IEmailService emailService = emailService;
private readonly IConfiguration configuration = configuration;
#endregion
2025-03-22 01:19:48 -03:00
#region Segurity Endpoints
2025-01-24 19:17:26 -03:00
[HttpPost]
[Route("generate-qr-code")]
public async Task<IActionResult> GenerateQRCodeAsync()
{
try
{
var user = await signInManager.GetTwoFactorAuthenticationUserAsync(); // Obtén el nombre de usuario actual
if (user == null)
{
return BadRequest(new Response { Status = "Error", Message = "Usuario no autenticado." });
}
var setupInfo = authenticator.GenerateSetupCode("MiApp", user.Email, user.Id, false, 10);
// Genera el código QR como una imagen
var qrCodeGenerator = new QRCodeGenerator();
var qrCodeData = qrCodeGenerator.CreateQrCode(setupInfo.ManualEntryKey, QRCodeGenerator.ECCLevel.Q);
var qrCode = new PngByteQRCode(qrCodeData);
// Convierte la imagen del código QR en bytes
var qrCodeImage = qrCode.GetGraphic(10); // Ajusta el tamaño según tus necesidades
var qrCodeImageData = Convert.ToBase64String(qrCodeImage);
// Devuelve el userId (clave secreta) y la imagen del código QR en la respuesta
return Ok(new AuthResponse
{
Status = "Success",
Message = "Código QR generado satisfactoriamente.",
ManualSetupKey = setupInfo.ManualEntryKey, // Proporciona esto al usuario para configuración manual en Google Authenticator
QRCodeImage = qrCodeImageData // Devuelve la imagen del código QR
});
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, new AuthResponse { Status = "Error", Message = "Error al generar el código QR." });
}
}
[HttpPost]
[Route("register")]
public async Task<IActionResult> Register([FromBody] RegisterUser registerUser)
{
//Chequeo Correo existente
var userExist = await userManager.FindByEmailAsync(registerUser.EmailAddress);
if (userExist != null)
{
return StatusCode(StatusCodes.Status409Conflict, "El correo ingresado ya esta en uso.");
}
userExist = await userManager.FindByNameAsync(registerUser.UserName);
if (userExist != null)
{
return StatusCode(StatusCodes.Status409Conflict, "El usuario ingresado ya existe.");
}
IdentityUser user = new()
{
Email = registerUser.EmailAddress,
SecurityStamp = Guid.NewGuid().ToString(),
UserName = registerUser.UserName
};
if (await roleManager.RoleExistsAsync(registerUser.Role))
{
var result = await userManager.CreateAsync(user, registerUser.Password);
if (!result.Succeeded)
{
return StatusCode(StatusCodes.Status500InternalServerError, $"Creacion de usuario fallida: {result}");
}
await userManager.AddToRoleAsync(user, registerUser.Role);
//Add Token to Verify the email
var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
var confirmationLink = Url.Action(nameof(ConfirmEmail), "Authentication", new { token, email = user.Email }, Request.Scheme);
var message = new Message([user.Email!], "phronCare - Link de verificacion", confirmationLink!);
emailService.SendEmail(message);
return StatusCode(StatusCodes.Status201Created, $"Usuario creado satisfactoriamente. Debe autenticar la cuenta generada desde la direccion de correo registrada: {user.Email}");
}
else
{
return StatusCode(StatusCodes.Status500InternalServerError, "El rol no existe. Intentalo nuevamente!");
}
}
[HttpGet("confirmemail")]
public async Task<IActionResult> ConfirmEmail(string token, string email)
{
var user = await userManager.FindByEmailAsync(email);
if (user != null)
{
var result = await userManager.ConfirmEmailAsync(user, token);
if (result.Succeeded)
{
return StatusCode(StatusCodes.Status200OK,
new Response { Status = "Success", Message = "Email verificado satisfactoriamente" });
}
}
return StatusCode(StatusCodes.Status409Conflict,
new Response { Status = "Error", Message = "Este usuario no existe" });
}
[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModel loginModel)
{
await signInManager.SignOutAsync();
var user = await userManager.FindByNameAsync(loginModel.Username);
if (user is null)
{
return Unauthorized("El nombre de usuario o contraseña son incorrectos");
};
var canSignIn = await signInManager.CanSignInAsync(user);
if (!canSignIn)
{
string message = string.Empty;
if (userManager.Options.SignIn.RequireConfirmedEmail && !(await userManager.IsEmailConfirmedAsync(user)))
{
message = "El usuario {" + user.UserName + "} no puede iniciar sesion sin tener el email confirmado.";
}
if (userManager.Options.SignIn.RequireConfirmedPhoneNumber && !(await userManager.IsPhoneNumberConfirmedAsync(user)))
{
message = "El usuario {" + user.UserName + "} no puede iniciar sesion sin tener confirmado el numero de telefono.";
}
return StatusCode(StatusCodes.Status401Unauthorized, message);
}
if (user != null && await userManager.CheckPasswordAsync(user, loginModel.Password))
{
if (!user.TwoFactorEnabled)
{
return await GenerateAccess(user);
}
else
{
return StatusCode(StatusCodes.Status202Accepted, new Response { Status = "Success", Message = $"Debe ingresar el codigo desde la app Google Authenticator : {user.UserName}" });
};
}
return Unauthorized("El nombre de usuario o contraseña son incorrectos");
}
[HttpPost]
[Route("login-2FA")]
public async Task<IActionResult> LoginWithOTP(string code, string username)
{
var user = await userManager.FindByNameAsync(username);
if (user is not null)
{
string secretKey = user.Id; // Obtén la clave secreta del usuario desde tu base de datos
bool isBase32 = false; // Define si la clave secreta está en formato Base32
bool validate = authenticator.ValidateTwoFactorPIN(secretKey, code, isBase32);
if (validate)
{
return await GenerateAccess(user);
}
}
return StatusCode(StatusCodes.Status401Unauthorized, new Response { Status = "Error", Message = "Código de verificación incorrecto." });
}
[HttpPost]
[AllowAnonymous]
[Route("forgot-password")]
public async Task<IActionResult> ForgotPassword([Required]string email)
{
var user = await userManager.FindByEmailAsync(email);
if (user != null)
{
var token = await userManager.GeneratePasswordResetTokenAsync(user);
var forgotPasswordLink = Url.Action(nameof(ResetPassword), "Authentication", new { token, email = user.Email }, Request.Scheme);
var message = new Message([user.Email!], "phronCare - Enlace de recuperacion de contraseña.", forgotPasswordLink!);
emailService.SendEmail(message);
return StatusCode(StatusCodes.Status200OK, $"La solicitud de cambio de contraseña se envio a: {user.Email} con éxito. Por favor verifique el enlace enviado a la casilla de correo.");
}
return StatusCode(StatusCodes.Status400BadRequest, "No se pudo enviar el enlace al correo, por favor intente nuevamente.");
}
[HttpGet("reset-password")]
public async Task<IActionResult> ResetPassword(string token, string email)
{
//var model =new ResetPassword {Token=token, Email = email};
var htmlContent = GetResetPasswordHtmlContent(email,token);
var bytes = Encoding.UTF8.GetBytes(htmlContent);
var stream = new MemoryStream(bytes);
return File(stream, "text/html");
}
private static string GetResetPasswordHtmlContent(string username, string token)
{
string htmlContent = @"<!DOCTYPE html><html><head><meta name=viewport content=""width=device-width""><meta http-equiv=Content-Type content=""text/html; charset=UTF-8""><title>phronCare - Notificación</title><style>body{background:white;font-family:'Helvetica',sans-serif;color:#fff}.container{background-image:linear-gradient(45deg,rgb(5,39,103) 0%,#3a0647 70%);border-radius:50px;color:#fff;padding:20px;margin:30px auto;width:500px;text-align:center;}h1{font-size:20px}h2{font-size:16px;color:yellow;overflow-wrap:break-word;}p{font-size:16px}.form-control{width: 90%;padding: .375rem .75rem;height: 110px;max-height: 150px;border: 1px solid #ced4da;border-radius: .25rem;font-size: 1rem;}textarea{resize: none;}</style></head><body><table class=container><tr><td><h1>phronCare XL: Notificación</h1><h2>" + username + "</h2><h2>Token</h2><textarea class='form-control' readonly style='max-height: 150px;'>" + token + "</textarea><h3>Tu solicitud de restablecimiento de contraseña ha sido confirmada</h3><p>Para restablecer tu contraseña en phronCare, ingrese las credenciales proporcionadas.</p></td></tr></table></body></html>";
return htmlContent;
}
[HttpPost]
[AllowAnonymous]
[Route("reset-password")]
public async Task<IActionResult> ResetPassword([FromBody] ResetPassword resetPassword)
{
var user = await userManager.FindByEmailAsync(resetPassword.Email);
if (user != null)
{
var resetPassResult = await userManager.ResetPasswordAsync(user, resetPassword.Token, resetPassword.Password);
if (!resetPassResult.Succeeded)
{
return StatusCode(StatusCodes.Status500InternalServerError, resetPassResult.Errors.First().Description);
}
return StatusCode(StatusCodes.Status200OK,$"La password ha sido cambiada correctamente.");
}
return StatusCode(StatusCodes.Status500InternalServerError,"No se pudo encontrar el usuario, por favor intente nuevamente.");
}
2025-03-22 01:19:48 -03:00
#endregion
2025-01-24 19:17:26 -03:00
#region GenerateAccess
private async Task<IActionResult> GenerateAccess(IdentityUser? user)
{
var authClaims = new List<Claim>
{
new(ClaimTypes.Name, user.UserName),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var userRoles = await userManager.GetRolesAsync(user);
foreach (var role in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, role));
}
2025-04-29 09:26:01 -03:00
2025-01-24 19:17:26 -03:00
var jwtToken = GetToken(authClaims);
2025-04-29 09:26:01 -03:00
2025-01-24 19:17:26 -03:00
var userSession = new UserSession
{
UserName = user.UserName,
Role = userRoles.First(),
Token = new JwtSecurityTokenHandler().WriteToken(jwtToken),
ExpiresIn = (int)jwtToken.ValidTo.Subtract(DateTime.Now).TotalSeconds,
ExpiryTimeStamp = jwtToken.ValidTo
};
return Ok(userSession);
}
public class UserSession
{
public string UserName { get; set; }
public string Token { get; set; }
public string Role { get; set; }
public int ExpiresIn { get; set; }
public DateTime ExpiryTimeStamp { get; set; }
}
#endregion
#region GenerateToken
private JwtSecurityToken GetToken(List<Claim> authClaims)
{
2025-04-28 19:33:43 -03:00
var secret = configuration["JWT:Secret"];
if (string.IsNullOrWhiteSpace(secret))
throw new InvalidOperationException("El Secret no está configurado.");
// Convertir explícitamente a bytes
var keyBytes = Encoding.UTF8.GetBytes(secret);
var authSigningKey = new SymmetricSecurityKey(keyBytes);
var credentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256);
2025-01-24 19:17:26 -03:00
var token = new JwtSecurityToken(
issuer: configuration["JWT:ValidIssuer"],
audience: configuration["JWT:ValidAudience"],
2025-04-28 19:33:43 -03:00
expires: DateTime.UtcNow.AddHours(JWT_TOKEN_VALIDITY_HOURS),
2025-01-24 19:17:26 -03:00
claims: authClaims,
2025-04-28 19:33:43 -03:00
signingCredentials: credentials
2025-01-24 19:17:26 -03:00
);
2025-04-28 19:33:43 -03:00
2025-01-24 19:17:26 -03:00
return token;
}
#endregion
}
2025-03-22 01:19:48 -03:00
}