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 234a2096d1..823129d59b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -229,19 +229,30 @@ public class UserController extends BaseController { User user = checkNotNull(userService.findUserByEmail(getCurrentUser().getTenantId(), email)); accessControlService.checkPermission(getCurrentUser(), Resource.USER, Operation.READ, user.getId(), user); - String activationLink = getActivationLink(user.getId(), request); - mailService.sendActivationEmail(activationLink, email); + ActivationLink activationLink = getActivationLink(user.getId(), request); + mailService.sendActivationEmail(activationLink.value(), email); } - @ApiOperation(value = "Get the activation link (getActivationLink)", + @ApiOperation(value = "Get activation link (getActivationLink)", notes = "Get the activation link for the user. " + "The base url for activation link is configurable in the general settings of system administrator. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - @RequestMapping(value = "/user/{userId}/activationLink", method = RequestMethod.GET, produces = "text/plain") + @GetMapping(value = "/user/{userId}/activationLink", produces = "text/plain") @ResponseBody public String getActivationLink(@Parameter(description = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId, HttpServletRequest request) throws ThingsboardException { + return getActivationLinkInfo(strUserId, request).value(); + } + + @ApiOperation(value = "Get activation link info (getActivationLinkInfo)", + notes = "Get the activation link info for the user. " + + "The base url for activation link is configurable in the general settings of system administrator. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/user/{userId}/activationLinkInfo") + public ActivationLink getActivationLinkInfo(@Parameter(description = USER_ID_PARAM_DESCRIPTION) + @PathVariable(USER_ID) String strUserId, + HttpServletRequest request) throws ThingsboardException { checkParameter(USER_ID, strUserId); UserId userId = new UserId(toUUID(strUserId)); checkUserId(userId, Operation.READ); @@ -587,17 +598,20 @@ public class UserController extends BaseController { userService.removeMobileSession(user.getTenantId(), mobileToken); } - private String getActivationLink(UserId userId, HttpServletRequest request) throws ThingsboardException { + private ActivationLink getActivationLink(UserId userId, HttpServletRequest request) throws ThingsboardException { TenantId tenantId = getTenantId(); UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, userId); if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) { - if (System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(15) > userCredentials.getActivateTokenExpTime()) { // renew link if less than 15 minutes before expiration + long ttl = userCredentials.getActivationTokenTtl(); + if (ttl < TimeUnit.MINUTES.toMillis(15)) { // renew link if less than 15 minutes before expiration userCredentials = userService.generateUserActivationToken(userCredentials); userCredentials = userService.saveUserCredentials(tenantId, userCredentials); + ttl = userCredentials.getActivationTokenTtl(); log.debug("[{}][{}] Regenerated expired user activation token", tenantId, userId); } String baseUrl = systemSecurityService.getBaseUrl(tenantId, getCurrentUser().getCustomerId(), request); - return String.format(ACTIVATE_URL_PATTERN, baseUrl, userCredentials.getActivateToken()); + String link = String.format(ACTIVATE_URL_PATTERN, baseUrl, userCredentials.getActivateToken()); + return new ActivationLink(link, ttl); } else { throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } @@ -609,4 +623,6 @@ public class UserController extends BaseController { } } + record ActivationLink(String value, long ttlMs) {} + } 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 de60cb61ea..e4010713d8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.SecuritySettings; +import org.thingsboard.server.controller.UserController.ActivationLink; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.user.UserCredentialsDao; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -38,6 +39,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.anyString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -217,8 +219,11 @@ public class AuthControllerTest extends AbstractControllerTest { UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId()); assertThat(userCredentials.getActivateTokenExpTime()).isCloseTo(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttl), Offset.offset(120000L)); - String initialActivationLink = doGet("/api/user/" + user.getId() + "/activationLink", String.class); + String initialActivationLink = getActivationLink(user); String initialActivationToken = StringUtils.substringAfterLast(initialActivationLink, "activateToken="); + ActivationLink activationLinkInfo = getActivationLinkInfo(user); + assertThat(TimeUnit.MILLISECONDS.toHours(activationLinkInfo.ttlMs())).isCloseTo(ttl, within(1L)); + assertThat(activationLinkInfo.value()).isEqualTo(initialActivationLink); // expiring activation token userCredentials.setActivateTokenExpTime(System.currentTimeMillis() - 1); @@ -231,19 +236,24 @@ public class AuthControllerTest extends AbstractControllerTest { .andExpect(jsonPath("$.message", is("Activation token expired"))); // checking that activation link is regenerated when requested - String regeneratedActivationLink = doGet("/api/user/" + user.getId() + "/activationLink", String.class); - assertThat(regeneratedActivationLink).isNotEqualTo(initialActivationLink); + ActivationLink regeneratedActivationLink = getActivationLinkInfo(user); + assertThat(regeneratedActivationLink.value()).isNotEqualTo(initialActivationLink); + assertThat(TimeUnit.MILLISECONDS.toHours(regeneratedActivationLink.ttlMs())).isCloseTo(ttl, within(1L)); // checking link renewal if less than 15 minutes before expiration userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId()); userCredentials.setActivateTokenExpTime(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(30)); userCredentialsDao.save(tenantId, userCredentials); - assertThat(doGet("/api/user/" + user.getId() + "/activationLink", String.class)).isEqualTo(regeneratedActivationLink); + activationLinkInfo = getActivationLinkInfo(user); + assertThat(activationLinkInfo.value()).isEqualTo(regeneratedActivationLink.value()); + assertThat(TimeUnit.MILLISECONDS.toMinutes(activationLinkInfo.ttlMs())).isCloseTo(30, within(1L)); + userCredentials.setActivateTokenExpTime(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10)); userCredentialsDao.save(tenantId, userCredentials); - String newActivationLink = doGet("/api/user/" + user.getId() + "/activationLink", String.class); - assertThat(newActivationLink).isNotEqualTo(regeneratedActivationLink); - String newActivationToken = StringUtils.substringAfterLast(newActivationLink, "activateToken="); + ActivationLink newActivationLink = getActivationLinkInfo(user); + assertThat(newActivationLink.value()).isNotEqualTo(regeneratedActivationLink.value()); + assertThat(TimeUnit.MILLISECONDS.toHours(newActivationLink.ttlMs())).isCloseTo(ttl, within(1L)); + String newActivationToken = StringUtils.substringAfterLast(newActivationLink.value(), "activateToken="); userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId()); assertThat(userCredentials.getActivateTokenExpTime()).isCloseTo(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttl), Offset.offset(120000L)); @@ -265,4 +275,12 @@ public class AuthControllerTest extends AbstractControllerTest { doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk()); } + private String getActivationLink(User user) throws Exception { + return doGet("/api/user/" + user.getId() + "/activationLink", String.class); + } + + private ActivationLink getActivationLinkInfo(User user) throws Exception { + return doGet("/api/user/" + user.getId() + "/activationLinkInfo", ActivationLink.class); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java index 06645feec3..f6e23b9e1a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java @@ -52,7 +52,12 @@ public class UserCredentials extends BaseDataWithAdditionalInfo activateTokenExpTime; + return getActivationTokenTtl() == 0; + } + + @JsonIgnore + public long getActivationTokenTtl() { + return activateTokenExpTime != null ? Math.max(activateTokenExpTime - System.currentTimeMillis(), 0) : 0; } @JsonIgnore