diff --git a/agate-core/src/main/java/org/obiba/agate/service/CurrentPasswordInvalidException.java b/agate-core/src/main/java/org/obiba/agate/service/CurrentPasswordInvalidException.java new file mode 100644 index 00000000..eeb6f38c --- /dev/null +++ b/agate-core/src/main/java/org/obiba/agate/service/CurrentPasswordInvalidException.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2019 OBiBa. All rights reserved. + * + * This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.obiba.agate.service; + +public class CurrentPasswordInvalidException extends RuntimeException { + + public CurrentPasswordInvalidException() { + super("The current password is invalid"); + } + +} 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 a2f74f96..77c98d7d 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 @@ -24,6 +24,7 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; + import org.apache.shiro.authz.UnauthenticatedException; import org.apache.shiro.crypto.hash.Sha512Hash; import org.apache.shiro.mgt.SessionsSecurityManager; import org.apache.shiro.realm.Realm; @@ -268,6 +269,17 @@ public void updateUserPassword(@Nonnull User user, @Nonnull String password) { save(userCredentials); } + public void updateUserPassword(@Nonnull User user, @Nonnull String password0, @Nonnull String password) { + if (user == null) throw new BadRequestException("Invalid User"); + UserCredentials userCredentials = findUserCredentials(user.getName()); + if (userCredentials == null) throw new BadRequestException("Invalid User"); + String hashedPassword0 = hashPassword(password0); + if (!userCredentials.getPassword().equals(hashedPassword0)) { + throw new CurrentPasswordInvalidException(); + } + updateUserPassword(user, password); + } + public User createUser(@Nonnull User user, @Nullable String password) { checkNameOrEmail(user.getName()); checkName(user.getFirstName()); @@ -625,6 +637,9 @@ public void updateUserProfile(User user, JSONObject profile) throws JSONExceptio * @throws org.obiba.agate.service.NoSuchUserException */ public User getCurrentUser() { + if (SecurityUtils.getSubject().getPrincipal() == null) { + throw new UnauthenticatedException(); + } String username = SecurityUtils.getSubject().getPrincipal().toString(); User currentUser = findUser(username); if (currentUser == null) throw NoSuchUserException.withName(username); diff --git a/agate-rest/src/main/java/org/obiba/agate/web/rest/CurrentPasswordInvalidExceptionMapper.java b/agate-rest/src/main/java/org/obiba/agate/web/rest/CurrentPasswordInvalidExceptionMapper.java new file mode 100644 index 00000000..df7d08c1 --- /dev/null +++ b/agate-rest/src/main/java/org/obiba/agate/web/rest/CurrentPasswordInvalidExceptionMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019 OBiBa. All rights reserved. + * + * This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.obiba.agate.web.rest; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; +import org.obiba.agate.service.CurrentPasswordInvalidException; +import org.obiba.agate.service.PasswordNotChangedException; +import org.obiba.jersey.exceptionmapper.AbstractErrorDtoExceptionMapper; +import org.obiba.web.model.ErrorDtos; + + +@Provider +public class CurrentPasswordInvalidExceptionMapper extends AbstractErrorDtoExceptionMapper { + + @Override + protected Response.Status getStatus() { + return Response.Status.BAD_REQUEST; + } + + @Override + protected ErrorDtos.ClientErrorDto getErrorDto(CurrentPasswordInvalidException e) { + return ErrorDtos.ClientErrorDto.newBuilder() // + .setCode(getStatus().getStatusCode()) // + .setMessageTemplate("server.error.password.current-invalid") // + .setMessage(e.getMessage()) // + .build(); + } + +} diff --git a/agate-rest/src/main/java/org/obiba/agate/web/rest/user/AbstractUserResource.java b/agate-rest/src/main/java/org/obiba/agate/web/rest/user/AbstractUserResource.java index 85a3f704..0eee4877 100644 --- a/agate-rest/src/main/java/org/obiba/agate/web/rest/user/AbstractUserResource.java +++ b/agate-rest/src/main/java/org/obiba/agate/web/rest/user/AbstractUserResource.java @@ -50,8 +50,9 @@ public Agate.UserDto get() { @PUT @Path("/password") - public Response updatePassword(@FormParam("password") String password) { - userService.updateUserPassword(getUser(), password); + public Response updatePassword(@FormParam("password0") String password0, @FormParam("password") String password) { + // provide current user password and new password + userService.updateUserPassword(getUser(), password0, password); return Response.noContent().build(); } diff --git a/agate-webapp/src/main/resources/_templates/libs/profile-scripts.ftl b/agate-webapp/src/main/resources/_templates/libs/profile-scripts.ftl index dd8786bf..ff353c1f 100644 --- a/agate-webapp/src/main/resources/_templates/libs/profile-scripts.ftl +++ b/agate-webapp/src/main/resources/_templates/libs/profile-scripts.ftl @@ -10,6 +10,7 @@ ]; const errorMessages = { + 'server.error.password.current-invalid': "<@message "server.error.password.current-invalid"/>", 'server.error.password.not-changed': "<@message "server.error.password.not-changed"/>", 'server.error.password.too-weak': "<@message "server.error.password.too-weak"/>" }; diff --git a/agate-webapp/src/main/resources/_templates/profile.ftl b/agate-webapp/src/main/resources/_templates/profile.ftl index 04a32296..a267ee1f 100644 --- a/agate-webapp/src/main/resources/_templates/profile.ftl +++ b/agate-webapp/src/main/resources/_templates/profile.ftl @@ -117,6 +117,10 @@ <@message "password-no-match"/> +
+ <@message "password-not-modified"/> +
+
<@message "update-password-failed"/>
@@ -126,6 +130,14 @@
+
+ "> +
+
+ +
+
+
">
diff --git a/agate-webapp/src/main/resources/i18n/messages_en.properties b/agate-webapp/src/main/resources/i18n/messages_en.properties index 9d08e224..634d55cb 100644 --- a/agate-webapp/src/main/resources/i18n/messages_en.properties +++ b/agate-webapp/src/main/resources/i18n/messages_en.properties @@ -59,6 +59,7 @@ keep-signed-in = Keep me signed in server.error.bad-request = Bad parameters. server.error.bad-captcha = Bad captcha. server.error.email-already-assigned = Email is already taken. +server.error.password.current-invalid = The current password is invalid. server.error.password.not-changed = New password is identical to the current one. server.error.password.too-weak = Password must contain at least one digit, one upper case alphabet, one lower case alphabet, one special character (which includes @#$%^&+=!) and no white space. server.error.unhandledException = Unexpected server error, please try again later. @@ -75,12 +76,14 @@ reset = Reset update-password = Update password reset-key-missing = The password reset key is missing reset-password-failed = Unable to update password, make sure the reset key is valid. +current-password = Current password new-password = New password repeat-new-password = Repeat new password update = Update password-missing = A password is required. password-too-short = Password is too short (minimum is 8 characters). password-no-match = The passwords do not match. +password-not-modified = The new password must be different from the current one. 2fa-caption = Enter 6-digits PIN code validate = Validate invalid-redirect = Invalid redirection diff --git a/agate-webapp/src/main/resources/i18n/messages_fr.properties b/agate-webapp/src/main/resources/i18n/messages_fr.properties index c81ff1d4..758cd36d 100644 --- a/agate-webapp/src/main/resources/i18n/messages_fr.properties +++ b/agate-webapp/src/main/resources/i18n/messages_fr.properties @@ -59,6 +59,7 @@ keep-signed-in = Garder ma connexion server.error.bad-request = Paramètres invalides. server.error.bad-captcha = Le captcha a échoué. server.error.email-already-assigned = Le courriel est déjà enregistré. +server.error.password.current-invalid = Le mot de passe courant est non valide. server.error.password.not-changed = Le nouveau mot de passe est identique au mot de passe courant. server.error.password.too-weak = Le mot de passe doit contenir au moins un chiffre, une lettre minuscule, une lettre majuscule, un caractère spécial (tel que @#$%^&+=!) et aucun espace. server.error.unhandledException = Erreur inattendue, veuillez réessayer plus tard. @@ -75,12 +76,14 @@ reset = Réinitialiser update-password = Mise à jour du mot de passe reset-key-missing = La clé de réinitialisation du mot de passe est manquante. reset-password-failed = La mise à jour du mot de passe a échoué. Veuillez vérifier que la clé de réinitialisation est valide. +current-password = Mot de passe actuel new-password = Nouveau mot de passe repeat-new-password = Répéter le nouveau mot de passe update = Mise à jour password-missing = Un mot de passe est requis. password-too-short = Le mot de passe est trop court (minimum de 8 caractères). password-no-match = Les mots de passe ne correspondent pas. +password-not-modified = Le nouveau mot de passe doit être différent du mot de passe courant. 2fa-caption = Entrer le code NIP à 6 chiffres validate = Valider invalid-redirect = Redirection non valide diff --git a/agate-webapp/src/main/webapp/assets/js/agate.js b/agate-webapp/src/main/webapp/assets/js/agate.js index 72bdd0ed..8b0e9a21 100644 --- a/agate-webapp/src/main/webapp/assets/js/agate.js +++ b/agate-webapp/src/main/webapp/assets/js/agate.js @@ -267,15 +267,27 @@ var agatejs = (function() { var formData = form.serializeArray(); + // current password if (formData[0].value.trim() === '') { onFailure('PasswordMissing'); return; } - if (formData[0].value.length < 8) { + // new password + if (formData[1].value.trim() === '') { + onFailure('PasswordMissing'); + return; + } + // compare current and new password + if (formData[0].value === formData[1].value) { + onFailure('PasswordNotModified'); + return; + } + if (formData[1].value.length < 8) { onFailure('PasswordTooShort'); return; } - if (formData[0].value !== formData[1].value) { + // compare repeated new password + if (formData[1].value !== formData[2].value) { onFailure('PasswordNoMatch'); return; }