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.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Marco.Pms.DataAccess.Data;
@ -48,34 +49,70 @@ namespace MarcoBMS.Services.Controllers
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginDto loginDto)
{
var user = await _context.ApplicationUsers.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username);
if (user != null)
try
{
// 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)
{
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)
{
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);
//var refreshToken = GenerateRefreshToken();
if (user.UserName == null) return NotFound(ApiResponse<object>.ErrorResponse("UserName Not found", "UserName Not found", 404)); ;
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _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.LogWarning("Login failed: Incorrect password for UserId: {UserId}", user.Id);
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
}
}
// 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));
}
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid username or password.", "Invalid username or password.", 401));
// 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 refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
_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));
}
}
[HttpPost("login-mobile")]
@ -150,111 +187,272 @@ namespace MarcoBMS.Services.Controllers
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")]
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));
}
try
{
// Revoke the refresh token
bool isRevoked = await _refreshTokenService.RevokeRefreshTokenAsync(logoutDto.RefreshToken);
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));
}
// Optional: Blacklist the access token (JWT)
// Optional: Blacklist the JWT access token
string jwtToken = HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
if (!string.IsNullOrEmpty(jwtToken))
if (!string.IsNullOrWhiteSpace(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));
}
catch (Exception ex)
{
// _logger.LogError(ex, "Error during logout");
return BadRequest(ApiResponse<object>.ErrorResponse("Internal server error", ex.Message, 500));
_logger.LogError("Unexpected error during logout : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error occurred", ex.Message, 500));
}
}
[HttpPost("refresh-token")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenDto refreshTokenDto)
{
var refreshToken = await _refreshTokenService.GetRefreshToken(refreshTokenDto.RefreshToken);
if (refreshToken == null || refreshToken.ExpiryDate < DateTime.UtcNow)
if (string.IsNullOrWhiteSpace(refreshTokenDto.RefreshToken))
{
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
await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken);
try
{
// 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
var user = await _userManager.FindByIdAsync(refreshToken.UserId ?? string.Empty);
if (user == null)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid request.", 400));
if (refreshToken.ExpiryDate < DateTime.UtcNow)
{
_logger.LogWarning("Refresh token expired");
return Unauthorized(ApiResponse<object>.ErrorResponse("Refresh token expired.", "Token expired.", 401));
}
Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
// Step 2: Mark the token as used
await _refreshTokenService.MarkRefreshTokenAsUsed(refreshToken);
_logger.LogInfo("Refresh token marked as used");
if (user.UserName == null) return NotFound(ApiResponse<object>.ErrorResponse("UserName Not found", "UserName Not found", 404));
// Step 3: Validate and retrieve user
var user = await _userManager.FindByIdAsync(refreshToken.UserId ?? string.Empty);
if (user == null)
{
_logger.LogWarning("User not found for RefreshToken: {Token}", refreshTokenDto.RefreshToken);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "User not found.", 400));
}
var newJwtToken = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId, _jwtSettings);
var newRefreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
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));
}
return Ok(ApiResponse<object>.SuccessResponse(new { token = newJwtToken, refreshToken = newRefreshToken }, "User refresh token generated successfully.", 200));
// 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 newRefreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), _jwtSettings);
_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")]
public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordDto forgotPasswordDto)
{
if (string.IsNullOrWhiteSpace(forgotPasswordDto.Email))
{
_logger.LogWarning("ForgotPassword request received without email.");
return BadRequest(ApiResponse<object>.ErrorResponse("Email is required.", "Email is required.", 400));
}
var user = await _userManager.FindByEmailAsync(forgotPasswordDto.Email);
if (user == null)
return NotFound(ApiResponse<object>.ErrorResponse("User not found.", "User not found.", 404));
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));
}
/* SEND USER REGISTRATION MAIL*/
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
try
{
// Generate token and build reset link
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
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);
return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset link sent.", 200));
_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));
}
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")]
public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordDto model)
{
var user = await _userManager.FindByEmailAsync(model.Email ?? string.Empty);
if (user == null)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request.", "Invalid request.", 400));
_logger.LogInfo("Password reset request received for email: {Email}", model.Email ?? string.Empty);
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(
user,
TokenOptions.DefaultProvider, // This is the token provider
UserManager<ApplicationUser>.ResetPasswordTokenPurpose,
WebUtility.UrlDecode(model.Token)
);
user,
TokenOptions.DefaultProvider, // This is the token provider
UserManager<ApplicationUser>.ResetPasswordTokenPurpose,
WebUtility.UrlDecode(model.Token)
);
string token = "";
if (!isTokenValid)
{
_logger.LogWarning("Decoded token failed, retrying with raw token for email: {Email}", model.Email);
var isDecodedTokenValid = await _userManager.VerifyUserTokenAsync(
user,
TokenOptions.DefaultProvider, // This is the token provider
TokenOptions.DefaultProvider,
UserManager<ApplicationUser>.ResetPasswordTokenPurpose,
model.Token
model.Token
);
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;
}
@ -263,26 +461,31 @@ namespace MarcoBMS.Services.Controllers
token = WebUtility.UrlDecode(model.Token);
}
var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword ?? string.Empty);
var result = await _userManager.ResetPasswordAsync(user, token, model.NewPassword);
if (!result.Succeeded)
{
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
{
Employee emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
await _emailSender.SendResetPasswordSuccessEmail(user.Email ?? string.Empty, emp.FirstName + " " + emp.LastName);
var emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
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)
{
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
}
return Ok(ApiResponse<object>.SuccessResponse(result.Succeeded, "Password reset successfully.", 200));
_logger.LogInfo("Password reset successful for user: {Email}", model.Email);
return Ok(ApiResponse<object>.SuccessResponse(true, "Password reset successfully.", 200));
}
[HttpPost("send-otp")]