From 82f89de36517ade60ca9db47937ac3b4d28d5ac1 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 5 Dec 2023 17:08:23 +0200 Subject: [PATCH 1/4] Added max length password policy. Added boolean value to force users update their not valid password. --- .../ThingsboardErrorResponseHandler.java | 2 +- .../system/DefaultSystemSecurityService.java | 48 +++++++---- .../server/controller/AuthControllerTest.java | 83 +++++++++++++++++++ .../security/model/UserPasswordPolicy.java | 4 + 4 files changed, 118 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index 0cd581b2e5..dfdcf16090 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -178,7 +178,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand private void handleAuthenticationException(AuthenticationException authenticationException, HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.UNAUTHORIZED.value()); if (authenticationException instanceof BadCredentialsException || authenticationException instanceof UsernameNotFoundException) { - JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(authenticationException.getMessage().isEmpty() ? "Invalid username or password" : authenticationException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof DisabledException) { JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof LockedException) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 2c2956b288..9fe24eaf01 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -107,6 +107,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { securitySettings = new SecuritySettings(); securitySettings.setPasswordPolicy(new UserPasswordPolicy()); securitySettings.getPasswordPolicy().setMinimumLength(6); + securitySettings.getPasswordPolicy().setMaximumLength(72); } return securitySettings; } @@ -131,9 +132,18 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @Override public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException { + SecuritySettings securitySettings = self.getSecuritySettings(tenantId); + UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy(); + + if (passwordPolicy.getForceUserToResetPasswordIfNotValid()) { + try { + validatePasswordByPolicy(password, passwordPolicy); + } catch (DataValidationException e) { + throw new BadCredentialsException("Password does not pass validation. Please try again or reset password to valid one."); + } + } if (!encoder.matches(password, userCredentials.getPassword())) { int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId()); - SecuritySettings securitySettings = self.getSecuritySettings(tenantId); if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) { if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) { lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts()); @@ -149,7 +159,6 @@ public class DefaultSystemSecurityService implements SystemSecurityService { userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId()); - SecuritySettings securitySettings = self.getSecuritySettings(tenantId); if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) { if ((userCredentials.getCreatedTime() + TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) @@ -199,8 +208,26 @@ public class DefaultSystemSecurityService implements SystemSecurityService { SecuritySettings securitySettings = self.getSecuritySettings(tenantId); UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy(); + validatePasswordByPolicy(password, passwordPolicy); + + if (userCredentials != null && isPositiveInteger(passwordPolicy.getPasswordReuseFrequencyDays())) { + long passwordReuseFrequencyTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(passwordPolicy.getPasswordReuseFrequencyDays()); + JsonNode additionalInfo = userCredentials.getAdditionalInfo(); + if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) { + JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY); + Map userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {}); + for (Map.Entry entry : userPasswordHistoryMap.entrySet()) { + if (encoder.matches(password, entry.getValue()) && Long.parseLong(entry.getKey()) > passwordReuseFrequencyTs) { + throw new DataValidationException("Password was already used for the last " + passwordPolicy.getPasswordReuseFrequencyDays() + " days"); + } + } + } + } + } + + private void validatePasswordByPolicy(String password, UserPasswordPolicy passwordPolicy) { List passwordRules = new ArrayList<>(); - passwordRules.add(new LengthRule(passwordPolicy.getMinimumLength(), Integer.MAX_VALUE)); + passwordRules.add(new LengthRule(passwordPolicy.getMinimumLength(), passwordPolicy.getMaximumLength())); if (isPositiveInteger(passwordPolicy.getMinimumUppercaseLetters())) { passwordRules.add(new CharacterRule(EnglishCharacterData.UpperCase, passwordPolicy.getMinimumUppercaseLetters())); } @@ -223,21 +250,6 @@ public class DefaultSystemSecurityService implements SystemSecurityService { String message = String.join("\n", validator.getMessages(result)); throw new DataValidationException(message); } - - if (userCredentials != null && isPositiveInteger(passwordPolicy.getPasswordReuseFrequencyDays())) { - long passwordReuseFrequencyTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(passwordPolicy.getPasswordReuseFrequencyDays()); - JsonNode additionalInfo = userCredentials.getAdditionalInfo(); - if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) { - JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY); - Map userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {}); - for (Map.Entry entry : userPasswordHistoryMap.entrySet()) { - if (encoder.matches(password, entry.getValue()) && Long.parseLong(entry.getKey()) > passwordReuseFrequencyTs) { - throw new DataValidationException("Password was already used for the last " + passwordPolicy.getPasswordReuseFrequencyDays() + " days"); - } - } - - } - } } @Override diff --git a/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java index b833e45d34..099f6d8817 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java @@ -15,19 +15,41 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.After; import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.http.HttpHeaders; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.service.security.auth.rest.LoginRequest; +import org.thingsboard.server.service.security.model.ChangePasswordRequest; import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.anyString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest public class AuthControllerTest extends AbstractControllerTest { + @After + public void tearDown() throws Exception { + loginSysAdmin(); + SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class); + + securitySettings.getPasswordPolicy().setMaximumLength(72); + securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(false); + + doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk()); + } + @Test public void testGetUser() throws Exception { @@ -84,4 +106,65 @@ public class AuthControllerTest extends AbstractControllerTest { .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name()))) .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL))); } + + @Test + public void testShouldNotUpdatePasswordWithValueLongerThanDefaultLimit() throws Exception { + loginTenantAdmin(); + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); + changePasswordRequest.setCurrentPassword("tenant"); + changePasswordRequest.setNewPassword(RandomStringUtils.randomAlphanumeric(73)); + doPost("/api/auth/changePassword", changePasswordRequest) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message", is("Password must be no more than 72 characters in length."))); + } + + @Test + public void testShouldNotAuthorizeUserIfHisPasswordBecameTooLong() throws Exception { + loginTenantAdmin(); + + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); + changePasswordRequest.setCurrentPassword("tenant"); + String newPassword = RandomStringUtils.randomAlphanumeric(16); + changePasswordRequest.setNewPassword(newPassword); + doPost("/api/auth/changePassword", changePasswordRequest) + .andExpect(status().isOk()); + loginUser(TENANT_ADMIN_EMAIL, newPassword); + + loginSysAdmin(); + SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class); + securitySettings.getPasswordPolicy().setMaximumLength(15); + securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(true); + doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk()); + + //try to login with user password that is not valid after security settings was updated + doPost("/api/auth/login", new LoginRequest(TENANT_ADMIN_EMAIL, newPassword)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.message", is("Password does not pass validation. Please try again or reset password to valid one."))); + } + + @Test + public void testShouldNotResetPasswordToTooLongValue() throws Exception { + loginTenantAdmin(); + + JsonNode resetPasswordByEmailRequest = JacksonUtil.newObjectNode() + .put("email", TENANT_ADMIN_EMAIL); + + doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest) + .andExpect(status().isOk()); + Thread.sleep(1000); + doGet("/api/noauth/resetPassword?resetToken={resetToken}", this.currentResetPasswordToken) + .andExpect(status().isSeeOther()) + .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + this.currentResetPasswordToken)); + + String newPassword = RandomStringUtils.randomAlphanumeric(73); + JsonNode resetPasswordRequest = JacksonUtil.newObjectNode() + .put("resetToken", this.currentResetPasswordToken) + .put("password", newPassword); + + Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString()); + doPost("/api/noauth/resetPassword", resetPasswordRequest) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message", + is("Password must be no more than 72 characters in length."))); + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java index 881af89e27..fd067287b5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java @@ -27,6 +27,8 @@ public class UserPasswordPolicy implements Serializable { @ApiModelProperty(position = 1, value = "Minimum number of symbols in the password." ) private Integer minimumLength; + @ApiModelProperty(position = 1, value = "Maximum number of symbols in the password." ) + private Integer maximumLength; @ApiModelProperty(position = 1, value = "Minimum number of uppercase letters in the password." ) private Integer minimumUppercaseLetters; @ApiModelProperty(position = 1, value = "Minimum number of lowercase letters in the password." ) @@ -37,6 +39,8 @@ public class UserPasswordPolicy implements Serializable { private Integer minimumSpecialCharacters; @ApiModelProperty(position = 1, value = "Allow whitespaces") private Boolean allowWhitespaces = true; + @ApiModelProperty(position = 1, value = "Force user to update password if existing one does not pass validation") + private Boolean forceUserToResetPasswordIfNotValid = false; @ApiModelProperty(position = 1, value = "Password expiration period (days). Force expiration of the password." ) private Integer passwordExpirationPeriodDays; From 8ba9c7d944c9e5b0e61a4dad393e40057968e5e2 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 5 Dec 2023 18:45:33 +0200 Subject: [PATCH 2/4] fixed potential NPE exceptions --- .../exception/ThingsboardErrorResponseHandler.java | 3 ++- .../system/DefaultSystemSecurityService.java | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index dfdcf16090..e05fbe9e43 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -178,7 +178,8 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand private void handleAuthenticationException(AuthenticationException authenticationException, HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.UNAUTHORIZED.value()); if (authenticationException instanceof BadCredentialsException || authenticationException instanceof UsernameNotFoundException) { - JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(authenticationException.getMessage().isEmpty() ? "Invalid username or password" : authenticationException.getMessage(), ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + String message = (authenticationException.getMessage() == null || authenticationException.getMessage().isEmpty()) ? "Invalid username or password" : authenticationException.getMessage(); + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(message, ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof DisabledException) { JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof LockedException) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 9fe24eaf01..3e2f6e9018 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -135,11 +135,11 @@ public class DefaultSystemSecurityService implements SystemSecurityService { SecuritySettings securitySettings = self.getSecuritySettings(tenantId); UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy(); - if (passwordPolicy.getForceUserToResetPasswordIfNotValid()) { + if (Boolean.TRUE.equals(passwordPolicy.getForceUserToResetPasswordIfNotValid())) { try { validatePasswordByPolicy(password, passwordPolicy); } catch (DataValidationException e) { - throw new BadCredentialsException("Password does not pass validation. Please try again or reset password to valid one."); + throw new BadCredentialsException("The entered password violates our policies. If this is your real password, please reset it."); } } if (!encoder.matches(password, userCredentials.getPassword())) { @@ -150,7 +150,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { throw new LockedException("Authentication Failed. Username was locked due to security policy."); } } - throw new BadCredentialsException("Authentication Failed. Username or Password not valid."); + throw new BadCredentialsException("Invalid username or Password."); } if (!userCredentials.isEnabled()) { @@ -227,7 +227,12 @@ public class DefaultSystemSecurityService implements SystemSecurityService { private void validatePasswordByPolicy(String password, UserPasswordPolicy passwordPolicy) { List passwordRules = new ArrayList<>(); - passwordRules.add(new LengthRule(passwordPolicy.getMinimumLength(), passwordPolicy.getMaximumLength())); + + Integer maximumLength = passwordPolicy.getMaximumLength(); + Integer minLengthBound = passwordPolicy.getMinimumLength(); + int maxLengthBound = (maximumLength != null && maximumLength > passwordPolicy.getMinimumLength()) ? maximumLength : Integer.MAX_VALUE; + + passwordRules.add(new LengthRule(minLengthBound, maxLengthBound)); if (isPositiveInteger(passwordPolicy.getMinimumUppercaseLetters())) { passwordRules.add(new CharacterRule(EnglishCharacterData.UpperCase, passwordPolicy.getMinimumUppercaseLetters())); } From bdf8c6d3db6d8764b15d3e4f757e39c040f219aa Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 6 Dec 2023 11:13:21 +0200 Subject: [PATCH 3/4] fixed test --- .../org/thingsboard/server/controller/AuthControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java index 099f6d8817..bab102aeca 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java @@ -139,7 +139,7 @@ public class AuthControllerTest extends AbstractControllerTest { //try to login with user password that is not valid after security settings was updated doPost("/api/auth/login", new LoginRequest(TENANT_ADMIN_EMAIL, newPassword)) .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.message", is("Password does not pass validation. Please try again or reset password to valid one."))); + .andExpect(jsonPath("$.message", is("The entered password violates our policies. If this is your real password, please reset it."))); } @Test From 8b6b1dd7d75bacf9744b8e2cedca160b4bc29bbe Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 7 Dec 2023 17:35:49 +0200 Subject: [PATCH 4/4] added new type of error - UserPasswordNotValidException to correctly handle it on UI --- ...ingsboardCredentialsViolationResponse.java | 33 +++++++++++++++++++ .../ThingsboardErrorResponseHandler.java | 7 ++-- .../UserPasswordNotValidException.java | 26 +++++++++++++++ .../system/DefaultSystemSecurityService.java | 8 +++-- .../data/exception/ThingsboardErrorCode.java | 3 +- 5 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsViolationResponse.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordNotValidException.java diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsViolationResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsViolationResponse.java new file mode 100644 index 0000000000..421ca04a34 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsViolationResponse.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.exception; + +import io.swagger.annotations.ApiModel; +import org.springframework.http.HttpStatus; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; + +@ApiModel +public class ThingsboardCredentialsViolationResponse extends ThingsboardErrorResponse { + + protected ThingsboardCredentialsViolationResponse(String message) { + super(message, ThingsboardErrorCode.PASSWORD_VIOLATION, HttpStatus.UNAUTHORIZED); + } + + public static ThingsboardCredentialsViolationResponse of(final String message) { + return new ThingsboardCredentialsViolationResponse(message); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index e05fbe9e43..fbb2f1d8e2 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; +import org.thingsboard.server.service.security.exception.UserPasswordNotValidException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -178,8 +179,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand private void handleAuthenticationException(AuthenticationException authenticationException, HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.UNAUTHORIZED.value()); if (authenticationException instanceof BadCredentialsException || authenticationException instanceof UsernameNotFoundException) { - String message = (authenticationException.getMessage() == null || authenticationException.getMessage().isEmpty()) ? "Invalid username or password" : authenticationException.getMessage(); - JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(message, ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); + JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof DisabledException) { JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } else if (authenticationException instanceof LockedException) { @@ -192,6 +192,9 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand UserPasswordExpiredException expiredException = (UserPasswordExpiredException) authenticationException; String resetToken = expiredException.getResetToken(); JacksonUtil.writeValue(response.getWriter(), ThingsboardCredentialsExpiredResponse.of(expiredException.getMessage(), resetToken)); + } else if (authenticationException instanceof UserPasswordNotValidException) { + UserPasswordNotValidException expiredException = (UserPasswordNotValidException) authenticationException; + JacksonUtil.writeValue(response.getWriter(), ThingsboardCredentialsViolationResponse.of(expiredException.getMessage())); } else { JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordNotValidException.java b/application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordNotValidException.java new file mode 100644 index 0000000000..7cd01be7d5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/exception/UserPasswordNotValidException.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.exception; + +import org.springframework.security.core.AuthenticationException; + +public class UserPasswordNotValidException extends AuthenticationException { + + public UserPasswordNotValidException(String msg) { + super(msg); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 3e2f6e9018..5dacefc924 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -57,6 +57,7 @@ import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserServiceImpl; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; +import org.thingsboard.server.service.security.exception.UserPasswordNotValidException; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.utils.MiscUtils; import ua_parser.Client; @@ -135,11 +136,12 @@ public class DefaultSystemSecurityService implements SystemSecurityService { SecuritySettings securitySettings = self.getSecuritySettings(tenantId); UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy(); - if (Boolean.TRUE.equals(passwordPolicy.getForceUserToResetPasswordIfNotValid())) { + if (!tenantId.isSysTenantId() && Boolean.TRUE.equals(passwordPolicy.getForceUserToResetPasswordIfNotValid())) { try { validatePasswordByPolicy(password, passwordPolicy); } catch (DataValidationException e) { - throw new BadCredentialsException("The entered password violates our policies. If this is your real password, please reset it."); + throw new UserPasswordNotValidException("The entered password violates our policies. If this is your real password, please reset it."); + } } if (!encoder.matches(password, userCredentials.getPassword())) { @@ -150,7 +152,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { throw new LockedException("Authentication Failed. Username was locked due to security policy."); } } - throw new BadCredentialsException("Invalid username or Password."); + throw new BadCredentialsException("Authentication Failed. Username or Password not valid."); } if (!userCredentials.isEnabled()) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java index 00ffd655dc..1ab0e3e634 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java @@ -29,7 +29,8 @@ public enum ThingsboardErrorCode { ITEM_NOT_FOUND(32), TOO_MANY_REQUESTS(33), TOO_MANY_UPDATES(34), - SUBSCRIPTION_VIOLATION(40); + SUBSCRIPTION_VIOLATION(40), + PASSWORD_VIOLATION(45); private int errorCode;