Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/forgot password #421

Merged
merged 69 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
051cb5e
Added forgot password servlet as well as some UI and i18n files changes.
ivanmrsulja Sep 29, 2023
4926978
Moved labels in the correct language file.
ivanmrsulja Sep 29, 2023
65f629c
Implemented localisation and delayed successive requests to prevent S…
ivanmrsulja Oct 2, 2023
5f2bdf3
Refactored logic in order to not force user to change password upon l…
ivanmrsulja Oct 6, 2023
f3e4c90
Added generic app name loading in url.
ivanmrsulja Oct 13, 2023
74528fb
Fixed wait time bug.
ivanmrsulja Oct 13, 2023
fab4304
Link unavaliable after unsuccessfull login bug fix.
ivanmrsulja Oct 24, 2023
62bebcc
Remade message notifications to be inside VIVO context.
ivanmrsulja Oct 27, 2023
8ec6dcb
Moved password change form to separate page. Updated translation file…
ivanmrsulja Oct 30, 2023
d7852d1
Removed contact us section. Cleaned up mitigation code.
ivanmrsulja Oct 31, 2023
618c169
Added proper support page link, fixed information disclosure issue.
ivanmrsulja Oct 31, 2023
81f14d8
Fixed codestyle violations.
ivanmrsulja Nov 1, 2023
054afd5
Added captcha.
ivanmrsulja Nov 1, 2023
767e8fe
Fixed localization bug regarding Serbian labels.
ivanmrsulja Nov 2, 2023
3a96b35
Added feature toggle. Fixed concurrency issues in SPAM mitigation class.
ivanmrsulja Nov 3, 2023
fcbb7bc
Switched to use enabled/disabled instead of boolean value for configu…
ivanmrsulja Nov 7, 2023
459a690
Added example configuration for forgot password functionality feature…
ivanmrsulja Nov 7, 2023
d797bfe
Renamed showFormIfEnabled to showForm. Updated java docs.
ivanmrsulja Nov 7, 2023
6bd8fcb
Fixed bug with property loading. Improved localization.
ivanmrsulja Nov 14, 2023
c16c556
Update home/src/main/resources/rdf/i18n/de_DE/interface-i18n/firsttim…
ivanmrsulja Nov 14, 2023
015d694
Further localization improvement.
ivanmrsulja Nov 14, 2023
84c27f7
Merge conflict resolved.
ivanmrsulja Nov 14, 2023
b14ca0a
Added conditional rendering of contact form link.
ivanmrsulja Nov 15, 2023
4285a15
Update home/src/main/resources/rdf/i18n/es/interface-i18n/firsttime/v…
ivanmrsulja Nov 15, 2023
e10cc2a
Update home/src/main/resources/rdf/i18n/es/interface-i18n/firsttime/v…
ivanmrsulja Nov 15, 2023
490d6ca
Update home/src/main/resources/rdf/i18n/es/interface-i18n/firsttime/v…
ivanmrsulja Nov 15, 2023
5cf2dc9
Update home/src/main/resources/rdf/i18n/es/interface-i18n/firsttime/v…
ivanmrsulja Nov 15, 2023
0d1af5b
Update home/src/main/resources/rdf/i18n/es/interface-i18n/firsttime/v…
ivanmrsulja Nov 15, 2023
2e4ec5e
Update home/src/main/resources/rdf/i18n/es/interface-i18n/firsttime/v…
ivanmrsulja Nov 15, 2023
d153416
Update home/src/main/resources/rdf/i18n/ru_RU/interface-i18n/firsttim…
ivanmrsulja Nov 16, 2023
bba34cf
Improved localization by using arguments. Cleaned up code and added n…
ivanmrsulja Nov 16, 2023
6ef68e3
Merge conflict resolved
ivanmrsulja Nov 16, 2023
df2f2b1
Updated javadocs.
ivanmrsulja Nov 16, 2023
53e24aa
Fixed codestyle violation.
ivanmrsulja Nov 16, 2023
95c7108
Improved feature toggle, as Georgy suggested.
ivanmrsulja Nov 17, 2023
159b6da
Fixed codestyle violation.
ivanmrsulja Nov 17, 2023
8643c5c
Renamed ForgotPassword to ForgotPasswordController.
ivanmrsulja Nov 17, 2023
201245b
Update home/src/main/resources/rdf/i18n/pt_BR/interface-i18n/firsttim…
ivanmrsulja Dec 19, 2023
b1316ea
Improved french localization.
ivanmrsulja Dec 19, 2023
da2f013
Merge branch 'feature/forgot-password' of https://github.com/ivanmrsu…
ivanmrsulja Dec 19, 2023
8c92761
Merge branch 'main' into feature/forgot-password
ivanmrsulja Dec 25, 2023
c2656a4
Added configurable admin notification.
ivanmrsulja Dec 25, 2023
187cb67
Improved pt-BR localization.
ivanmrsulja Jan 9, 2024
ded5461
Merge conflict resolved.
ivanmrsulja Jan 11, 2024
4171adb
Integrated new captcha implementation into forgot password page.
ivanmrsulja Jan 11, 2024
0526d50
Improved localization.
ivanmrsulja Jan 16, 2024
3c3ce68
Addressed renaming and refactoring comments. Improved localization.
ivanmrsulja Jan 26, 2024
6b771c7
Removed spam mitigation, changed default settings to disabled.
ivanmrsulja Feb 9, 2024
19ec481
Aligned example.runtime.properties with java code.
ivanmrsulja Feb 12, 2024
56d54f9
Corrected localization .ttl files indentation.
ivanmrsulja Feb 19, 2024
35ddb2a
Merge conflict resolved.
ivanmrsulja Feb 19, 2024
50200f4
Removed leftover conflict markup. Updated styles.
ivanmrsulja Feb 20, 2024
28bf8c9
Added email length check.
ivanmrsulja Feb 21, 2024
e66f97f
Removed leftover conflict markup in .ftl files.
ivanmrsulja Feb 21, 2024
bbd4ff2
Removed unused labels.
ivanmrsulja Feb 21, 2024
c542224
Refactored localization for contact form link.
ivanmrsulja Feb 21, 2024
4348509
Removed leftover realperson files and theme styles.
ivanmrsulja Feb 22, 2024
55bbe7c
Update home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttim…
ivanmrsulja Feb 23, 2024
0c2e8d9
Added translations for admin email notification.
ivanmrsulja Mar 1, 2024
1b70450
Moved styles to separate css files, refactored selectors.
ivanmrsulja Mar 6, 2024
2831dfa
Code cleanup and slight email check refactoring.
ivanmrsulja Mar 14, 2024
a4f9209
Fixed potential NPE and improved error message localization.
ivanmrsulja Mar 25, 2024
a5ab108
Fixed minor licencing and tag usage errors.
ivanmrsulja Apr 8, 2024
a7bf2ac
Removed useless blank line in css file.
ivanmrsulja Apr 12, 2024
f7de431
Added newline at the end of file.
ivanmrsulja Apr 12, 2024
d2bc008
Updated example properties.
ivanmrsulja Apr 12, 2024
b7a5339
Updated spanish localization.
ivanmrsulja Apr 15, 2024
4644d24
Update home/src/main/resources/rdf/i18n/de_DE/interface-i18n/firsttim…
ivanmrsulja May 9, 2024
44f9bdd
Updated localization.
ivanmrsulja May 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.HashMap;
import java.util.Map;

import edu.cornell.mannlib.vitro.webapp.controller.authenticate.PasswordChangeRequestSpamMitigation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

Expand Down Expand Up @@ -40,6 +41,8 @@ public void resetPassword() {
log.debug("Set password on '" + userAccount.getEmailAddress()
+ "' to '" + newPassword + "'");

PasswordChangeRequestSpamMitigation
.requestSuccessfullyHandledAndUserPasswordUpdated(userAccount.getEmailAddress());
notifyUser();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package edu.cornell.mannlib.vitro.webapp.controller.authenticate;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.mail.Message;
import javax.servlet.ServletContext;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import edu.cornell.mannlib.vitro.webapp.beans.UserAccount;
import edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder;
import edu.cornell.mannlib.vitro.webapp.dao.UserAccountsDao;
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
import edu.cornell.mannlib.vitro.webapp.email.FreemarkerEmailFactory;
import edu.cornell.mannlib.vitro.webapp.email.FreemarkerEmailMessage;
import edu.cornell.mannlib.vitro.webapp.i18n.I18n;
import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle;
import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

@WebServlet(name = "forgot-password", urlPatterns = {"/forgot-password"})
public class ForgotPassword extends VitroHttpServlet {

private static final String RESET_PASSWORD_URL = "/accounts/resetPassword";

private static final int DAYS_TO_USE_PASSWORD_LINK = 5;

private static final Log log = LogFactory.getLog(ForgotPassword.class.getName());


@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
PrintWriter out = setupResponsePrintWriter(response);
log.info("Password reset requested from client: " + request.getRemoteAddr());

VitroRequest vreq = new VitroRequest(request);
UserAccountsDao userAccountsDao = constructUserAccountsDao(vreq);
I18nBundle i18n = I18n.bundle(vreq);

String email = request.getParameter("email");

UserAccount userAccount = getAccountForInternalAuth(email, request);
if (userAccount == null) {
out.println("<h1>" + i18n.text("password_reset_email_non_existing") + "</h1>");
return;
}

PasswordChangeRequestSpamMitigationResponse mitigationResponse =
PasswordChangeRequestSpamMitigation.isPasswordResetRequestable(userAccount);
if (!mitigationResponse.getCanBeRequested()) {
out.println(
"<h1>" + i18n.text("password_reset_too_many_requests") +
mitigationResponse.getNextRequestAvailableAtDate() +
i18n.text("password_reset_too_many_requests_at_time") +
mitigationResponse.getNextRequestAvailableAtTime() + "</h1>");
return;
}

requestPasswordChange(userAccount, userAccountsDao);
notifyUser(userAccount, i18n, vreq);
PasswordChangeRequestSpamMitigation.requestSuccessfullyHandledAndUserIsNotified(userAccount.getEmailAddress());

out.println("<h1>" + i18n.text("password_reset_email_sent") + email + "</h1>");
}

private void notifyUser(UserAccount userAccount, I18nBundle i18n,
VitroRequest vreq) {

Map<String, Object> body = new HashMap<String, Object>();
body.put("userAccount", userAccount);
body.put("passwordLink",
buildResetPasswordLink(userAccount.getEmailAddress(), userAccount.getEmailKey(), vreq));
body.put("siteName", vreq.getAppBean().getApplicationName());
body.put("subject", i18n.text("password_reset_pending_email_subject"));
body.put("textMessage", i18n.text("password_reset_pending_email_plain_text"));
body.put("htmlMessage", i18n.text("password_reset_pending_email_html_text"));

FreemarkerEmailMessage emailMessage = FreemarkerEmailFactory
.createNewMessage(vreq);
emailMessage.addRecipient(Message.RecipientType.TO, userAccount.getEmailAddress());
emailMessage.setBodyMap(body);
emailMessage.processTemplate();
emailMessage.send();
}

private UserAccountsDao constructUserAccountsDao(VitroRequest vreq) {
ServletContext ctx = vreq.getSession().getServletContext();
WebappDaoFactory wdf = ModelAccess.on(ctx).getWebappDaoFactory();
return wdf.getUserAccountsDao();
}

private PrintWriter setupResponsePrintWriter(HttpServletResponse response) throws IOException {
response.setContentType("text/html");
return response.getWriter();
}

private void requestPasswordChange(UserAccount userAccount, UserAccountsDao userAccountsDao) {
userAccount.setPasswordLinkExpires(figureExpirationDate().getTime());
userAccount.generateEmailKey();
userAccountsDao.updateUserAccount(userAccount);
}

private UserAccount getAccountForInternalAuth(String emailAddress, HttpServletRequest request) {
UserAccountsDao userAccountsDao = getUserAccountsDao(request);
if (userAccountsDao == null) {
return null;
}
return userAccountsDao.getUserAccountByEmail(emailAddress);
}

private UserAccountsDao getUserAccountsDao(HttpServletRequest request) {
UserAccountsDao userAccountsDao = getWebappDaoFactory(request)
.getUserAccountsDao();
if (userAccountsDao == null) {
log.error("getUserAccountsDao: no UserAccountsDao");
}

return userAccountsDao;
}

private WebappDaoFactory getWebappDaoFactory(HttpServletRequest request) {
return ModelAccess.on(request).getWebappDaoFactory();
}

private String buildResetPasswordLink(String email, String key, VitroRequest vreq) {
try {
String relativeUrl = UrlBuilder.getUrl(RESET_PASSWORD_URL, "user", email, "key", key);

URL context = new URL(vreq.getRequestURL().toString());
URL url = new URL(context, relativeUrl);
return url.toExternalForm();
} catch (MalformedURLException e) {
return "error_creating_password_link";
}
}

private Date figureExpirationDate() {
Calendar c = Calendar.getInstance();
c.add(Calendar.DATE, DAYS_TO_USE_PASSWORD_LINK);
return c.getTime();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package edu.cornell.mannlib.vitro.webapp.controller.authenticate;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

import edu.cornell.mannlib.vitro.webapp.beans.UserAccount;

public class PasswordChangeRequestSpamMitigation {

private static final Map<String, LocalDateTime> requestHistory = new HashMap<>();

private static final Map<String, Integer> requestFrequency = new HashMap<>();

private static final long INTERVAL_INCREASE_MINUTES = 1;

private static boolean initializeHistoryRequestDataIfNotExists(String emailAddress) {
if (requestHistory.containsKey(emailAddress)) {
return false;
}

requestHistory.put(emailAddress, LocalDateTime.now());
requestFrequency.put(emailAddress, 0);
return true;
}

public static PasswordChangeRequestSpamMitigationResponse isPasswordResetRequestable(UserAccount userAccount) {
boolean justInitialised = initializeHistoryRequestDataIfNotExists(userAccount.getEmailAddress());

Integer numberOfSuccessiveRequests = requestFrequency.get(userAccount.getEmailAddress());
LocalDateTime momentOfFirstRequest = requestHistory.get(userAccount.getEmailAddress());
LocalDateTime nextRequestAvailableAt =
momentOfFirstRequest.plusMinutes(numberOfSuccessiveRequests * INTERVAL_INCREASE_MINUTES);

if (nextRequestAvailableAt.isAfter(LocalDateTime.now())) {
String[] dateTimeTokens = nextRequestAvailableAt.toString().split("T");
String dateString = dateTimeTokens[0];
String timeString = dateTimeTokens[1].split("\\.")[0];
return new PasswordChangeRequestSpamMitigationResponse(false, dateString, timeString);
}

if (numberOfSuccessiveRequests > 0) {
requestHistory.put(userAccount.getEmailAddress(), LocalDateTime.now());
}

return new PasswordChangeRequestSpamMitigationResponse(true);
}

public static void requestSuccessfullyHandledAndUserIsNotified(String email) {
ivanmrsulja marked this conversation as resolved.
Show resolved Hide resolved
requestFrequency.computeIfPresent(email, (key, value) -> ++value);
}

public static void requestSuccessfullyHandledAndUserPasswordUpdated(String email) {
ivanmrsulja marked this conversation as resolved.
Show resolved Hide resolved
requestHistory.remove(email);
requestFrequency.remove(email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package edu.cornell.mannlib.vitro.webapp.controller.authenticate;

public class PasswordChangeRequestSpamMitigationResponse {

private Boolean canBeRequested;

private String nextRequestAvailableAtDate;

private String nextRequestAvailableAtTime;


public PasswordChangeRequestSpamMitigationResponse(Boolean canBeRequested, String nextRequestAvailableAtDate,
String nextRequestAvailableAtTime) {
this.canBeRequested = canBeRequested;
this.nextRequestAvailableAtDate = nextRequestAvailableAtDate;
this.nextRequestAvailableAtTime = nextRequestAvailableAtTime;
}

public PasswordChangeRequestSpamMitigationResponse(Boolean canBeRequested) {
this.canBeRequested = canBeRequested;
}

public Boolean getCanBeRequested() {
return canBeRequested;
}

public void setCanBeRequested(Boolean canBeRequested) {
this.canBeRequested = canBeRequested;
}

public String getNextRequestAvailableAtDate() {
return nextRequestAvailableAtDate;
}

public void setNextRequestAvailableAtDate(String nextRequestAvailableAtDate) {
this.nextRequestAvailableAtDate = nextRequestAvailableAtDate;
}

public String getNextRequestAvailableAtTime() {
return nextRequestAvailableAtTime;
}

public void setNextRequestAvailableAtTime(String nextRequestAvailableAtTime) {
this.nextRequestAvailableAtTime = nextRequestAvailableAtTime;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public String toString() {
private static enum TemplateVariable {
LOGIN_NAME("loginName"),
FORM_ACTION("formAction"),
FORGOT_PASSWORD("forgotPassword"),
INFO_MESSAGE("infoMessage"),
ERROR_MESSAGE("errorMessage"),
EXTERNAL_AUTH_NAME("externalAuthName"),
Expand Down Expand Up @@ -134,6 +135,7 @@ private WidgetTemplateValues showLoginScreen(HttpServletRequest request, String
WidgetTemplateValues values = new WidgetTemplateValues(Macro.LOGIN.toString());
values.put(TemplateVariable.FORM_ACTION.toString(), getAuthenticateUrl(request));
values.put(TemplateVariable.LOGIN_NAME.toString(), bean.getUsername());
values.put(TemplateVariable.FORGOT_PASSWORD.toString(), getForgotPasswordUrl(request));

boolean showExternalAuth = StringUtils.isNotBlank(
ConfigurationProperties.getBean(request).getProperty(
Expand Down Expand Up @@ -232,4 +234,10 @@ private String getCancelUrl(HttpServletRequest request) {
return contextPath + "/authenticate" + urlParams;
}

/** What's the password recovery URL for this servlet? */
private String getForgotPasswordUrl(HttpServletRequest request) {
String contextPath = request.getContextPath();
return contextPath + "/forgot-password";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,46 @@ uil-data:reset_password_note.Vitro
uil:hasKey "reset_password_note" ;
uil:hasPackage "Vitro-languages" .

uil-data:password_reset_email_sent.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
rdfs:label "E-Mail zur Passwortwiederherstellung gesendet an "@de-DE ;
uil:hasApp "Vitro" ;
ivanmrsulja marked this conversation as resolved.
Show resolved Hide resolved
uil:hasKey "password_reset_email_sent" ;
uil:hasPackage "Vitro-languages" .

uil-data:password_reset_email_non_existing.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
rdfs:label "Die von Ihnen angegebene E-Mail-Adresse ist keinem Benutzerkonto zugeordnet."@de-DE ;
uil:hasApp "Vitro" ;
uil:hasKey "password_reset_email_non_existing" ;
uil:hasPackage "Vitro-languages" .

uil-data:password_reset_too_many_requests.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
rdfs:label "Die nächste Anfrage können Sie am "@de-DE ;
uil:hasApp "Vitro" ;
uil:hasKey "password_reset_too_many_requests" ;
uil:hasPackage "Vitro-languages" .

uil-data:password_reset_too_many_requests_at_time.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
rdfs:label " um "@de-DE ;
uil:hasApp "Vitro" ;
uil:hasKey "password_reset_too_many_requests_at_time" ;
uil:hasPackage "Vitro-languages" .

uil-data:password_reset_label.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
rdfs:label "I forgot my password"@de-DE ;
uil:hasApp "Vitro" ;
uil:hasKey "password_reset_label" ;
uil:hasPackage "Vitro-languages" .

uil-data:startup_status.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,46 @@ uil-data:reset_password_note.Vitro
uil:hasKey "reset_password_note" ;
uil:hasPackage "Vitro-languages" .

uil-data:password_reset_email_sent.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
rdfs:label "Password recovery email sent to "@en-CA ;
uil:hasApp "Vitro" ;
uil:hasKey "password_reset_email_sent" ;
uil:hasPackage "Vitro-languages" .

uil-data:password_reset_email_non_existing.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
rdfs:label "Email you have provided is not associated with any user account."@en-CA ;
uil:hasApp "Vitro" ;
uil:hasKey "password_reset_email_non_existing" ;
uil:hasPackage "Vitro-languages" .

uil-data:password_reset_too_many_requests.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
rdfs:label "You will be able to send the next request on "@en-CA ;
uil:hasApp "Vitro" ;
uil:hasKey "password_reset_too_many_requests" ;
uil:hasPackage "Vitro-languages" .

uil-data:password_reset_too_many_requests_at_time.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
rdfs:label " at "@en-CA ;
uil:hasApp "Vitro" ;
uil:hasKey "password_reset_too_many_requests_at_time" ;
uil:hasPackage "Vitro-languages" .

uil-data:password_reset_label.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
rdfs:label "I forgot my password"@en-CA ;
uil:hasApp "Vitro" ;
uil:hasKey "password_reset_label" ;
uil:hasPackage "Vitro-languages" .

uil-data:startup_status.Vitro
rdf:type owl:NamedIndividual ;
rdf:type uil:UILabel ;
Expand Down
Loading
Loading