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