From 7aa0c7ee72d4cf63453f0187cd86c3938144d388 Mon Sep 17 00:00:00 2001 From: Daniel Gerhardt <daniel.gerhardt@mni.thm.de> Date: Tue, 29 Jul 2014 22:50:35 +0200 Subject: [PATCH] Add password reset implementation --- .../arsnova/controller/UserController.java | 29 ++++- .../java/de/thm/arsnova/dao/CouchDBDao.java | 2 + .../java/de/thm/arsnova/entities/DbUser.java | 18 +++ .../de/thm/arsnova/services/IUserService.java | 4 + .../de/thm/arsnova/services/UserService.java | 106 +++++++++++++++--- src/main/webapp/arsnova.properties.example | 4 + src/test/resources/arsnova.properties.example | 4 + 7 files changed, 148 insertions(+), 19 deletions(-) diff --git a/src/main/java/de/thm/arsnova/controller/UserController.java b/src/main/java/de/thm/arsnova/controller/UserController.java index e86a512ee..9d3fe607d 100644 --- a/src/main/java/de/thm/arsnova/controller/UserController.java +++ b/src/main/java/de/thm/arsnova/controller/UserController.java @@ -80,7 +80,8 @@ public class UserController extends AbstractController { @RequestMapping(value = { "/{username}/activate" }, method = { RequestMethod.POST, RequestMethod.GET }) - public final void activate(@PathVariable final String username, + public final void activate( + @PathVariable final String username, @RequestParam final String key, final HttpServletRequest request, final HttpServletResponse response) { DbUser dbUser = userService.getDbUser(username); @@ -95,11 +96,35 @@ public class UserController extends AbstractController { } @RequestMapping(value = { "/{username}" }, method = RequestMethod.DELETE) - public final void activate(@PathVariable final String username, + public final void activate( + @PathVariable final String username, final HttpServletRequest request, final HttpServletResponse response) { if (null == userService.deleteDbUser(username)) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } } + + @RequestMapping(value = { "/{username}/resetpassword" }, method = RequestMethod.POST) + public final void resetPassword( + @PathVariable final String username, + @RequestParam(required = false) final String key, + @RequestParam(required = false) final String password, + final HttpServletRequest request, + final HttpServletResponse response) { + DbUser dbUser = userService.getDbUser(username); + if (null == dbUser) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + if (null != key) { + if (!userService.resetPassword(dbUser, key, password)) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + } else { + userService.initiatePasswordReset(username); + } + } } diff --git a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java index 8b85944ac..57857888f 100644 --- a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java +++ b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java @@ -1379,6 +1379,8 @@ public class CouchDBDao implements IDatabaseDao { d.put("username", user.getUsername()); d.put("password", user.getPassword()); d.put("activationKey", user.getActivationKey()); + d.put("passwordResetKey", user.getPasswordResetKey()); + d.put("passwordResetTime", user.getPasswordResetTime()); d.put("creation", user.getCreation()); d.put("lastLogin", user.getLastLogin()); diff --git a/src/main/java/de/thm/arsnova/entities/DbUser.java b/src/main/java/de/thm/arsnova/entities/DbUser.java index a6db5373f..7d185c25e 100644 --- a/src/main/java/de/thm/arsnova/entities/DbUser.java +++ b/src/main/java/de/thm/arsnova/entities/DbUser.java @@ -6,6 +6,8 @@ public class DbUser { private String username; private String password; private String activationKey; + private String passwordResetKey; + private long passwordResetTime; private long creation; private long lastLogin; @@ -59,6 +61,22 @@ public class DbUser { this.activationKey = activationKey; } + public String getPasswordResetKey() { + return passwordResetKey; + } + + public void setPasswordResetKey(String passwordResetKey) { + this.passwordResetKey = passwordResetKey; + } + + public long getPasswordResetTime() { + return passwordResetTime; + } + + public void setPasswordResetTime(long passwordResetTime) { + this.passwordResetTime = passwordResetTime; + } + public long getCreation() { return creation; } diff --git a/src/main/java/de/thm/arsnova/services/IUserService.java b/src/main/java/de/thm/arsnova/services/IUserService.java index 231ea9496..8245da538 100644 --- a/src/main/java/de/thm/arsnova/services/IUserService.java +++ b/src/main/java/de/thm/arsnova/services/IUserService.java @@ -66,4 +66,8 @@ public interface IUserService { DbUser updateDbUser(DbUser dbUser); DbUser deleteDbUser(String username); + + void initiatePasswordReset(String username); + + boolean resetPassword(DbUser dbUser, String key, String password); } diff --git a/src/main/java/de/thm/arsnova/services/UserService.java b/src/main/java/de/thm/arsnova/services/UserService.java index 7fd1000b1..d2eaad804 100644 --- a/src/main/java/de/thm/arsnova/services/UserService.java +++ b/src/main/java/de/thm/arsnova/services/UserService.java @@ -50,6 +50,7 @@ import com.github.leleuj.ss.oauth.client.authentication.OAuthAuthenticationToken import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.entities.DbUser; import de.thm.arsnova.entities.User; +import de.thm.arsnova.exceptions.BadRequestException; import de.thm.arsnova.exceptions.NotFoundException; import de.thm.arsnova.exceptions.UnauthorizedException; import de.thm.arsnova.socket.ARSnovaSocketIOServer; @@ -65,6 +66,10 @@ public class UserService implements IUserService { private static final int MAX_USER_INACTIVE_SECONDS = 120; + private static final int REPEATED_PASSWORD_RESET_DELAY_MS = 3 * 60 * 1000; + + private static final int PASSWORD_RESET_KEY_DURABILITY_MS = 2 * 60 * 60 * 1000; + public static final Logger LOGGER = LoggerFactory.getLogger(UserService.class); private static final ConcurrentHashMap<UUID, User> socketid2user = new ConcurrentHashMap<UUID, User>(); @@ -96,11 +101,14 @@ public class UserService implements IUserService { @Value("${security.user-db.activation-path}") private String activationPath; + @Value("${security.user-db.reset-password-path}") + private String resetPasswordPath; + @Value("${mail.sender.address}") - private String regMailSenderAddress; + private String mailSenderAddress; @Value("${mail.sender.name}") - private String regMailSenderName; + private String mailSenderName; @Value("${security.user-db.registration-mail.subject}") private String regMailSubject; @@ -108,6 +116,12 @@ public class UserService implements IUserService { @Value("${security.user-db.registration-mail.body}") private String regMailBody; + @Value("${security.user-db.reset-password-mail.subject}") + private String resetPasswordMailSubject; + + @Value("${security.user-db.reset-password-mail.body}") + private String resetPasswordMailBody; + @Value("${security.authentication.login-try-limit}") private int loginTryLimit; @@ -397,7 +411,7 @@ public class UserService implements IUserService { String activationUrl; try { activationUrl = MessageFormat.format( - "{0}{1}/{2}?username={3}&key={4}", + "{0}{1}/{2}?action=activate&username={3}&key={4}", rootUrl, customizationPath, activationPath, @@ -409,21 +423,8 @@ public class UserService implements IUserService { return; } - MimeMessage msg = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(msg, "UTF-8"); - try { - helper.setFrom(regMailSenderName + "<" + regMailSenderAddress + ">"); - helper.setTo(dbUser.getUsername()); - helper.setSubject(regMailSubject); - helper.setText(MessageFormat.format(regMailBody, activationUrl)); - LOGGER.info("Sending activation mail from \"{}\" to \"{}\"", new Object[] {msg.getFrom(), dbUser.getUsername()}); - mailSender.send(msg); - } catch (MessagingException e) { - LOGGER.warn("Activation mail could not be sent: {}", e); - } catch (MailException e) { - LOGGER.warn("Activation mail could not be sent: {}", e); - } + sendEmail(dbUser, regMailSubject, MessageFormat.format(regMailBody, activationUrl)); } private void parseMailAddressPattern() { @@ -476,4 +477,75 @@ public class UserService implements IUserService { return dbUser; } + + @Override + public void initiatePasswordReset(String username) { + DbUser dbUser = databaseDao.getUser(username); + if (null == dbUser) { + throw new NotFoundException(); + } + if (System.currentTimeMillis() < dbUser.getPasswordResetTime() + REPEATED_PASSWORD_RESET_DELAY_MS) { + throw new BadRequestException(); + } + + dbUser.setPasswordResetKey(RandomStringUtils.randomAlphanumeric(32)); + dbUser.setPasswordResetTime(System.currentTimeMillis()); + databaseDao.createOrUpdateUser(dbUser); + + String resetPasswordUrl; + try { + resetPasswordUrl = MessageFormat.format( + "{0}{1}/{2}?action=resetpassword&username={3}&key={4}", + rootUrl, + customizationPath, + resetPasswordPath, + UriUtils.encodeQueryParam(dbUser.getUsername(), "UTF-8"), + dbUser.getPasswordResetKey() + ); + } catch (UnsupportedEncodingException e1) { + LOGGER.error(e1.getMessage()); + + return; + } + + sendEmail(dbUser, resetPasswordMailSubject, MessageFormat.format(resetPasswordMailBody, resetPasswordUrl)); + } + + @Override + public boolean resetPassword(DbUser dbUser, String key, String password) { + if (null == key || "".equals(key) || !key.equals(dbUser.getPasswordResetKey())) { + return false; + } + if (System.currentTimeMillis() > dbUser.getPasswordResetTime() + PASSWORD_RESET_KEY_DURABILITY_MS) { + dbUser.setPasswordResetKey(null); + dbUser.setPasswordResetTime(0); + updateDbUser(dbUser); + + return false; + } + + dbUser.setPassword(encodePassword(password)); + dbUser.setPasswordResetKey(null); + updateDbUser(dbUser); + + return true; + } + + private void sendEmail(DbUser dbUser, String subject, String body) { + MimeMessage msg = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(msg, "UTF-8"); + try { + helper.setFrom(mailSenderName + "<" + mailSenderAddress + ">"); + helper.setTo(dbUser.getUsername()); + helper.setSubject(subject); + helper.setText(body); + + LOGGER.info("Sending mail \"{}\" from \"{}\" to \"{}\"", new Object[] {subject, msg.getFrom(), dbUser.getUsername()}); + mailSender.send(msg); + } catch (MessagingException e) { + LOGGER.warn("Mail \"{}\" could not be sent: {}", subject, e); + } catch (MailException e) { + LOGGER.warn("Mail \"{}\" could not be sent: {}", subject, e); + } + } } diff --git a/src/main/webapp/arsnova.properties.example b/src/main/webapp/arsnova.properties.example index d15734e3f..8297b9e0b 100644 --- a/src/main/webapp/arsnova.properties.example +++ b/src/main/webapp/arsnova.properties.example @@ -79,12 +79,16 @@ security.user-db.enabled=true security.user-db.title=ARSnova security.user-db.login-dialog-path=account.html security.user-db.activation-path=account.html +security.user-db.reset-password-path=account.html security.user-db.image= security.user-db.allowed-email-domains=* security.user-db.registration-mail.subject=ARSnova Registration security.user-db.registration-mail.body=Welcome to ARSnova!\n\nPlease confirm \ your registration by visiting the following web address:\n{0}\n\n\ Afterwards, you can log into ARSnova with your e-mail address and password. +security.user-db.reset-password-mail.subject=ARSnova Password Reset +security.user-db.reset-password-mail.body=You requested to reset your \ + password.\n\nPlease follow the link below to set a new password:\n{0} # LDAP authentication # diff --git a/src/test/resources/arsnova.properties.example b/src/test/resources/arsnova.properties.example index d15734e3f..8297b9e0b 100644 --- a/src/test/resources/arsnova.properties.example +++ b/src/test/resources/arsnova.properties.example @@ -79,12 +79,16 @@ security.user-db.enabled=true security.user-db.title=ARSnova security.user-db.login-dialog-path=account.html security.user-db.activation-path=account.html +security.user-db.reset-password-path=account.html security.user-db.image= security.user-db.allowed-email-domains=* security.user-db.registration-mail.subject=ARSnova Registration security.user-db.registration-mail.body=Welcome to ARSnova!\n\nPlease confirm \ your registration by visiting the following web address:\n{0}\n\n\ Afterwards, you can log into ARSnova with your e-mail address and password. +security.user-db.reset-password-mail.subject=ARSnova Password Reset +security.user-db.reset-password-mail.body=You requested to reset your \ + password.\n\nPlease follow the link below to set a new password:\n{0} # LDAP authentication # -- GitLab