diff --git a/src/main/java/com/safetypin/authentication/controller/AuthenticationController.java b/src/main/java/com/safetypin/authentication/controller/AuthenticationController.java index f18a904297e4f6d46ef2d02dfef859cdae77e913..5d5566f5b23331456ee7288d4a16e206b49197d1 100644 --- a/src/main/java/com/safetypin/authentication/controller/AuthenticationController.java +++ b/src/main/java/com/safetypin/authentication/controller/AuthenticationController.java @@ -13,6 +13,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import com.safetypin.authentication.dto.VerifyResetOTPRequest; import com.safetypin.authentication.dto.PasswordResetWithOTPRequest; +import com.safetypin.authentication.dto.ResetTokenResponse; @RestController @RequestMapping("/api/auth") @@ -101,10 +102,11 @@ public class AuthenticationController { @PostMapping("/verify-reset-otp") public ResponseEntity<AuthResponse> verifyResetOTP(@Valid @RequestBody VerifyResetOTPRequest request) { try { - boolean isValid = authenticationService.verifyPasswordResetOTP(request.getEmail(), request.getOtp()); - if (isValid) { + String resetToken = authenticationService.verifyPasswordResetOTP(request.getEmail(), request.getOtp()); + if (resetToken != null) { + ResetTokenResponse tokenResponse = new ResetTokenResponse(resetToken); return ResponseEntity.ok(new AuthResponse(true, - "OTP verified successfully. You can now reset your password.", null)); + "OTP verified successfully. Reset token valid for 3 minutes.", tokenResponse)); } else { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new AuthResponse(false, "Invalid OTP", null)); @@ -115,11 +117,11 @@ public class AuthenticationController { } } - // Endpoint to reset password with OTP + // Endpoint to reset password with reset token @PostMapping("/reset-password") public ResponseEntity<AuthResponse> resetPassword(@Valid @RequestBody PasswordResetWithOTPRequest request) { try { - authenticationService.resetPassword(request.getEmail(), request.getNewPassword()); + authenticationService.resetPassword(request.getEmail(), request.getNewPassword(), request.getResetToken()); return ResponseEntity.ok(new AuthResponse(true, "Password has been reset successfully", null)); } catch (InvalidCredentialsException | IllegalArgumentException e) { diff --git a/src/main/java/com/safetypin/authentication/dto/PasswordResetWithOTPRequest.java b/src/main/java/com/safetypin/authentication/dto/PasswordResetWithOTPRequest.java index 143445e9b6462c7c7415eadf0b7d1c5b0df18af1..cfc81a84c1dccc4593630281e9a07e15300dbd62 100644 --- a/src/main/java/com/safetypin/authentication/dto/PasswordResetWithOTPRequest.java +++ b/src/main/java/com/safetypin/authentication/dto/PasswordResetWithOTPRequest.java @@ -17,6 +17,6 @@ public class PasswordResetWithOTPRequest { @Size(min = 8, message = "Password must be at least 8 characters") private String newPassword; - // Remove OTP field and its getter/setter - + @NotBlank(message = "Reset token is required") + private String resetToken; } diff --git a/src/main/java/com/safetypin/authentication/dto/ResetTokenResponse.java b/src/main/java/com/safetypin/authentication/dto/ResetTokenResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..225b1d5d06c8d137357be372a1780bffb90d5672 --- /dev/null +++ b/src/main/java/com/safetypin/authentication/dto/ResetTokenResponse.java @@ -0,0 +1,15 @@ +package com.safetypin.authentication.dto; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class ResetTokenResponse { + private String resetToken; + + public ResetTokenResponse(String resetToken) { + this.resetToken = resetToken; + } + +} diff --git a/src/main/java/com/safetypin/authentication/service/AuthenticationService.java b/src/main/java/com/safetypin/authentication/service/AuthenticationService.java index 8aa30631b34ef5f4223eebf1b98237c3a2b299f8..977e500ef644b2a3a23e5c6574ae70e65924d5b2 100644 --- a/src/main/java/com/safetypin/authentication/service/AuthenticationService.java +++ b/src/main/java/com/safetypin/authentication/service/AuthenticationService.java @@ -107,8 +107,8 @@ public class AuthenticationService { logger.info("Password reset OTP generated for email {} at {}: {}", email, java.time.LocalDateTime.now(), otp); } - // Verify OTP for password reset - public boolean verifyPasswordResetOTP(String email, String otp) { + // Verify OTP for password reset and generate a reset token + public String verifyPasswordResetOTP(String email, String otp) { Optional<User> userOpt = userService.findByEmail(email); if (userOpt.isEmpty() || !EMAIL_PROVIDER.equals(userOpt.get().getProvider())) { throw new IllegalArgumentException("Password reset is only available for email-registered users."); @@ -116,33 +116,31 @@ public class AuthenticationService { boolean isValid = otpService.verifyOTP(email, otp); if (isValid) { - // Mark this email as verified for password reset - otpService.markVerifiedForPasswordReset(email); - logger.info("Password reset OTP verified for {} at {}", email, java.time.LocalDateTime.now()); + // Generate a reset token valid for 3 minutes + String resetToken = otpService.generateResetToken(email); + logger.info("Password reset OTP verified for {}. Reset token generated at {}", email, java.time.LocalDateTime.now()); + return resetToken; } else { logger.warn("Invalid password reset OTP attempt for {} at {}", email, java.time.LocalDateTime.now()); + return null; } - return isValid; } - // Reset password without requiring OTP again - public void resetPassword(String email, String newPassword) { + // Reset password using the reset token + public void resetPassword(String email, String newPassword, String resetToken) { Optional<User> userOpt = userService.findByEmail(email); if (userOpt.isEmpty() || !EMAIL_PROVIDER.equals(userOpt.get().getProvider())) { throw new IllegalArgumentException("Password reset is only available for email-registered users."); } - if (!otpService.isVerifiedForPasswordReset(email)) { - throw new InvalidCredentialsException("You must verify your OTP before resetting password."); + if (resetToken == null || !otpService.verifyResetToken(resetToken, email)) { + throw new InvalidCredentialsException("Invalid or expired reset token. Please request a new OTP."); } User user = userOpt.get(); user.setPassword(passwordEncoder.encode(newPassword)); userService.save(user); - // Clear the verification status - otpService.clearPasswordResetVerification(email); - logger.info("Password reset successfully for {} at {}", email, java.time.LocalDateTime.now()); } diff --git a/src/main/java/com/safetypin/authentication/service/OTPService.java b/src/main/java/com/safetypin/authentication/service/OTPService.java index d14198469bc8f8803a674861a98c5f45fd839af7..835a99fd9f627a1629b1365d470eef7f524fcade 100644 --- a/src/main/java/com/safetypin/authentication/service/OTPService.java +++ b/src/main/java/com/safetypin/authentication/service/OTPService.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import java.security.SecureRandom; import java.time.LocalDateTime; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -15,13 +16,12 @@ import java.util.concurrent.ExecutionException; public class OTPService { private static final long OTP_EXPIRATION_SECONDS = 120; // 2 minutes expiration private static final Logger log = LoggerFactory.getLogger(OTPService.class); + private static final long RESET_TOKEN_EXPIRATION_SECONDS = 180; // 3 minutes private final EmailService emailService; private final ConcurrentHashMap<String, OTPDetails> otpStorage = new ConcurrentHashMap<>(); private final SecureRandom random = new SecureRandom(); - - // Store verified emails for password reset - private final ConcurrentHashMap<String, LocalDateTime> verifiedResetEmails = new ConcurrentHashMap<>(); - private static final long RESET_VERIFICATION_EXPIRATION_SECONDS = 300; // 5 minutes + // Store reset tokens with their expiration time + private final ConcurrentHashMap<String, ResetTokenDetails> resetTokenStorage = new ConcurrentHashMap<>(); @Autowired public OTPService(EmailService emailService) { @@ -51,7 +51,7 @@ public class OTPService { if (otp == null) { throw new NullPointerException("OTP cannot be null"); } - + OTPDetails details = otpStorage.get(email); if (details == null) { return false; @@ -69,49 +69,54 @@ public class OTPService { } /** - * Marks an email as verified for password reset + * Generate a reset token after OTP verification + * * @param email the email address + * @return the reset token */ - public void markVerifiedForPasswordReset(String email) { - verifiedResetEmails.put(email, LocalDateTime.now()); + public String generateResetToken(String email) { + String token = UUID.randomUUID().toString(); + resetTokenStorage.put(token, new ResetTokenDetails(email, LocalDateTime.now())); + log.info("Generated reset token for {}", email); + return token; } - + /** - * Checks if email is verified for password reset - * @param email the email address - * @return true if verified, false otherwise + * Verify if a reset token is valid + * + * @param token the reset token + * @param email the email associated with the token + * @return true if valid, false otherwise */ - public boolean isVerifiedForPasswordReset(String email) { - LocalDateTime verificationTime = verifiedResetEmails.get(email); - if (verificationTime == null) { + public boolean verifyResetToken(String token, String email) { + ResetTokenDetails details = resetTokenStorage.get(token); + + if (details == null) { + log.warn("Reset token not found: {}", token); return false; } - - // Check if verification has expired - if (verificationTime.plusSeconds(RESET_VERIFICATION_EXPIRATION_SECONDS).isBefore(LocalDateTime.now())) { - verifiedResetEmails.remove(email); + + // Check if token has expired + if (details.generatedAt.plusSeconds(RESET_TOKEN_EXPIRATION_SECONDS).isBefore(LocalDateTime.now())) { + log.warn("Reset token expired: {}", token); + resetTokenStorage.remove(token); return false; } - + + // Check if token matches the email + if (!details.email.equals(email)) { + log.warn("Email mismatch for token. Expected: {}, Actual: {}", details.email, email); + return false; + } + + // Token is valid, remove it to prevent reuse + resetTokenStorage.remove(token); return true; } - - /** - * Clears the verification status for password reset - * @param email the email address - */ - public void clearPasswordResetVerification(String email) { - verifiedResetEmails.remove(email); - } - /** - * Clears the OTP for the specified email - * @param email the email address - */ - public void clearOTP(String email) { - otpStorage.remove(email); + private record OTPDetails(String otp, LocalDateTime generatedAt) { } - private record OTPDetails(String otp, LocalDateTime generatedAt) { + private record ResetTokenDetails(String email, LocalDateTime generatedAt) { } }