/**
* =============================================================================
*
* ORCID (R) Open Source
* http://orcid.org
*
* Copyright (c) 2012-2014 ORCID, Inc.
* Licensed under an MIT-Style License (MIT)
* http://orcid.org/open-source-license
*
* This copyright and license information (including a link to the full license)
* shall be included in its entirety in all copies or substantial portion of
* the software.
*
* =============================================================================
*/
package org.orcid.frontend.web.controllers;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.orcid.core.manager.EncryptionManager;
import org.orcid.core.manager.LoadOptions;
import org.orcid.core.manager.NotificationManager;
import org.orcid.core.manager.OrcidProfileCacheManager;
import org.orcid.core.manager.ProfileEntityManager;
import org.orcid.core.manager.RegistrationManager;
import org.orcid.core.utils.PasswordResetToken;
import org.orcid.frontend.spring.ShibbolethAjaxAuthenticationSuccessHandler;
import org.orcid.frontend.spring.SocialAjaxAuthenticationSuccessHandler;
import org.orcid.frontend.spring.web.social.config.SocialContext;
import org.orcid.frontend.web.forms.OneTimeResetPasswordForm;
import org.orcid.jaxb.model.message.OrcidProfile;
import org.orcid.jaxb.model.message.SecurityQuestionId;
import org.orcid.password.constants.OrcidPasswordConstants;
import org.orcid.pojo.EmailRequest;
import org.orcid.pojo.Redirect;
import org.orcid.pojo.ajaxForm.PojoUtil;
import org.orcid.pojo.ajaxForm.Reactivation;
import org.orcid.pojo.ajaxForm.Registration;
import org.orcid.utils.OrcidStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class PasswordResetController extends BaseController {
private static final Logger LOGGER = LoggerFactory.getLogger(PasswordResetController.class);
@Resource
private RegistrationManager registrationManager;
@Resource
private EncryptionManager encryptionManager;
@Resource
private NotificationManager notificationManager;
@Resource
private OrcidProfileCacheManager orcidProfileCacheManager;
@Autowired
private SocialContext socialContext;
@Resource
private SocialAjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandlerSocial;
@Resource
private ShibbolethAjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandlerShibboleth;
@Resource
private ProfileEntityManager profileEntityManager;
@Resource
private RegistrationController registrationController;
private static final List<String> RESET_PASSWORD_PARAMS_WHITELIST = Arrays.asList("_");
@RequestMapping(value = "/reset-password", method = RequestMethod.GET)
public ModelAndView resetPassword(@RequestParam(value = "expired", required = false) boolean expired) {
ModelAndView mav = new ModelAndView("reset_password");
mav.addObject("tokenExpired", expired);
return mav;
}
@RequestMapping(value = "/reset-password.json", method = RequestMethod.GET)
public @ResponseBody EmailRequest getPasswordResetRequest() {
return new EmailRequest();
}
@RequestMapping(value = "/validate-reset-password.json", method = RequestMethod.POST)
public @ResponseBody EmailRequest validateResetPasswordRequest(@RequestBody EmailRequest passwordResetRequest) {
List<String> errors = new ArrayList<>();
passwordResetRequest.setErrors(errors);
if (!validateEmailAddress(passwordResetRequest.getEmail())) {
errors.add(getMessage("Email.resetPasswordForm.invalidEmail"));
}
return passwordResetRequest;
}
@RequestMapping(value = "/reset-password.json", method = RequestMethod.POST)
public @ResponseBody ResponseEntity<EmailRequest> issuePasswordResetRequest(HttpServletRequest request, @RequestBody EmailRequest passwordResetRequest) {
for (String param : request.getParameterMap().keySet()) {
if (!RESET_PASSWORD_PARAMS_WHITELIST.contains(param)) {
// found parameter that has not been white-listed
return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY);
}
}
List<String> errors = new ArrayList<>();
passwordResetRequest.setErrors(errors);
if (!validateEmailAddress(passwordResetRequest.getEmail())) {
errors.add(getMessage("Email.resetPasswordForm.invalidEmail"));
return new ResponseEntity<>(passwordResetRequest, HttpStatus.OK);
}
OrcidProfile profile = orcidProfileManager.retrieveOrcidProfileByEmail(passwordResetRequest.getEmail(), LoadOptions.BIO_ONLY);
if (profile == null) {
errors.add(getMessage("orcid.frontend.reset.password.email_not_found", passwordResetRequest.getEmail()));
return new ResponseEntity<>(passwordResetRequest, HttpStatus.OK);
}
if (profile.isDeactivated()) {
errors.add(getMessage("orcid.frontend.reset.password.disabled_account", passwordResetRequest.getEmail()));
return new ResponseEntity<>(passwordResetRequest, HttpStatus.OK);
}
registrationManager.resetUserPassword(passwordResetRequest.getEmail(), profile);
passwordResetRequest.setSuccessMessage(getMessage("orcid.frontend.reset.password.successfulReset") + " " + passwordResetRequest.getEmail());
return new ResponseEntity<>(passwordResetRequest, HttpStatus.OK);
}
@RequestMapping(value = "/reset-password-email/{encryptedEmail}", method = RequestMethod.GET)
public ModelAndView resetPasswordEmail(HttpServletRequest request, @PathVariable("encryptedEmail") String encryptedEmail, RedirectAttributes redirectAttributes) {
PasswordResetToken passwordResetToken = buildResetTokenFromEncryptedLink(encryptedEmail);
if (isTokenExpired(passwordResetToken)) {
redirectAttributes.addFlashAttribute("passwordResetLinkExpired", true);
return new ModelAndView("redirect:/reset-password?expired=true");
}
ModelAndView result = new ModelAndView("password_one_time_reset_optional_security_questions");
result.addObject("noIndex", true);
return result;
}
@RequestMapping(value = "/reset-password-form-validate.json", method = RequestMethod.POST)
public @ResponseBody OneTimeResetPasswordForm resetPasswordConfirmValidate(@RequestBody OneTimeResetPasswordForm resetPasswordForm) {
resetPasswordForm.setErrors(new ArrayList<String>());
if (resetPasswordForm.getPassword() == null || !resetPasswordForm.getPassword().matches(OrcidPasswordConstants.ORCID_PASSWORD_REGEX)) {
setError(resetPasswordForm, "Pattern.registrationForm.password");
}
if (resetPasswordForm.getRetypedPassword() != null && !resetPasswordForm.getRetypedPassword().equals(resetPasswordForm.getPassword())) {
setError(resetPasswordForm, "FieldMatch.registrationForm");
}
if (registrationManager.passwordIsCommon(resetPasswordForm.getPassword())) {
setError(resetPasswordForm, "password.too_common", resetPasswordForm.getPassword());
}
return resetPasswordForm;
}
@RequestMapping(value = "/password-reset.json", method = RequestMethod.GET)
public @ResponseBody OneTimeResetPasswordForm getResetPassword() {
OneTimeResetPasswordForm form = new OneTimeResetPasswordForm();
form.setSecurityQuestionId(0);
return form;
}
@RequestMapping(value = "/reset-password-email.json", method = RequestMethod.POST)
public @ResponseBody OneTimeResetPasswordForm submitPasswordReset(HttpServletRequest request, HttpServletResponse response,
@RequestBody OneTimeResetPasswordForm oneTimeResetPasswordForm) {
oneTimeResetPasswordForm.setErrors(new ArrayList<String>());
PasswordResetToken passwordResetToken = buildResetTokenFromEncryptedLink(oneTimeResetPasswordForm.getEncryptedEmail());
if (isTokenExpired(passwordResetToken)) {
setError(oneTimeResetPasswordForm, "orcid.frontend.reset.password.resetLinkExpired");
return oneTimeResetPasswordForm;
}
if (oneTimeResetPasswordForm.getPassword() == null || !oneTimeResetPasswordForm.getPassword().matches(OrcidPasswordConstants.ORCID_PASSWORD_REGEX)) {
setError(oneTimeResetPasswordForm, "Pattern.registrationForm.password");
return oneTimeResetPasswordForm;
}
OrcidProfile profileToUpdate = orcidProfileManager.retrieveOrcidProfileByEmail(passwordResetToken.getEmail(), LoadOptions.INTERNAL_ONLY);
profileToUpdate.setPassword(oneTimeResetPasswordForm.getPassword());
if (oneTimeResetPasswordForm.isSecurityDetailsPopulated()) {
profileToUpdate.getOrcidInternal().getSecurityDetails().setSecurityQuestionId(new SecurityQuestionId(oneTimeResetPasswordForm.getSecurityQuestionId()));
profileToUpdate.setSecurityQuestionAnswer(oneTimeResetPasswordForm.getSecurityQuestionAnswer());
}
orcidProfileManager.updatePasswordInformation(profileToUpdate);
String redirectUrl = calculateRedirectUrl(request, response);
oneTimeResetPasswordForm.setSuccessRedirectLocation(redirectUrl);
return oneTimeResetPasswordForm;
}
private PasswordResetToken buildResetTokenFromEncryptedLink(String encryptedLink) {
try {
String paramsString = encryptionManager.decryptForExternalUse(new String(Base64.decodeBase64(encryptedLink), "UTF-8"));
return new PasswordResetToken(paramsString);
} catch (UnsupportedEncodingException e) {
LOGGER.error("Could not decrypt " + encryptedLink);
throw new RuntimeException(getMessage("web.orcid.decrypt_passwordreset.exception"));
}
}
private boolean isTokenExpired(PasswordResetToken passwordResetToken) {
Date expiryDateOfOneHourFromIssueDate = org.apache.commons.lang.time.DateUtils.addHours(passwordResetToken.getIssueDate(), 4);
Date now = new Date();
return (expiryDateOfOneHourFromIssueDate.getTime() < now.getTime());
}
@RequestMapping(value = "/sendReactivation.json", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void sendReactivation(@RequestParam("email") String email) {
OrcidProfile orcidProfile = orcidProfileCacheManager.retrieve(emailManager.findOrcidIdByEmail(email));
notificationManager.sendReactivationEmail(email, orcidProfile);
}
@RequestMapping(value = "/reactivation/{resetParams}", method = RequestMethod.GET)
public ModelAndView reactivation(HttpServletRequest request, @PathVariable("resetParams") String resetParams, RedirectAttributes redirectAttributes) {
PasswordResetToken passwordResetToken = buildResetTokenFromEncryptedLink(resetParams);
ModelAndView mav = new ModelAndView("reactivation");
if (isTokenExpired(passwordResetToken)) {
mav.addObject("reactivationLinkExpired", true);
}
mav.addObject("resetParams", resetParams);
return mav;
}
@RequestMapping(value = { "/reactivationConfirm.json", "/shibboleth/reactivationConfirm.json" }, method = RequestMethod.POST)
public @ResponseBody Object setReactivationConfirm(HttpServletRequest request, HttpServletResponse response, @RequestBody Reactivation reg)
throws UnsupportedEncodingException {
Redirect r = new Redirect();
// Strip any html code from names before validating them
if (!PojoUtil.isEmpty(reg.getFamilyNames())) {
reg.getFamilyNames().setValue(OrcidStringUtils.stripHtml(reg.getFamilyNames().getValue()));
}
if (!PojoUtil.isEmpty(reg.getGivenNames())) {
reg.getGivenNames().setValue(OrcidStringUtils.stripHtml(reg.getGivenNames().getValue()));
}
// make sure validation still passes
validateReactivationFields(request, reg);
if (reg.getErrors() != null && reg.getErrors().size() > 0) {
return reg;
}
if (reg.getValNumServer() == 0 || reg.getValNumClient() != reg.getValNumServer() / 2) {
r.setUrl(getBaseUri() + "/register");
return r;
}
reactivateAndLogUserIn(request, response, reg);
if ("social".equals(reg.getLinkType()) && socialContext.isSignedIn(request, response) != null) {
ajaxAuthenticationSuccessHandlerSocial.linkSocialAccount(request, response);
} else if ("shibboleth".equals(reg.getLinkType())) {
ajaxAuthenticationSuccessHandlerShibboleth.linkShibbolethAccount(request, response);
}
String redirectUrl = calculateRedirectUrl(request, response);
r.setUrl(redirectUrl);
return r;
}
public void validateReactivationFields(HttpServletRequest request, Registration reg) {
reg.setErrors(new ArrayList<String>());
givenNameValidate(reg.getGivenNames());
passwordValidate(reg.getPasswordConfirm(), reg.getPassword());
passwordConfirmValidate(reg.getPasswordConfirm(), reg.getPassword());
termsOfUserValidate(reg.getTermsOfUse());
copyErrors(reg.getGivenNames(), reg);
copyErrors(reg.getPassword(), reg);
copyErrors(reg.getPasswordConfirm(), reg);
copyErrors(reg.getTermsOfUse(), reg);
}
public void reactivateAndLogUserIn(HttpServletRequest request, HttpServletResponse response, Reactivation reactivation) {
PasswordResetToken resetParams = buildResetTokenFromEncryptedLink(reactivation.getResetParams());
String email = resetParams.getEmail();
String orcid = emailManager.findOrcidIdByEmail(email);
LOGGER.info("About to reactivate record, orcid={}, email={}", orcid, email);
String password = reactivation.getPassword().getValue();
// Reactivate user
profileEntityManager.reactivate(orcid, reactivation.getGivenNames().getValue(), reactivation.getFamilyNames().getValue(), password,
reactivation.getActivitiesVisibilityDefault().getVisibility());
// Verify email used to reactivate
emailManager.verifyEmail(email);
registrationController.logUserIn(request, response, orcid, password);
}
}