From bca6e50931706e16ea1985ed99d0bf4e3920f4c2 Mon Sep 17 00:00:00 2001 From: Camelia Dumitru Date: Mon, 10 Feb 2025 08:30:58 +0000 Subject: [PATCH] Added the resetLink function for admin portal --- .../orcid/core/utils/PasswordResetToken.java | 12 ++++ .../orcid/core/utils/VerifyEmailUtils.java | 6 ++ .../orcid/pojo/AdminResetPasswordLink.java | 56 ++++++++++++++++ .../web/controllers/AdminController.java | 51 +++++++++++++++ .../controllers/PasswordResetController.java | 2 +- .../web/controllers/AdminControllerTest.java | 64 +++++++++++++++++++ 6 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 orcid-core/src/main/java/org/orcid/pojo/AdminResetPasswordLink.java diff --git a/orcid-core/src/main/java/org/orcid/core/utils/PasswordResetToken.java b/orcid-core/src/main/java/org/orcid/core/utils/PasswordResetToken.java index a9efaa96eac..649534db093 100644 --- a/orcid-core/src/main/java/org/orcid/core/utils/PasswordResetToken.java +++ b/orcid-core/src/main/java/org/orcid/core/utils/PasswordResetToken.java @@ -20,11 +20,13 @@ public class PasswordResetToken { // public static final String RESET_TOKEN_DATE_FORMAT = "dd/MM/yyyy HH:mm:ss"; private static final String EMAIL_PARAM_KEY = "email"; private static final String ISSUE_DATE_PARAM_KEY = "issueDate"; + private static final String HOURS_PARAM_KEY = "h"; private static final String EQUALS = "="; private static final String SEPARATOR = "&"; private String email; private XMLGregorianCalendar issueDate; + private int durationInHours = 4; public PasswordResetToken() { @@ -38,19 +40,28 @@ public PasswordResetToken(String paramsString) { if (keyValue.length == 2) { params.put(keyValue[0], keyValue[1]); } + } email = params.get(EMAIL_PARAM_KEY); issueDate = DateUtils.convertToXMLGregorianCalendar(params.get(ISSUE_DATE_PARAM_KEY)); + if(StringUtils.isNotBlank(params.get(HOURS_PARAM_KEY))) { + durationInHours = Integer.valueOf(params.get(HOURS_PARAM_KEY)); + } } public String getEmail() { return email; } + + public int getDurationInHours() { + return durationInHours; + } public Date getIssueDate() { return issueDate.toGregorianCalendar().getTime(); } + /** * @@ -61,6 +72,7 @@ public String toParamsString() { List> pairs = new ArrayList>(); pairs.add(new ImmutablePair(EMAIL_PARAM_KEY, email)); pairs.add(new ImmutablePair(ISSUE_DATE_PARAM_KEY, String.valueOf(issueDate))); + pairs.add(new ImmutablePair(HOURS_PARAM_KEY, String.valueOf(durationInHours))); List items = new ArrayList(pairs.size()); for (Pair pair : pairs) { diff --git a/orcid-core/src/main/java/org/orcid/core/utils/VerifyEmailUtils.java b/orcid-core/src/main/java/org/orcid/core/utils/VerifyEmailUtils.java index 22368423d76..eff7ddeb3f4 100644 --- a/orcid-core/src/main/java/org/orcid/core/utils/VerifyEmailUtils.java +++ b/orcid-core/src/main/java/org/orcid/core/utils/VerifyEmailUtils.java @@ -80,6 +80,12 @@ public String createResetEmail(String userEmail, String baseUri) { String resetParams = MessageFormat.format("email={0}&issueDate={1}", new Object[] { userEmail, date.toXMLFormat() }); return createEmailBaseUrl(resetParams, baseUri, "reset-password-email"); } + + public String createResetLinkForAdmin(String userEmail, String baseUri) { + XMLGregorianCalendar date = DateUtils.convertToXMLGregorianCalendarNoTimeZoneNoMillis(new Date()); + String resetParams = MessageFormat.format("email={0}&issueDate={1}&h=24", new Object[] { userEmail, date.toXMLFormat() }); + return createEmailBaseUrl(resetParams, baseUri, "reset-password-email"); + } public String createReactivationUrl(String userEmail, String baseUri) { XMLGregorianCalendar date = DateUtils.convertToXMLGregorianCalendarNoTimeZoneNoMillis(new Date()); diff --git a/orcid-core/src/main/java/org/orcid/pojo/AdminResetPasswordLink.java b/orcid-core/src/main/java/org/orcid/pojo/AdminResetPasswordLink.java new file mode 100644 index 00000000000..3dfeec8f61f --- /dev/null +++ b/orcid-core/src/main/java/org/orcid/pojo/AdminResetPasswordLink.java @@ -0,0 +1,56 @@ +package org.orcid.pojo; + +import java.util.Date; + +public class AdminResetPasswordLink { + private String resetLink; + + private String email; + + private String error; + + private Date issueDate; + + private int durationInHours = 4; + + public String getResetLink() { + return resetLink; + } + + public void setResetLink(String resetLink) { + this.resetLink = resetLink; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public Date getIssueDate() { + return issueDate; + } + + public void setIssueDate(Date issueDate) { + this.issueDate = issueDate; + } + + public int getDurationInHours() { + return durationInHours; + } + + public void setDurationInHours(int durationInHours) { + this.durationInHours = durationInHours; + } + +} diff --git a/orcid-web/src/main/java/org/orcid/frontend/web/controllers/AdminController.java b/orcid-web/src/main/java/org/orcid/frontend/web/controllers/AdminController.java index 46fad3cfbbf..8682cfc5975 100644 --- a/orcid-web/src/main/java/org/orcid/frontend/web/controllers/AdminController.java +++ b/orcid-web/src/main/java/org/orcid/frontend/web/controllers/AdminController.java @@ -15,8 +15,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.orcid.core.manager.AdminManager; +import org.orcid.core.manager.EncryptionManager; import org.orcid.core.manager.ProfileEntityCacheManager; import org.orcid.core.manager.TwoFactorAuthenticationManager; import org.orcid.core.manager.v3.ClientDetailsManager; @@ -24,6 +26,8 @@ import org.orcid.core.manager.v3.ProfileEntityManager; import org.orcid.core.manager.v3.SpamManager; import org.orcid.core.manager.v3.read_only.RecordNameManagerReadOnly; +import org.orcid.core.utils.PasswordResetToken; +import org.orcid.core.utils.VerifyEmailUtils; import org.orcid.frontend.email.RecordEmailSender; import org.orcid.frontend.web.util.PasswordConstants; import org.orcid.jaxb.model.clientgroup.ClientType; @@ -37,6 +41,7 @@ import org.orcid.persistence.jpa.entities.ProfileEntity; import org.orcid.pojo.AdminChangePassword; import org.orcid.pojo.AdminDelegatesRequest; +import org.orcid.pojo.AdminResetPasswordLink; import org.orcid.pojo.ConvertClient; import org.orcid.pojo.LockAccounts; import org.orcid.pojo.ProfileDeprecationRequest; @@ -45,6 +50,7 @@ import org.orcid.utils.OrcidStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestBody; @@ -80,6 +86,12 @@ public class AdminController extends BaseController { @Resource(name = "clientDetailsManagerV3") private ClientDetailsManager clientDetailsManager; + + @Resource + private VerifyEmailUtils verifyEmailUtils; + + @Resource + private EncryptionManager encryptionManager; @Resource(name = "spamManager") SpamManager spamManager; @@ -89,6 +101,10 @@ public class AdminController extends BaseController { @Resource private TwoFactorAuthenticationManager twoFactorAuthenticationManager; + + + @Value("${org.orcid.admin.registry.url:https://orcid.org}") + private String registryUrl; private static final String CLAIMED = "(claimed)"; private static final String DEACTIVATED = "(deactivated)"; @@ -624,6 +640,31 @@ public Map findIdByEmailHelper(String csvEmails) { } return form; } + + /** + * Reset password validate + * + * @throws IllegalAccessException + * @throws UnsupportedEncodingException + */ + @RequestMapping(value = "/reset-password-link", method = RequestMethod.POST) + public @ResponseBody AdminResetPasswordLink resetPasswordLink(HttpServletRequest serverRequest, HttpServletResponse response, + @RequestBody AdminResetPasswordLink form) throws IllegalAccessException, UnsupportedEncodingException { + isAdmin(serverRequest, response); + form.setError(null); + String email = URLDecoder.decode(form.getEmail(), "UTF-8").trim(); + if (OrcidStringUtils.isEmailValid(email) && emailManager.emailExists(email)) { + String resetLink = verifyEmailUtils.createResetLinkForAdmin(email, registryUrl); + form.setResetLink(resetLink); + //need issue date as well + PasswordResetToken passwordResetToken = buildResetTokenFromEncryptedLink(resetLink); + form.setIssueDate(passwordResetToken.getIssueDate()); + form.setDurationInHours(passwordResetToken.getDurationInHours()); + } else { + form.setError(getMessage("admin.errors.unexisting_email")); + } + return form; + } /** * Admin switch user @@ -1161,4 +1202,14 @@ private String getOrcidFromParam(String orcidOrEmail) { return data; } } + + 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")); + } + } } diff --git a/orcid-web/src/main/java/org/orcid/frontend/web/controllers/PasswordResetController.java b/orcid-web/src/main/java/org/orcid/frontend/web/controllers/PasswordResetController.java index 14a07c9d155..48ed2673959 100644 --- a/orcid-web/src/main/java/org/orcid/frontend/web/controllers/PasswordResetController.java +++ b/orcid-web/src/main/java/org/orcid/frontend/web/controllers/PasswordResetController.java @@ -338,7 +338,7 @@ private PasswordResetToken buildResetTokenFromEncryptedLink(String encryptedLink } private boolean isTokenExpired(PasswordResetToken passwordResetToken) { - Date expiryDateOfOneHourFromIssueDate = org.apache.commons.lang.time.DateUtils.addHours(passwordResetToken.getIssueDate(), 4); + Date expiryDateOfOneHourFromIssueDate = org.apache.commons.lang.time.DateUtils.addHours(passwordResetToken.getIssueDate(), passwordResetToken.getDurationInHours()); Date now = new Date(); return (expiryDateOfOneHourFromIssueDate.getTime() < now.getTime()); } diff --git a/orcid-web/src/test/java/org/orcid/frontend/web/controllers/AdminControllerTest.java b/orcid-web/src/test/java/org/orcid/frontend/web/controllers/AdminControllerTest.java index 519493b405c..6155345401b 100644 --- a/orcid-web/src/test/java/org/orcid/frontend/web/controllers/AdminControllerTest.java +++ b/orcid-web/src/test/java/org/orcid/frontend/web/controllers/AdminControllerTest.java @@ -26,7 +26,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; +import org.apache.commons.codec.binary.Base64; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -44,7 +47,9 @@ import org.orcid.core.common.manager.EmailFrequencyManager; import org.orcid.core.locale.LocaleManager; import org.orcid.core.manager.AdminManager; +import org.orcid.core.manager.EncryptionManager; import org.orcid.core.manager.ProfileEntityCacheManager; +import org.orcid.core.manager.impl.OrcidUrlManager; import org.orcid.core.manager.v3.ClientDetailsManager; import org.orcid.core.manager.v3.EmailManager; import org.orcid.core.manager.v3.NotificationManager; @@ -58,6 +63,8 @@ import org.orcid.core.profile.history.ProfileHistoryEventType; import org.orcid.core.security.OrcidUserDetailsService; import org.orcid.core.security.OrcidWebRole; +import org.orcid.core.utils.PasswordResetToken; +import org.orcid.core.utils.VerifyEmailUtils; import org.orcid.frontend.email.RecordEmailSender; import org.orcid.frontend.web.util.BaseControllerTest; import org.orcid.jaxb.model.clientgroup.ClientType; @@ -73,6 +80,7 @@ import org.orcid.persistence.jpa.entities.RecordNameEntity; import org.orcid.pojo.AdminChangePassword; import org.orcid.pojo.AdminDelegatesRequest; +import org.orcid.pojo.AdminResetPasswordLink; import org.orcid.pojo.ConvertClient; import org.orcid.pojo.LockAccounts; import org.orcid.pojo.ProfileDeprecationRequest; @@ -80,6 +88,7 @@ import org.orcid.pojo.ajaxForm.Text; import org.orcid.test.OrcidJUnit4ClassRunner; import org.orcid.test.TargetProxyHelper; +import org.orcid.utils.DateUtils; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -148,6 +157,17 @@ public class AdminControllerTest extends BaseControllerTest { HttpServletResponse mockResponse = mock(HttpServletResponse.class); + @Mock + private OrcidUrlManager mockOrcidUrlManager; + + @Mock + private VerifyEmailUtils mockVerifyEmailUtils; + + @Mock + private EncryptionManager mockEncryptionManager; + + + @Captor private ArgumentCaptor adminUser; @@ -1403,4 +1423,48 @@ public void testConvertClient() throws IllegalAccessException, UnsupportedEncodi Mockito.verify(clientDetailsManager).convertPublicClientToMember(Mockito.eq("public-client"), Mockito.eq("legal-group")); } + @Test + public void resetPasswordLink() throws Exception { + VerifyEmailUtils verifyEmailUtils = Mockito.mock(VerifyEmailUtils.class); + EncryptionManager encryptionManager= Mockito.mock(EncryptionManager.class); + OrcidSecurityManager orcidSecurityManager = Mockito.mock(OrcidSecurityManager.class); + AdminController adminController = new AdminController(); + EmailManager emailManager = Mockito.mock(EmailManager.class); + LocaleManager localeManager = Mockito.mock(LocaleManager.class); + + + ReflectionTestUtils.setField(adminController, "verifyEmailUtils", verifyEmailUtils); + ReflectionTestUtils.setField(adminController, "encryptionManager", encryptionManager); + ReflectionTestUtils.setField(adminController, "emailManager", emailManager); + ReflectionTestUtils.setField(adminController, "localeManager", localeManager); + ReflectionTestUtils.setField(adminController, "orcidSecurityManager", orcidSecurityManager); + + Mockito.when(orcidSecurityManager.isAdmin()).thenReturn(true); + + Mockito.when(emailManager.emailExists(Mockito.anyString())).thenReturn(true); + Mockito.when(emailManager.emailExists(Mockito.eq("not-found-email1@test.com"))).thenReturn(false); + Mockito.when(emailManager.emailExists(Mockito.eq("not-found-email2@test.com"))).thenReturn(false); + + Mockito.when(localeManager.resolveMessage(Mockito.anyString(), Mockito.any())).thenReturn("That email address is not on our records"); + Mockito.when(verifyEmailUtils.createResetLinkForAdmin(Mockito.anyString(), Mockito.any())).thenReturn("xyz"); + + + + AdminResetPasswordLink adminResetPasswordLink = new AdminResetPasswordLink(); + adminResetPasswordLink.setEmail("not-found-email1@test.com"); + + adminResetPasswordLink = adminController.resetPasswordLink(mockRequest, mockResponse, adminResetPasswordLink); + + assertEquals("That email address is not on our records", adminResetPasswordLink.getError()); + + adminResetPasswordLink = new AdminResetPasswordLink(); + adminResetPasswordLink.setEmail("existent_email@test.com"); + XMLGregorianCalendar date = DateUtils.convertToXMLGregorianCalendarNoTimeZoneNoMillis(new Date()); + Mockito.when(encryptionManager.decryptForExternalUse(Mockito.anyString())).thenReturn("email=existent_email@test.com&issueDate="+ date.toXMLFormat()+ "&h=24"); + adminResetPasswordLink = adminController.resetPasswordLink(mockRequest, mockResponse, adminResetPasswordLink); + assertNotNull(adminResetPasswordLink.getResetLink()); + assertEquals(24,adminResetPasswordLink.getDurationInHours()); + + } + } \ No newline at end of file