Implement API to log in through MPIN authentication. #87

Merged
ashutosh.nehete merged 1 commits from Ashutosh_Task#484_Login_MPIN into Issue_Jun_1W_2 2025-06-07 08:01:04 +00:00
2 changed files with 281 additions and 69 deletions

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.Dtos.Authentication
{
public class VerifyMPINDto
{
public Guid EmployeeId { get; set; }
public string? MPIN { get; set; }
public string? MPINToken { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System.Net; using System.Net;
using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
@ -48,34 +49,70 @@ namespace MarcoBMS.Services.Controllers
[HttpPost("login")] [HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginDto loginDto) public async Task<IActionResult> Login([FromBody] LoginDto loginDto)
{ {
var user = await _context.ApplicationUsers.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username); try
if (user != null)
{ {
// Find user by email or phone number
var user = await _context.ApplicationUsers
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username);
if (user == null)
{
_logger.LogWarning("Login failed: User not found for input {Username}", loginDto.Username ?? string.Empty);
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
}
// Check if the user is active
if (!user.IsActive) if (!user.IsActive)
{ {
return BadRequest(ApiResponse<object>.ErrorResponse("User is In Active", "User is In Active", 400)); _logger.LogWarning("Login failed: Inactive user attempted login - UserId: {UserId}", user.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("User is inactive", "User is inactive", 400));
} }
// Ensure the user's email is confirmed
if (!user.EmailConfirmed) if (!user.EmailConfirmed)
{ {
return BadRequest(ApiResponse<object>.ErrorResponse("Your email is not verified, Please verify your email", "Your email is not verified, Please verify your email", 400)); _logger.LogWarning("Login failed: Email not confirmed for UserId: {UserId}", user.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Email not verified", "Your email is not verified, please verify your email", 400));
} }
if (await _userManager.CheckPasswordAsync(user, loginDto.Password ?? string.Empty))
// Validate the password
if (!await _userManager.CheckPasswordAsync(user, loginDto.Password ?? string.Empty))
{ {
Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); _logger.LogWarning("Login failed: Incorrect password for UserId: {UserId}", user.Id);
//var refreshToken = GenerateRefreshToken(); return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
if (user.UserName == null) return NotFound(ApiResponse<object>.ErrorResponse("UserName Not found", "UserName Not found", 404)); ; }
// Retrieve employee details
var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
if (emp == null)
{
_logger.LogWarning("Login failed: No employee record found for UserId: {UserId}", user.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Employee record not found", "Employee not found", 404));
}
// Ensure UserName exists for JWT
if (string.IsNullOrWhiteSpace(user.UserName))
{
_logger.LogWarning("Login failed: Username not found for UserId: {UserId}", user.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Username not found", "Username not found", 404));
}
// Generate tokens
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings); var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings); var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
return Ok(ApiResponse<object>.SuccessResponse(new { token = token, refreshToken = refreshToken }, "User logged in successfully.", 200)); _logger.LogInfo("User login successful - UserId: {UserId}", user.Id);
return Ok(ApiResponse<object>.SuccessResponse(new
{
token,
refreshToken
}, "User logged in successfully.", 200));
} }
catch (Exception ex)
{
_logger.LogError("Unexpected error during login : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
} }
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
} }
[HttpPost("login-mobile")] [HttpPost("login-mobile")]
@ -150,93 +187,248 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(responseData, "User logged in successfully.", 200)); return Ok(ApiResponse<object>.SuccessResponse(responseData, "User logged in successfully.", 200));
} }
[HttpPost("login-mpin")]
public async Task<IActionResult> VerifyMPIN([FromBody] VerifyMPINDto verifyMPIN)
{
try
{
// Validate the MPIN token and extract claims
var claimsPrincipal = _refreshTokenService.ValidateToken(verifyMPIN.MPINToken, _jwtSettings);
if (claimsPrincipal?.Identity == null || !claimsPrincipal.Identity.IsAuthenticated)
{
_logger.LogWarning("Invalid or unauthenticated MPIN token");
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid MPIN token", "Unauthorized", 401));
}
string? tokenType = claimsPrincipal.FindFirst("token_type")?.Value;
string? tokenTenantId = claimsPrincipal.FindFirst("TenantId")?.Value;
string? tokenUserId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Validate essential claims
if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenTenantId) || string.IsNullOrWhiteSpace(tokenUserId))
{
_logger.LogWarning("MPIN token claims are incomplete");
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid token claims", "MPIN token does not match your identity", 401));
}
Guid tenantId = Guid.Parse(tokenTenantId);
// Fetch employee by ID and tenant
var requestEmployee = await _context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.TenantId == tenantId && e.ApplicationUserId == tokenUserId && e.IsActive);
if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId))
{
_logger.LogWarning("Employee not found or invalid for verification - EmployeeId: {EmployeeId}", verifyMPIN.EmployeeId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request", "Provided invalid employee information", 400));
}
// Validate that the token belongs to the same employee making the request
if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin")
{
_logger.LogWarning("Token identity does not match employee info - EmployeeId: {EmployeeId}", requestEmployee.Id);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "MPIN token does not match your identity", 401));
}
// Ensure MPIN input is valid
if (string.IsNullOrWhiteSpace(verifyMPIN.MPIN))
{
_logger.LogWarning("MPIN not provided for EmployeeId: {EmployeeId}", requestEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request", "MPIN not provided", 400));
}
// Retrieve MPIN details
var mpinDetails = await _context.MPINDetails
.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(requestEmployee.ApplicationUserId) && p.TenantId == tenantId);
if (mpinDetails == null)
{
_logger.LogWarning("MPIN not set for EmployeeId: {EmployeeId}", requestEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("MPIN not set", "You have not set an MPIN", 400));
}
// Compare hashed MPIN
var providedMPINHash = ComputeSha256Hash(verifyMPIN.MPIN);
if (providedMPINHash != mpinDetails.MPIN)
{
_logger.LogWarning("MPIN mismatch for EmployeeId: {EmployeeId}", requestEmployee.Id);
return Unauthorized(ApiResponse<object>.ErrorResponse("MPIN mismatch", "MPIN did not match", 401));
}
// Generate new tokens
var jwtToken = _refreshTokenService.GenerateJwtToken(requestEmployee.Email, tenantId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenantId.ToString(), _jwtSettings);
_logger.LogInfo("MPIN verification successful - EmployeeId: {EmployeeId}", requestEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(new
{
token = jwtToken,
refreshToken
}, "User logged in successfully.", 200));
}
catch (Exception ex)
{
_logger.LogError("Unexpected error occurred while verifying MPIN : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
}
}
[HttpPost("logout")] [HttpPost("logout")]
public async Task<IActionResult> Logout([FromBody] LogoutDto logoutDto) public async Task<IActionResult> Logout([FromBody] LogoutDto logoutDto)
{ {
if (string.IsNullOrEmpty(logoutDto.RefreshToken)) if (string.IsNullOrWhiteSpace(logoutDto.RefreshToken))
{ {
_logger.LogWarning("Logout failed: Refresh token is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Refresh token is required", "Refresh token is required", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Refresh token is required", "Refresh token is required", 400));
} }
try try
{ {
// Revoke the refresh token // Revoke the refresh token
bool isRevoked = await _refreshTokenService.RevokeRefreshTokenAsync(logoutDto.RefreshToken); bool isRevoked = await _refreshTokenService.RevokeRefreshTokenAsync(logoutDto.RefreshToken);
if (!isRevoked) if (!isRevoked)
{
_logger.LogWarning("Logout failed: Invalid or expired refresh token");
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid or expired refresh token", "Invalid or expired refresh token", 401)); return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid or expired refresh token", "Invalid or expired refresh token", 401));
}
// Optional: Blacklist the JWT access token
// Optional: Blacklist the access token (JWT)
string jwtToken = HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); string jwtToken = HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
if (!string.IsNullOrEmpty(jwtToken)) if (!string.IsNullOrWhiteSpace(jwtToken))
{ {
await _refreshTokenService.BlacklistJwtTokenAsync(jwtToken); await _refreshTokenService.BlacklistJwtTokenAsync(jwtToken);
_logger.LogInfo("JWT access token blacklisted successfully");
} }
_logger.LogInfo("User logged out successfully");
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Logged out successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(new { }, "Logged out successfully", 200));
} }
catch (Exception ex) catch (Exception ex)
{ {
// _logger.LogError(ex, "Error during logout"); _logger.LogError("Unexpected error during logout : {Error}", ex.Message);
return BadRequest(ApiResponse<object>.ErrorResponse("Internal server error", ex.Message, 500)); return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error occurred", ex.Message, 500));
} }
} }
[HttpPost("refresh-token")] [HttpPost("refresh-token")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenDto refreshTokenDto) public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenDto refreshTokenDto)
{ {
var refreshToken = await _refreshTokenService.GetRefreshToken(refreshTokenDto.RefreshToken); if (string.IsNullOrWhiteSpace(refreshTokenDto.RefreshToken))
if (refreshToken == null || refreshToken.ExpiryDate < DateTime.UtcNow)
{ {
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid or expired refresh token.", "Invalid or expired refresh token.", 401)); _logger.LogWarning("Refresh token is missing from the request body.");
return BadRequest(ApiResponse<object>.ErrorResponse("Refresh token is required.", "Missing refresh token.", 400));
} }
// Mark token as used try
await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken); {
// Step 1: Fetch and validate the refresh token
var refreshToken = await _refreshTokenService.GetRefreshToken(refreshTokenDto.RefreshToken);
if (refreshToken == null)
{
_logger.LogWarning("Refresh token not found in the database");
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid or expired refresh token.", "Token not found.", 401));
}
// Generate new JWT token and refresh token if (refreshToken.ExpiryDate < DateTime.UtcNow)
{
_logger.LogWarning("Refresh token expired");
return Unauthorized(ApiResponse<object>.ErrorResponse("Refresh token expired.", "Token expired.", 401));
}
// Step 2: Mark the token as used
await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken);
_logger.LogInfo("Refresh token marked as used");
// Step 3: Validate and retrieve user
var user = await _userManager.FindByIdAsync(refreshToken.UserId ?? string.Empty); var user = await _userManager.FindByIdAsync(refreshToken.UserId ?? string.Empty);
if (user == null) if (user == null)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid request.", 400)); {
_logger.LogWarning("User not found for RefreshToken: {Token}", refreshTokenDto.RefreshToken);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "User not found.", 400));
}
Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); if (string.IsNullOrWhiteSpace(user.UserName))
{
_logger.LogError("Username missing for user ID: {UserId}", user.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Username not found.", "Username not found.", 404));
}
if (user.UserName == null) return NotFound(ApiResponse<object>.ErrorResponse("UserName Not found", "UserName Not found", 404)); // Step 4: Fetch employee and generate new tokens
var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
var newJwtToken = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings); var newJwtToken = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings);
var newRefreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings); var newRefreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
return Ok(ApiResponse<object>.SuccessResponse(new { token = newJwtToken, refreshToken = newRefreshToken }, "User refresh token generated successfully.", 200)); _logger.LogInfo("New access and refresh token issued for user: {UserId}", user.Id);
return Ok(ApiResponse<object>.SuccessResponse(
new { token = newJwtToken, refreshToken = newRefreshToken },
"User refresh token generated successfully.",
200));
}
catch (Exception ex)
{
_logger.LogError("An unexpected error occurred during token refresh. : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error occurred.", ex.Message, 500));
}
} }
[HttpPost("forgot-password")] [HttpPost("forgot-password")]
public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordDto forgotPasswordDto) public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordDto forgotPasswordDto)
{ {
var user = await _userManager.FindByEmailAsync(forgotPasswordDto.Email); if (string.IsNullOrWhiteSpace(forgotPasswordDto.Email))
if (user == null) {
return NotFound(ApiResponse<object>.ErrorResponse("User not found.", "User not found.", 404)); _logger.LogWarning("ForgotPassword request received without email.");
return BadRequest(ApiResponse<object>.ErrorResponse("Email is required.", "Email is required.", 400));
}
/* SEND USER REGISTRATION MAIL*/ var user = await _userManager.FindByEmailAsync(forgotPasswordDto.Email);
if (user == null || user.Email == null)
{
_logger.LogWarning("ForgotPassword requested for non-existent or null-email user: {Email}", forgotPasswordDto.Email);
// Do not disclose whether the email exists (security best practice)
return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset link sent if the account exists.", 200));
}
try
{
// Generate token and build reset link
var token = await _userManager.GeneratePasswordResetTokenAsync(user); var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
if (user.Email == null) return NotFound(ApiResponse<object>.ErrorResponse("Email Not found", "Email Not found", 404)); // Send reset email
await _emailSender.SendResetPasswordEmail(user.Email, user.UserName ?? "User", resetLink);
await _emailSender.SendResetPasswordEmail(user.Email, "", resetLink); _logger.LogInfo("Password reset link sent to user: {Email}", user.Email);
return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset link sent if the account exists.", 200));
}
return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset link sent.", 200)); catch (Exception ex)
{
_logger.LogError("Error while sending password reset email to: {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Error sending password reset email.", ex.Message, 500));
}
} }
[HttpPost("reset-password")] [HttpPost("reset-password")]
public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordDto model) public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordDto model)
{ {
var user = await _userManager.FindByEmailAsync(model.Email ?? string.Empty); _logger.LogInfo("Password reset request received for email: {Email}", model.Email ?? string.Empty);
if (user == null)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid request.", 400)); if (string.IsNullOrWhiteSpace(model.Email) || string.IsNullOrWhiteSpace(model.Token) || string.IsNullOrWhiteSpace(model.NewPassword))
{
_logger.LogWarning("Reset password failed due to missing input fields for email: {Email}", model.Email ?? string.Empty);
return BadRequest(ApiResponse<object>.ErrorResponse("All fields are required.", "Invalid input.", 400));
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
_logger.LogWarning("Reset password failed - user not found for email: {Email}", model.Email);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid user.", 400));
}
// var isTokenValid = await _userManager.VerifyUserTokenAsync(user,UserManager<ApplicationUser>.ResetPasswordTokenPurpose, model.ResetCode);
var isTokenValid = await _userManager.VerifyUserTokenAsync( var isTokenValid = await _userManager.VerifyUserTokenAsync(
user, user,
TokenOptions.DefaultProvider, // This is the token provider TokenOptions.DefaultProvider, // This is the token provider
@ -247,14 +439,20 @@ namespace MarcoBMS.Services.Controllers
if (!isTokenValid) if (!isTokenValid)
{ {
_logger.LogWarning("Decoded token failed, retrying with raw token for email: {Email}", model.Email);
var isDecodedTokenValid = await _userManager.VerifyUserTokenAsync( var isDecodedTokenValid = await _userManager.VerifyUserTokenAsync(
user, user,
TokenOptions.DefaultProvider, // This is the token provider TokenOptions.DefaultProvider,
UserManager<ApplicationUser>.ResetPasswordTokenPurpose, UserManager<ApplicationUser>.ResetPasswordTokenPurpose,
model.Token model.Token
); );
if (!isDecodedTokenValid) if (!isDecodedTokenValid)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid request.", 400)); {
_logger.LogWarning("Both decoded and raw token failed for email: {Email}", model.Email);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid or expired reset token.", 400));
}
token = model.Token; token = model.Token;
} }
@ -263,26 +461,31 @@ namespace MarcoBMS.Services.Controllers
token = WebUtility.UrlDecode(model.Token); token = WebUtility.UrlDecode(model.Token);
} }
var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword);
var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword ?? string.Empty);
if (!result.Succeeded) if (!result.Succeeded)
{ {
var errors = result.Errors.Select(e => e.Description).ToList(); var errors = result.Errors.Select(e => e.Description).ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to Change password", errors, 400)); _logger.LogWarning("Reset password failed for user: {Email}. Errors: {Errors}", model.Email, string.Join(", ", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to reset password.", errors, 400));
} }
try try
{ {
Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
await _emailSender.SendResetPasswordSuccessEmail(user.Email ?? string.Empty, emp.FirstName + " " + emp.LastName); string fullName = $"{emp.FirstName} {emp.LastName}".Trim();
await _emailSender.SendResetPasswordSuccessEmail(user.Email!, fullName);
_logger.LogInfo("Reset password success email sent to user: {Email}", model.Email);
} }
catch (Exception ex) catch (Exception ex)
{ {
return BadRequest(ApiResponse<object>.ErrorResponse(ex.Message, ex.Message, 400)); _logger.LogError("Error while sending reset password success email to user: {Error}", ex.Message);
// Continue, do not fail because of email issue
} }
_logger.LogInfo("Password reset successful for user: {Email}", model.Email);
return Ok(ApiResponse<object>.SuccessResponse(result.Succeeded, "Password reset successfully.", 200)); return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset successfully.", 200));
} }
[HttpPost("send-otp")] [HttpPost("send-otp")]