diff --git a/agate-core/pom.xml b/agate-core/pom.xml
index 9f469d5ce..cd2b3b757 100644
--- a/agate-core/pom.xml
+++ b/agate-core/pom.xml
@@ -159,6 +159,10 @@
postgresql
+
+ org.owasp.esapi
+ esapi
+
dev.samstevens.totp
totp
diff --git a/agate-core/src/main/java/org/obiba/agate/domain/Configuration.java b/agate-core/src/main/java/org/obiba/agate/domain/Configuration.java
index 180857197..4a3f06076 100644
--- a/agate-core/src/main/java/org/obiba/agate/domain/Configuration.java
+++ b/agate-core/src/main/java/org/obiba/agate/domain/Configuration.java
@@ -14,7 +14,6 @@
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
-import org.hibernate.validator.constraints.NotBlank;
import org.obiba.mongodb.domain.AbstractAuditableDocument;
import org.obiba.runtime.Version;
import org.springframework.data.annotation.Transient;
@@ -28,8 +27,6 @@
@Document
public class Configuration extends AbstractAuditableDocument {
- private static final long serialVersionUID = -9020464712632680519L;
-
public static final String DEFAULT_NAME = "Agate";
public static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
@@ -40,7 +37,6 @@ public class Configuration extends AbstractAuditableDocument {
public static final int DEFAULT_INACTIVE_TIMEOUT = 365 * 24; // 1 year (in hours)
- @NotBlank
private String name = DEFAULT_NAME;
private String domain;
diff --git a/agate-core/src/main/java/org/obiba/agate/domain/User.java b/agate-core/src/main/java/org/obiba/agate/domain/User.java
index 29358ea6a..da62dd795 100644
--- a/agate-core/src/main/java/org/obiba/agate/domain/User.java
+++ b/agate-core/src/main/java/org/obiba/agate/domain/User.java
@@ -14,7 +14,6 @@
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
-import org.hibernate.validator.constraints.Email;
import org.joda.time.DateTime;
import org.obiba.agate.security.Roles;
import org.obiba.mongodb.domain.AbstractAuditableDocument;
@@ -41,7 +40,6 @@ public class User extends AbstractAuditableDocument {
private String lastName;
- @Email
@Indexed(unique = true)
private String email;
diff --git a/agate-webapp/src/main/java/org/obiba/agate/web/controller/domain/UserProfile.java b/agate-core/src/main/java/org/obiba/agate/domain/UserProfile.java
similarity index 91%
rename from agate-webapp/src/main/java/org/obiba/agate/web/controller/domain/UserProfile.java
rename to agate-core/src/main/java/org/obiba/agate/domain/UserProfile.java
index 0e10d7c04..30b35e66a 100644
--- a/agate-webapp/src/main/java/org/obiba/agate/web/controller/domain/UserProfile.java
+++ b/agate-core/src/main/java/org/obiba/agate/domain/UserProfile.java
@@ -1,6 +1,5 @@
-package org.obiba.agate.web.controller.domain;
+package org.obiba.agate.domain;
-import org.obiba.agate.domain.User;
import org.owasp.esapi.ESAPI;
import java.util.Map;
@@ -51,7 +50,7 @@ public String getDisplayName() {
}
public String getPreferredLanguage() {
- return user.getPreferredLanguage();
+ return ESAPI.encoder().encodeForHTML(user.getPreferredLanguage());
}
public boolean getOtpEnabled() {
diff --git a/agate-core/src/main/java/org/obiba/agate/service/UserService.java b/agate-core/src/main/java/org/obiba/agate/service/UserService.java
index e06d39160..130763d32 100644
--- a/agate-core/src/main/java/org/obiba/agate/service/UserService.java
+++ b/agate-core/src/main/java/org/obiba/agate/service/UserService.java
@@ -38,6 +38,7 @@
import org.obiba.agate.repository.UserCredentialsRepository;
import org.obiba.agate.repository.UserRepository;
import org.obiba.agate.service.support.MessageResolverMethod;
+ import org.obiba.agate.validator.NameValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
@@ -78,6 +79,7 @@ public class UserService {
+ "(?=.*[@#$%^&+=!])" // a special character that must occur at least once
+ "(?=\\S+$).{" + PWD_MINIMUM_LENGTH + "," + PWD_MAXIMUM_LENGTH + "}$");
+
@Value("${login.otpTimeout:300}")
private int otpTimeout;
@@ -266,6 +268,10 @@ public void updateUserPassword(@Nonnull User user, @Nonnull String password) {
}
public User createUser(@Nonnull User user, @Nullable String password) {
+ checkName(user.getName());
+ checkName(user.getFirstName());
+ checkName(user.getLastName());
+
if (Strings.isNullOrEmpty(password)) {
if (user.getRealm() == null) user.setRealm(AgateRealm.AGATE_USER_REALM.getName());
else {
@@ -455,7 +461,7 @@ public void sendPendingEmail(UserJoinedEvent userJoinedEvent) {
String organization = configurationService.getConfiguration().getName();
Map context = Maps.newHashMap();
- context.put("user", user);
+ context.put("user", new UserProfile(user));
administrators.forEach(u -> sendEmail(u, "[" + organization + "] " + env.getProperty("registration.pendingForReviewSubject"),
"pendingForReviewEmail", context));
@@ -479,7 +485,7 @@ private void sendEmail(User user, String subject, String templateName, Map ctx = context == null ? Maps.newHashMap() : Maps.newHashMap(context);
if (!ctx.containsKey("user"))
- ctx.put("user", user);
+ ctx.put("user", new UserProfile(user));
ctx.put("organization", configurationService.getConfiguration().getName());
// get user's realm and find if there is a specific agate base url for this realm
Optional realmConfig = getRealmConfigs(user).stream().findFirst();
@@ -711,4 +717,10 @@ private List getRealmConfigs(User user) {
return realmConfigRepository.findAll().stream().filter(realmConfig -> user.getRealm().equals(realmConfig.getName())).collect(Collectors.toList());
}
+ private void checkName(String name) {
+ if (!NameValidator.isValid(name)) {
+ throw new BadRequestException("Name contains invalid characters");
+ }
+ }
+
}
diff --git a/agate-core/src/main/java/org/obiba/agate/validator/EmailValidator.java b/agate-core/src/main/java/org/obiba/agate/validator/EmailValidator.java
new file mode 100644
index 000000000..82be7043d
--- /dev/null
+++ b/agate-core/src/main/java/org/obiba/agate/validator/EmailValidator.java
@@ -0,0 +1,40 @@
+package org.obiba.agate.validator;
+
+import jakarta.mail.internet.InternetAddress;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+
+public final class EmailValidator {
+
+ public static boolean isValid(String email) {
+ if (email == null || email.trim().isEmpty()) {
+ return false;
+ }
+
+ // Check length constraints
+ if (email.length() > 254) {
+ return false; // Email too long
+ }
+
+ String[] parts = email.split("@");
+ if (parts[0].length() > 64) {
+ return false; // Local part too long
+ }
+
+ // Check for common issues
+ if (email.contains("..") || email.startsWith(".") || email.endsWith(".")) {
+ return false; // Invalid dot placement
+ }
+
+ try {
+ InternetAddress emailAddr = new InternetAddress(email);
+ emailAddr.validate();
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/agate-core/src/main/java/org/obiba/agate/validator/NameValidator.java b/agate-core/src/main/java/org/obiba/agate/validator/NameValidator.java
new file mode 100644
index 000000000..7919db97e
--- /dev/null
+++ b/agate-core/src/main/java/org/obiba/agate/validator/NameValidator.java
@@ -0,0 +1,18 @@
+package org.obiba.agate.validator;
+
+import java.util.regex.Pattern;
+
+public final class NameValidator {
+
+ // International pattern supporting accents and other characters
+ private static final Pattern INTERNATIONAL_NAME_PATTERN = Pattern.compile(
+ "^[\\p{L}][\\p{L}\\s\\-']{1,49}$");
+
+ public static boolean isValid(String name) {
+ if (name == null || name.isEmpty()) {
+ return true;
+ }
+
+ return INTERNATIONAL_NAME_PATTERN.matcher(name).matches();
+ }
+}
diff --git a/agate-rest/src/main/java/org/obiba/agate/web/rest/user/UsersPublicResource.java b/agate-rest/src/main/java/org/obiba/agate/web/rest/user/UsersPublicResource.java
index 842f6f4e5..5ded25ae9 100644
--- a/agate-rest/src/main/java/org/obiba/agate/web/rest/user/UsersPublicResource.java
+++ b/agate-rest/src/main/java/org/obiba/agate/web/rest/user/UsersPublicResource.java
@@ -19,13 +19,12 @@
import com.google.common.collect.Sets;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
-import org.bson.types.ObjectId;
-import org.hibernate.validator.internal.constraintvalidators.hv.EmailValidator;
import org.joda.time.DateTime;
import org.obiba.agate.domain.*;
import org.obiba.agate.security.AgateUserRealm;
import org.obiba.agate.security.Roles;
import org.obiba.agate.service.*;
+import org.obiba.agate.validator.EmailValidator;
import org.obiba.agate.web.rest.config.JerseyConfiguration;
import org.obiba.agate.web.rest.security.InvalidApplicationKeyException;
import org.obiba.shiro.authc.HttpAuthorizationToken;
@@ -185,7 +184,7 @@ public Response create(@Context HttpServletRequest request, MultivaluedMap