diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index e81d9945d1..c0230dc33c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -254,7 +254,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Get password reset link (getPasswordResetLink)", notes = "Generate and return a password reset link for the specified user. " + - "Issues a fresh reset token, invalidating any previously issued reset token for this user. " + + "Reuses the currently valid reset token, regenerating it only when it is about to expire. " + "Available only for users that are already activated and whose account is enabled. " + "The base url for the link is configurable in the general settings of system administrator. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @@ -268,7 +268,7 @@ public class UserController extends BaseController { @ApiOperation(value = "Get password reset link info (getPasswordResetLinkInfo)", notes = "Generate and return a password reset link info for the specified user. " + - "Issues a fresh reset token, invalidating any previously issued reset token for this user. " + + "Reuses the currently valid reset token, regenerating it only when it is about to expire. " + "Available only for users that are already activated and whose account is enabled. " + "The base url for the link is configurable in the general settings of system administrator. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @@ -280,7 +280,7 @@ public class UserController extends BaseController { UserId userId = new UserId(toUUID(strUserId)); checkUserId(userId, Operation.WRITE_CREDENTIALS); SecurityUser securityUser = getCurrentUser(); - return tbUserService.getPasswordResetLink(securityUser.getTenantId(), securityUser.getCustomerId(), userId, request); + return tbUserService.getPasswordResetLink(securityUser.getTenantId(), securityUser.getCustomerId(), userId, request, securityUser); } @ApiOperation(value = "Delete User (deleteUser)", diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java index 1b67a97322..d6b9306377 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java @@ -99,7 +99,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse } @Override - public UserPasswordResetLink getPasswordResetLink(TenantId tenantId, CustomerId customerId, UserId userId, HttpServletRequest request) throws ThingsboardException { + public UserPasswordResetLink getPasswordResetLink(TenantId tenantId, CustomerId customerId, UserId userId, HttpServletRequest request, User responsibleUser) throws ThingsboardException { UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, userId); if (!userCredentials.isEnabled()) { if (userCredentials.getActivateToken() != null) { @@ -107,10 +107,12 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse } throw new ThingsboardException("User account is disabled!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } - userCredentials = userService.generatePasswordResetToken(userCredentials); - userCredentials = userService.saveUserCredentials(tenantId, userCredentials); + userCredentials = userService.checkUserPasswordResetToken(tenantId, userCredentials); String baseUrl = systemSecurityService.getBaseUrl(tenantId, customerId, request); String link = baseUrl + "/api/noauth/resetPassword?resetToken=" + userCredentials.getResetToken(); + User user = userService.findUserById(tenantId, userId); + logEntityActionService.logEntityAction(tenantId, userId, user, user.getCustomerId(), + ActionType.CREDENTIALS_READ, responsibleUser, userId.toString()); return new UserPasswordResetLink(link, userCredentials.getResetTokenTtl()); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java index 442ce0b842..0bf23fb55e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java @@ -32,6 +32,6 @@ public interface TbUserService { UserActivationLink getActivationLink(TenantId tenantId, CustomerId customerId, UserId userId, HttpServletRequest request) throws ThingsboardException; - UserPasswordResetLink getPasswordResetLink(TenantId tenantId, CustomerId customerId, UserId userId, HttpServletRequest request) throws ThingsboardException; + UserPasswordResetLink getPasswordResetLink(TenantId tenantId, CustomerId customerId, UserId userId, HttpServletRequest request, User responsibleUser) throws ThingsboardException; } 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 4b509d6cc2..53199b9e01 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java @@ -324,25 +324,26 @@ public class AuthControllerTest extends AbstractControllerTest { } @Test - public void testGetPasswordResetLinkRegeneratesToken() throws Exception { + public void testGetPasswordResetLinkReusesValidToken() throws Exception { loginTenantAdmin(); User user = new User(); user.setAuthority(Authority.TENANT_ADMIN); - user.setEmail("tenant-admin-reset-regen@thingsboard.org"); + user.setEmail("tenant-admin-reset-reuse@thingsboard.org"); User savedUser = createUserAndLogin(user, "initialPassword1"); loginTenantAdmin(); + // repeated calls are idempotent while the token is still valid — no churn for prefetch/double-click UserPasswordResetLink first = getPasswordResetLinkInfo(savedUser); UserPasswordResetLink second = getPasswordResetLinkInfo(savedUser); - assertThat(second.value()).isNotEqualTo(first.value()); + assertThat(second.value()).isEqualTo(first.value()); - // first token must now be invalid — second one wins - String firstToken = StringUtils.substringAfterLast(first.value(), "resetToken="); + // the reused link still works end-to-end + String resetToken = StringUtils.substringAfterLast(first.value(), "resetToken="); + Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString()); doPost("/api/noauth/resetPassword", JacksonUtil.newObjectNode() - .put("resetToken", firstToken) + .put("resetToken", resetToken) .put("password", "newPassword1")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message", is("Invalid reset token!"))); + .andExpect(status().isOk()); } @Test diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index 60a54037ab..91ea743f65 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -65,6 +65,8 @@ public interface UserService extends EntityDaoService { UserCredentials checkUserActivationToken(TenantId tenantId, UserCredentials userCredentials); + UserCredentials checkUserPasswordResetToken(TenantId tenantId, UserCredentials userCredentials); + UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); void deleteUser(TenantId tenantId, User user); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index a5fb56e7eb..e82d24de20 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -304,6 +304,16 @@ public class UserServiceImpl extends AbstractCachedEntityService