From 57e9a53cf13bc7517b555e429a7131a12e077a11 Mon Sep 17 00:00:00 2001 From: Claudio Maggioni Date: Fri, 6 Mar 2020 11:10:58 +0100 Subject: [PATCH] Password reset manually tested and fixed --- build.gradle | 1 + .../config/EmailConfigurationService.java | 90 +++++++++++++++++++ .../controller/UserAccountController.java | 88 +++++++++++++----- .../models/ConfirmationTokenRepository.java | 4 + src/main/resources/application.properties | 4 +- 5 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java diff --git a/build.gradle b/build.gradle index 086613b..d7a34ff 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation 'org.postgresql:postgresql' compile "io.springfox:springfox-swagger2:2.9.2" compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' + compile "org.springframework.boot:spring-boot-configuration-processor" implementation('org.springframework.boot:spring-boot-starter-web') { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-json' diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java new file mode 100644 index 0000000..424d2d3 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/EmailConfigurationService.java @@ -0,0 +1,90 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +import javax.validation.constraints.NotNull; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Class to interface with `email.*` properties in application.properties. This properties are used + * for generating the email to send on password reset or registration + * + * @see ch.usi.inf.sa4.sanmarinoes.smarthut.controller.UserAccountController + */ +@Component +@EnableConfigurationProperties +@ConfigurationProperties(prefix = "email") +public class EmailConfigurationService { + + /** The email subject for a registration email */ + @NotNull private String registrationSubject; + + /** The text in the email body preceding the confirmation URL for a registration email */ + @NotNull private String registration; + + /** + * The URL to follow for registration email confirmation. Has to end with the start of a query + * parameter + */ + @NotNull private String registrationPath; + + /** The email subject for a reset password email */ + @NotNull private String resetPasswordSubject; + + /** The text in the email body preceding the confirmation URL for a reset password email */ + @NotNull private String resetPassword; + + /** + * The URL to follow for password reset email confirmation. Has to end with the start of a query + * parameter + */ + @NotNull private String resetPasswordPath; + + public String getRegistrationSubject() { + return registrationSubject; + } + + public void setRegistrationSubject(String registrationSubject) { + this.registrationSubject = registrationSubject; + } + + public String getRegistration() { + return registration; + } + + public void setRegistration(String registration) { + this.registration = registration; + } + + public String getRegistrationPath() { + return registrationPath; + } + + public void setRegistrationPath(String registrationPath) { + this.registrationPath = registrationPath; + } + + public String getResetPasswordSubject() { + return resetPasswordSubject; + } + + public void setResetPasswordSubject(String resetPasswordSubject) { + this.resetPasswordSubject = resetPasswordSubject; + } + + public String getResetPassword() { + return resetPassword; + } + + public void setResetPassword(String resetPassword) { + this.resetPassword = resetPassword; + } + + public String getResetPasswordPath() { + return resetPasswordPath; + } + + public void setResetPasswordPath(String resetPasswordPath) { + this.resetPasswordPath = resetPasswordPath; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java index 4959e79..ebf354f 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/UserAccountController.java @@ -1,5 +1,6 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.EmailConfigurationService; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.InitPasswordResetRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.OkResponse; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.PasswordResetRequest; @@ -14,56 +15,66 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; import ch.usi.inf.sa4.sanmarinoes.smarthut.service.EmailSenderService; import javax.validation.Valid; import javax.validation.constraints.NotNull; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.mail.SimpleMailMessage; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.bind.annotation.*; +/** Unauthenticated set of endpoints to handle registration and password reset */ @RestController @EnableAutoConfiguration @RequestMapping("/register") public class UserAccountController { - @Autowired private UserRepository userRepository; + private final UserRepository userRepository; - @Autowired private ConfirmationTokenRepository confirmationTokenRepository; + private final ConfirmationTokenRepository confirmationTokenRepository; - @Autowired private EmailSenderService emailSenderService; + private final EmailSenderService emailSenderService; - @Autowired private BCryptPasswordEncoder encoder; + private final BCryptPasswordEncoder encoder; - @Value("email.registrationsubject") - private String emailRegistrationSubject; + private final EmailConfigurationService emailConfig; - @Value("email.resetpasswordsubject") - private String resetPasswordSubject; - - @Value("email.registration") - private String emailRegistrationText; - - @Value("email.resetpassword") - private String resetPasswordText; - - @Value("email.serverhost") - private String serverHost; + public UserAccountController( + UserRepository userRepository, + ConfirmationTokenRepository confirmationTokenRepository, + EmailSenderService emailSenderService, + BCryptPasswordEncoder encoder, + EmailConfigurationService emailConfig) { + this.userRepository = userRepository; + this.confirmationTokenRepository = confirmationTokenRepository; + this.emailSenderService = emailSenderService; + this.encoder = encoder; + this.emailConfig = emailConfig; + } private void sendEmail(String email, ConfirmationToken token, boolean isRegistration) { SimpleMailMessage mailMessage = new SimpleMailMessage(); mailMessage.setTo(email); - mailMessage.setSubject(isRegistration ? emailRegistrationSubject : resetPasswordSubject); + mailMessage.setSubject( + isRegistration + ? emailConfig.getRegistrationSubject() + : emailConfig.getResetPasswordSubject()); mailMessage.setFrom("smarthut.sm@gmail.com"); mailMessage.setText( - (isRegistration ? emailRegistrationText : resetPasswordText) + (isRegistration ? emailConfig.getRegistration() : emailConfig.getResetPassword()) + " " - + serverHost - + "/register/confirm-account?token=" + + (isRegistration + ? emailConfig.getRegistrationPath() + : emailConfig.getResetPasswordPath()) + token.getConfirmationToken()); emailSenderService.sendEmail(mailMessage); } + /** + * Unauthenticated endpoint to call to send a password reset email + * + * @param registrationData registration data of the new user + * @return success + * @throws DuplicateRegistrationException if a user exists with same email or username + */ @PostMapping public OkResponse registerUser(@Valid @RequestBody UserRegistrationRequest registrationData) throws DuplicateRegistrationException { @@ -104,6 +115,13 @@ public class UserAccountController { } } + /** + * Unauthenticated endpoint to call to send a password reset email + * + * @param resetRequest a JSON object containing the email of the user to reset + * @return success + * @throws UserNotFoundException if given email does not belong to any user + */ @PostMapping("/init-reset-password") public OkResponse initResetPassword(@Valid @RequestBody InitPasswordResetRequest resetRequest) throws UserNotFoundException { @@ -121,6 +139,10 @@ public class UserAccountController { } while (confirmationTokenRepository.findByConfirmationToken(token.getConfirmationToken()) != null); + // Delete existing email password reset tokens + confirmationTokenRepository.deleteByUserAndResetPassword(toReset, true); + + // Save new token confirmationTokenRepository.save(token); sendEmail(toReset.getEmail(), token, false); @@ -128,6 +150,13 @@ public class UserAccountController { return new OkResponse(); } + /** + * Unauthenticated endpoint to call with token sent by email to reset password + * + * @param resetRequest the token given via email and the new password + * @return success + * @throws EmailTokenNotFoundException if given token is not a valid token for password reset + */ @PutMapping("/reset-password") public OkResponse resetPassword(@Valid @RequestBody PasswordResetRequest resetRequest) throws EmailTokenNotFoundException { @@ -135,7 +164,7 @@ public class UserAccountController { confirmationTokenRepository.findByConfirmationToken( resetRequest.getConfirmationToken()); - if (token == null || token.getResetPassword()) { + if (token == null || !token.getResetPassword()) { throw new EmailTokenNotFoundException(); } @@ -143,9 +172,20 @@ public class UserAccountController { user.setPassword(encoder.encode(resetRequest.getPassword())); userRepository.save(user); + // Delete token to prevent further password changes + confirmationTokenRepository.delete(token); + return new OkResponse(); } + /** + * Unauthenticated endpoint to call with token sent by email to enable user + * + * @param confirmationToken the token given via email + * @return success + * @throws EmailTokenNotFoundException if given token is not a valid token for email + * confirmation + */ @GetMapping(value = "/confirm-account") public OkResponse confirmUserAccount(@RequestParam("token") @NotNull String confirmationToken) throws EmailTokenNotFoundException { diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java index 4bc18ce..9bf3791 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/ConfirmationTokenRepository.java @@ -1,7 +1,11 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; +import javax.transaction.Transactional; import org.springframework.data.repository.CrudRepository; public interface ConfirmationTokenRepository extends CrudRepository { ConfirmationToken findByConfirmationToken(String confirmationToken); + + @Transactional + void deleteByUserAndResetPassword(User user, boolean resetPassword); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 26d5e8e..14c91cd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -26,8 +26,8 @@ spring.mail.properties.mail.smtp.writetimeout=5000 email.registrationsubject=Complete your SmartHut.sm registration email.registration=To confirm your registration, please click here: +email.registraionpath=http://localhost:8080/register/confirm-account?token= email.resetpasswordsubject=SmartHut.sm password reset email.resetpassword=To reset your password, please click here: - -email.serverhost=http://localhost:8080/ \ No newline at end of file +email.resetpasswordpath=http://localhost:3000/password-reset?token= \ No newline at end of file