From e1ab81c6fc857fe4bd577120e2fda8c55faf3421 Mon Sep 17 00:00:00 2001 From: oyurov Date: Sun, 16 Oct 2022 17:23:31 +0200 Subject: [PATCH 1/8] Implemented possibility to outdate user jwt token after logout --- .../java/org/thingsboard/server/controller/AuthController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 72be55a135..b3d9cb6634 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -367,6 +367,7 @@ public class AuthController extends BaseController { user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(user.getId())); } catch (Exception e) { throw handleException(e); } From 3d7b829a9994089e91f54f39751490d70bd55c26 Mon Sep 17 00:00:00 2001 From: oyurov Date: Mon, 17 Oct 2022 13:23:12 +0200 Subject: [PATCH 2/8] Refactor tests to support outdate tokens after logout --- .../server/controller/AbstractWebTest.java | 12 ++++++++---- .../server/controller/BaseAlarmControllerTest.java | 4 ++-- .../server/controller/BaseAuthControllerTest.java | 2 ++ .../server/controller/BaseUserControllerTest.java | 8 ++++---- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 3783b746f3..ff079fc731 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -234,7 +234,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { customerUser = createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD); customerUserId = customerUser.getId(); - logout(); + resetTokens(); log.info("Executed web test setup"); } @@ -336,7 +336,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Assert.assertNotNull(savedDifferentCustomer); differentCustomerId = savedDifferentCustomer.getId(); - logout(); + resetTokens(); } protected void deleteDifferentTenant() throws Exception { @@ -350,7 +350,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected User createUserAndLogin(User user, String password) throws Exception { User savedUser = doPost("/api/user", user, User.class); - logout(); + resetTokens(); JsonNode activateRequest = getActivateRequest(password); JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); validateAndSetJwtToken(tokenInfo, user.getEmail()); @@ -413,12 +413,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Assert.assertEquals(username, subject); } - protected void logout() throws Exception { + protected void resetTokens() throws Exception { this.token = null; this.refreshToken = null; this.username = null; } + protected void logout() throws Exception { + doPost("/api/auth/logout"); + } + protected void setJwtToken(MockHttpServletRequestBuilder request) { if (this.token != null) { request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java index 4419e0e425..493b1818d9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java @@ -77,7 +77,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { device.setCustomerId(customerId); customerDevice = doPost("/api/device", device, Device.class); - logout(); + resetTokens(); } @After @@ -423,7 +423,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { testNotifyEntityNeverMsgToEdgeServiceOneTime(alarm, alarm.getId(), tenantId, ActionType.ADDED); - logout(); + resetTokens(); JsonNode publicLoginRequest = JacksonUtil.toJsonNode("{\"publicId\": \"" + publicId + "\"}"); JsonNode tokens = doPost("/api/auth/login/public", publicLoginRequest, JsonNode.class); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java index f64cac0f99..61945e59bb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java @@ -60,6 +60,8 @@ public abstract class BaseAuthControllerTest extends AbstractControllerTest { logout(); doGet("/api/auth/user") .andExpect(status().isUnauthorized()); + + resetTokens(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index 9c01f4323d..2f1dc97f14 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -106,7 +106,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { ActionType.ADDED, ActionType.ADDED, 1, 1, 1); Mockito.reset(tbClusterService, auditLogService); - logout(); + resetTokens(); doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) .andExpect(status().isSeeOther()) .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); @@ -123,7 +123,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name()))) .andExpect(jsonPath("$.email", is(email))); - logout(); + resetTokens(); login(email, "testPassword"); @@ -218,7 +218,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { user.setLastName("Downs"); User savedUser = createUserAndLogin(user, "testPassword1"); - logout(); + resetTokens(); JsonNode resetPasswordByEmailRequest = new ObjectMapper().createObjectNode() .put("email", email); @@ -244,7 +244,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name()))) .andExpect(jsonPath("$.email", is(email))); - logout(); + resetTokens(); login(email, "testPassword2"); doGet("/api/auth/user") From aaa8dbbfddb225f7175ab7272ae70f833063c4af Mon Sep 17 00:00:00 2001 From: oyurov Date: Tue, 18 Oct 2022 14:04:26 +0200 Subject: [PATCH 3/8] Fix test --- .../thingsboard/server/controller/AbstractWebTest.java | 2 +- .../server/controller/BaseAuthControllerTest.java | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index ff079fc731..a3b144f4a9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -420,7 +420,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected void logout() throws Exception { - doPost("/api/auth/logout"); + doPost("/api/auth/logout").andExpect(status().isOk()); } protected void setJwtToken(MockHttpServletRequestBuilder request) { diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java index 61945e59bb..627ce1a02b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.controller; +import org.junit.Test; +import org.thingsboard.server.common.data.security.Authority; + +import java.util.concurrent.TimeUnit; + import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.thingsboard.server.common.data.security.Authority; -import org.junit.Test; - public abstract class BaseAuthControllerTest extends AbstractControllerTest { @Test @@ -57,6 +59,8 @@ public abstract class BaseAuthControllerTest extends AbstractControllerTest { .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name()))) .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL))); + TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed + logout(); doGet("/api/auth/user") .andExpect(status().isUnauthorized()); From d10289253f1bfdb62feabdf3aaee130993ee58bb Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 2 Nov 2022 08:46:58 +0100 Subject: [PATCH 4/8] Refactor --- .../server/controller/AuthController.java | 18 +--- .../server/controller/UserController.java | 9 +- .../security/auth/TokenOutdatingService.java | 54 ++++++---- .../auth/jwt/RefreshTokenRepository.java | 38 ------- .../Oauth2AuthenticationSuccessHandler.java | 11 +- ...RestAwareAuthenticationSuccessHandler.java | 5 +- .../service/security/model/SecurityUser.java | 10 ++ .../security/model/token/JwtTokenFactory.java | 6 ++ .../security/auth/TokenOutdatingTest.java | 101 +++++++++--------- .../UserUpdateTimeRedisCache.java | 39 +++++++ .../UsersUpdateTimeCacheEvictEvent.java | 25 +++++ .../UsersUpdateTimeCaffeineCache.java | 37 +++++++ .../event/UserAuthDataChangedEvent.java | 10 +- .../server/dao/user/UserServiceImpl.java | 2 +- 14 files changed, 220 insertions(+), 145 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index b3d9cb6634..84e8f3df9d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -43,12 +43,10 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; -import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest; @@ -73,7 +71,6 @@ import java.net.URISyntaxException; public class AuthController extends BaseController { private final BCryptPasswordEncoder passwordEncoder; private final JwtTokenFactory tokenFactory; - private final RefreshTokenRepository refreshTokenRepository; private final MailService mailService; private final SystemSecurityService systemSecurityService; private final AuditLogService auditLogService; @@ -128,7 +125,7 @@ public class AuthController extends BaseController { sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId())); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); ObjectNode response = JacksonUtil.newObjectNode(); response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken()); @@ -268,10 +265,7 @@ public class AuthController extends BaseController { sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); - JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); - JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); - - return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); + return tokenFactory.createTokenPair(securityUser); } catch (Exception e) { throw handleException(e); } @@ -309,11 +303,9 @@ public class AuthController extends BaseController { String email = user.getEmail(); mailService.sendPasswordWasResetEmail(loginUrl, email); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId())); - JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); - JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); - return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); + return tokenFactory.createTokenPair(securityUser); } else { throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } @@ -367,7 +359,7 @@ public class AuthController extends BaseController { user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(user.getId())); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(user.getId(), user.getSessionId(), false)); } catch (Exception e) { throw handleException(e); } 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 e96a73b65c..6f19cde2e8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -44,10 +44,8 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; -import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.user.TbUserService; -import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -95,7 +93,6 @@ public class UserController extends BaseController { private final MailService mailService; private final JwtTokenFactory tokenFactory; - private final RefreshTokenRepository refreshTokenRepository; private final SystemSecurityService systemSecurityService; private final ApplicationEventPublisher eventPublisher; private final TbUserService tbUserService; @@ -163,9 +160,7 @@ public class UserController extends BaseController { UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); UserCredentials credentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), userId); SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); - JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); - JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); - return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); + return tokenFactory.createTokenPair(securityUser); } catch (Exception e) { throw handleException(e); } @@ -376,7 +371,7 @@ public class UserController extends BaseController { userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); if (!userCredentialsEnabled) { - eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId)); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId, null, true)); } } catch (Exception e) { throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index a623fc6862..6bb74ed758 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -17,18 +17,18 @@ package org.thingsboard.server.service.security.auth; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.CacheConstants; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.cache.usersUpdateTime.UsersUpdateTimeCacheEvictEvent; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.config.JwtSettings; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; -import javax.annotation.PostConstruct; +import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -36,30 +36,24 @@ import static java.util.concurrent.TimeUnit.SECONDS; @Service @RequiredArgsConstructor -public class TokenOutdatingService { - private final CacheManager cacheManager; +public class TokenOutdatingService extends AbstractCachedEntityService, UsersUpdateTimeCacheEvictEvent> { private final JwtTokenFactory tokenFactory; private final JwtSettings jwtSettings; - private Cache usersUpdateTimeCache; - - @PostConstruct - protected void initCache() { - usersUpdateTimeCache = cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE); - } @EventListener(classes = UserAuthDataChangedEvent.class) public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { - usersUpdateTimeCache.put(toKey(event.getUserId()), event.getTs()); + processUserSessions(event); } public boolean isOutdated(JwtToken token, UserId userId) { Claims claims = tokenFactory.parseTokenClaims(token).getBody(); long issueTime = claims.getIssuedAt().getTime(); - return Optional.ofNullable(usersUpdateTimeCache.get(toKey(userId), Long.class)) + String sessionId = claims.get("sessionId", String.class); + return Optional.ofNullable(cache.get(userId)) .map(outdatageTime -> { - if (System.currentTimeMillis() - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) { - return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); + if (outdatageTime.get().get(sessionId) != null && System.currentTimeMillis() - outdatageTime.get().get(sessionId) <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) { + return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime.get().get(sessionId)); } else { /* * Means that since the outdating has passed more than @@ -68,14 +62,36 @@ public class TokenOutdatingService { * as all the tokens issued before the outdatage time * are now expired by themselves * */ - usersUpdateTimeCache.evict(toKey(userId)); + handleEvictEvent(new UsersUpdateTimeCacheEvictEvent(userId, sessionId)); return false; } }) .orElse(false); } - private String toKey(UserId userId) { - return userId.getId().toString(); + @TransactionalEventListener(classes = UsersUpdateTimeCacheEvictEvent.class) + @Override + public void handleEvictEvent(UsersUpdateTimeCacheEvictEvent event) { + HashMap userSessions = cache.get(event.getUserId()).get(); + if (userSessions != null) { + userSessions.remove(event.getSessionId()); + cache.put(event.getUserId(), userSessions); + } + } + + private void processUserSessions(UserAuthDataChangedEvent event) { + if (cache.get(event.getUserId()) != null) { + HashMap userSessions = cache.get(event.getUserId()).get(); + if (event.isDropAllSessions()) { + userSessions.replaceAll((k, v) -> event.getTs()); + } else { + userSessions.put(event.getSessionId(), event.getTs()); + } + cache.put(event.getUserId(), userSessions); + } else { + cache.put(event.getUserId(), new HashMap<>() {{ + put(event.getSessionId(), event.getTs()); + }}); + } } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java deleted file mode 100644 index c5a666d464..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright © 2016-2022 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.auth.jwt; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.service.security.model.token.JwtTokenFactory; - -@Component -public class RefreshTokenRepository { - - private final JwtTokenFactory tokenFactory; - - @Autowired - public RefreshTokenRepository(final JwtTokenFactory tokenFactory) { - this.tokenFactory = tokenFactory; - } - - public JwtToken requestRefreshToken(SecurityUser user) { - return tokenFactory.createRefreshToken(user); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index e2a78eb605..7b85f22c2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -29,10 +29,9 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2Registration; -import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; +import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.system.SystemSecurityService; @@ -50,7 +49,6 @@ import java.util.UUID; public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtTokenFactory tokenFactory; - private final RefreshTokenRepository refreshTokenRepository; private final OAuth2ClientMapperProvider oauth2ClientMapperProvider; private final OAuth2Service oAuth2Service; private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; @@ -59,14 +57,12 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS @Autowired public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory, - final RefreshTokenRepository refreshTokenRepository, final OAuth2ClientMapperProvider oauth2ClientMapperProvider, final OAuth2Service oAuth2Service, final OAuth2AuthorizedClientService oAuth2AuthorizedClientService, final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository, final SystemSecurityService systemSecurityService) { this.tokenFactory = tokenFactory; - this.refreshTokenRepository = refreshTokenRepository; this.oauth2ClientMapperProvider = oauth2ClientMapperProvider; this.oAuth2Service = oAuth2Service; this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; @@ -97,11 +93,10 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), registration); - JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); - JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); + JwtTokenPair tokenPair = tokenFactory.createTokenPair(securityUser); clearAuthenticationAttributes(request, response); - getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); + getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken()); } catch (Exception e) { log.debug("Error occurred during processing authentication success result. " + "request [{}], response [{}], authentication [{}]", request, response, authentication, e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java index b4f0b293d3..4d7ef01914 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java @@ -25,7 +25,6 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; -import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; @@ -45,7 +44,6 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc private final ObjectMapper mapper; private final JwtTokenFactory tokenFactory; private final TwoFaConfigManager twoFaConfigManager; - private final RefreshTokenRepository refreshTokenRepository; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, @@ -62,8 +60,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc tokenPair.setRefreshToken(null); tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); } else { - tokenPair.setToken(tokenFactory.createAccessJwtToken(securityUser).getToken()); - tokenPair.setRefreshToken(refreshTokenRepository.requestRefreshToken(securityUser).getToken()); + tokenPair = tokenFactory.createTokenPair(securityUser); } response.setStatus(HttpStatus.OK.value()); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java index 380d6537f2..b7f480dfab 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.UserId; import java.util.Collection; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -31,6 +32,7 @@ public class SecurityUser extends User { private Collection authorities; private boolean enabled; private UserPrincipal userPrincipal; + private String sessionId; public SecurityUser() { super(); @@ -44,6 +46,7 @@ public class SecurityUser extends User { super(user); this.enabled = enabled; this.userPrincipal = userPrincipal; + this.sessionId = UUID.randomUUID().toString(); } public Collection getAuthorities() { @@ -71,4 +74,11 @@ public class SecurityUser extends User { this.userPrincipal = userPrincipal; } + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 8200f0c3d3..110ed5702b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -60,6 +60,7 @@ public class JwtTokenFactory { private static final String IS_PUBLIC = "isPublic"; private static final String TENANT_ID = "tenantId"; private static final String CUSTOMER_ID = "customerId"; + private static final String SESSION_ID = "sessionId"; private final JwtSettings settings; @@ -119,6 +120,7 @@ public class JwtTokenFactory { if (customerId != null) { securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId))); } + securityUser.setSessionId(claims.get(SESSION_ID, String.class)); UserPrincipal principal; if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { @@ -161,6 +163,7 @@ public class JwtTokenFactory { UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); securityUser.setUserPrincipal(principal); + securityUser.setSessionId(claims.get(SESSION_ID, String.class)); return securityUser; } @@ -183,6 +186,9 @@ public class JwtTokenFactory { Claims claims = Jwts.claims().setSubject(principal.getValue()); claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(SCOPES, scopes); + if (securityUser.getSessionId() != null) { + claims.put(SESSION_ID, securityUser.getSessionId()); + } ZonedDateTime currentTime = ZonedDateTime.now(); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index f804d4dcfd..f0e758be14 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -15,19 +15,27 @@ */ package org.thingsboard.server.service.security.auth; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootContextLoader; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.ComponentScan; import org.springframework.security.authentication.CredentialsExpiredException; -import org.thingsboard.server.common.data.CacheConstants; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; @@ -39,13 +47,9 @@ import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; import java.util.UUID; -import static java.util.concurrent.TimeUnit.DAYS; -import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -53,31 +57,33 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = TokenOutdatingTest.class, loader = SpringBootContextLoader.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@ComponentScan({"org.thingsboard.server"}) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DaoSqlTest +@TestPropertySource(properties = { + "security.jwt.tokenIssuer=test.io", + "security.jwt.tokenSigningKey=secret", + "security.jwt.tokenExpirationTime=600", + "security.jwt.refreshTokenExpTime=10" +}) public class TokenOutdatingTest { private JwtAuthenticationProvider accessTokenAuthenticationProvider; private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; + @Autowired private TokenOutdatingService tokenOutdatingService; - private ConcurrentMapCacheManager cacheManager; + @Autowired private JwtTokenFactory tokenFactory; - private JwtSettings jwtSettings; + private SecurityUser securityUser; - private UserId userId; - - @BeforeEach + @Before public void setUp() { - jwtSettings = new JwtSettings(); - jwtSettings.setTokenIssuer("test.io"); - jwtSettings.setTokenExpirationTime((int) MINUTES.toSeconds(10)); - jwtSettings.setRefreshTokenExpTime((int) DAYS.toSeconds(7)); - jwtSettings.setTokenSigningKey("secret"); - tokenFactory = new JwtTokenFactory(jwtSettings); - - cacheManager = new ConcurrentMapCacheManager(); - tokenOutdatingService = new TokenOutdatingService(cacheManager, tokenFactory, jwtSettings); - tokenOutdatingService.initCache(); - - userId = new UserId(UUID.randomUUID()); + UserId userId = new UserId(UUID.randomUUID()); + securityUser = createMockSecurityUser(userId); UserService userService = mock(UserService.class); @@ -97,28 +103,28 @@ public class TokenOutdatingTest { @Test public void testOutdateOldUserTokens() throws Exception { - JwtToken jwtToken = createAccessJwtToken(userId); + JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); SECONDS.sleep(1); // need to wait before outdating so that outdatage time is strictly after token issue time - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); - assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId)); + tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); SECONDS.sleep(1); - JwtToken newJwtToken = tokenFactory.createAccessJwtToken(createMockSecurityUser(userId)); - assertFalse(tokenOutdatingService.isOutdated(newJwtToken, userId)); + JwtToken newJwtToken = tokenFactory.createAccessJwtToken(securityUser); + assertFalse(tokenOutdatingService.isOutdated(newJwtToken, securityUser.getId())); } @Test public void testAuthenticateWithOutdatedAccessToken() throws InterruptedException { - RawAccessJwtToken accessJwtToken = getRawJwtToken(createAccessJwtToken(userId)); + RawAccessJwtToken accessJwtToken = getRawJwtToken(tokenFactory.createAccessJwtToken(securityUser)); assertDoesNotThrow(() -> { accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); + tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); assertThrows(JwtExpiredTokenException.class, () -> { accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); @@ -127,14 +133,14 @@ public class TokenOutdatingTest { @Test public void testAuthenticateWithOutdatedRefreshToken() throws InterruptedException { - RawAccessJwtToken refreshJwtToken = getRawJwtToken(createRefreshJwtToken(userId)); + RawAccessJwtToken refreshJwtToken = getRawJwtToken(tokenFactory.createRefreshToken(securityUser)); assertDoesNotThrow(() -> { refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); + tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); assertThrows(CredentialsExpiredException.class, () -> { refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); @@ -143,32 +149,20 @@ public class TokenOutdatingTest { @Test public void testTokensOutdatageTimeRemovalFromCache() throws Exception { - JwtToken jwtToken = createAccessJwtToken(userId); + JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); - - int refreshTokenExpirationTime = 3; - jwtSettings.setRefreshTokenExpTime(refreshTokenExpirationTime); + tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); - SECONDS.sleep(refreshTokenExpirationTime - 2); - - assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId)); - assertNotNull(cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE).get(userId.getId().toString())); + SECONDS.sleep(1); - SECONDS.sleep(3); + assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); - assertFalse(tokenOutdatingService.isOutdated(jwtToken, userId)); - assertNull(cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE).get(userId.getId().toString())); - } + SECONDS.sleep(10); - private JwtToken createAccessJwtToken(UserId userId) { - return tokenFactory.createAccessJwtToken(createMockSecurityUser(userId)); + assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); } - private JwtToken createRefreshJwtToken(UserId userId) { - return tokenFactory.createRefreshToken(createMockSecurityUser(userId)); - } private RawAccessJwtToken getRawJwtToken(JwtToken token) { return new RawAccessJwtToken(token.getToken()); @@ -180,6 +174,7 @@ public class TokenOutdatingTest { securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); securityUser.setAuthority(Authority.CUSTOMER_USER); securityUser.setId(userId); + securityUser.setSessionId(UUID.randomUUID().toString()); return securityUser; } } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java new file mode 100644 index 0000000000..3d96d4c2a7 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2022 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.cache.usersUpdateTime; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbFSTRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.UserId; + +import java.util.HashMap; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("UsersUpdateTimeCache") +public class UserUpdateTimeRedisCache extends RedisTbTransactionalCache> { + + @Autowired + public UserUpdateTimeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.USERS_UPDATE_TIME_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + } +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java new file mode 100644 index 0000000000..16173e388f --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2022 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.cache.usersUpdateTime; + +import lombok.Data; +import org.thingsboard.server.common.data.id.UserId; + +@Data +public class UsersUpdateTimeCacheEvictEvent { + private final UserId userId; + private final String sessionId; +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java new file mode 100644 index 0000000000..adb2117368 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2022 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.cache.usersUpdateTime; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.UserId; + +import java.util.HashMap; + + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("UsersUpdateTimeCache") +public class UsersUpdateTimeCaffeineCache extends CaffeineTbTransactionalCache> { + + @Autowired + public UsersUpdateTimeCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.USERS_UPDATE_TIME_CACHE); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java index 2dcd95f435..5d9ef3a1cd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java @@ -18,13 +18,19 @@ package org.thingsboard.server.common.data.security.event; import lombok.Data; import org.thingsboard.server.common.data.id.UserId; +import java.io.Serializable; + @Data -public class UserAuthDataChangedEvent { +public class UserAuthDataChangedEvent implements Serializable { private final UserId userId; + private final String sessionId; private final long ts; + private final boolean dropAllSessions; - public UserAuthDataChangedEvent(UserId userId) { + public UserAuthDataChangedEvent(UserId userId, String sessionId, boolean dropAllSessions) { this.userId = userId; + this.sessionId = sessionId; + this.dropAllSessions = dropAllSessions; this.ts = System.currentTimeMillis(); } 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 fc0de8a4fa..78ad93b68c 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 @@ -211,7 +211,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userAuthSettingsDao.removeByUserId(userId); deleteEntityRelations(tenantId, userId); userDao.removeById(tenantId, userId.getId()); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId)); + eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId, null, true)); } @Override From 215a44b7c7256a87c90858664b962f2cb5571ddd Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 2 Nov 2022 09:19:51 +0100 Subject: [PATCH 5/8] Added tests --- .../security/auth/TokenOutdatingTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index f0e758be14..187b77fd75 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -163,6 +163,29 @@ public class TokenOutdatingTest { assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); } + @Test + public void testOnlyOneTokenExpired() throws InterruptedException { + JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); + + SecurityUser anotherSecurityUser = new SecurityUser(securityUser, securityUser.isEnabled(), securityUser.getUserPrincipal()); + JwtToken anotherJwtToken = tokenFactory.createAccessJwtToken(anotherSecurityUser); + + assertDoesNotThrow(() -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); + }); + + SECONDS.sleep(1); + tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + + assertThrows(JwtExpiredTokenException.class, () -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); + }); + + assertDoesNotThrow(() -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken))); + }); + } + private RawAccessJwtToken getRawJwtToken(JwtToken token) { return new RawAccessJwtToken(token.getToken()); From 6d34aa237c0e0e11e8ac7bfc8e0e5f2bcc6a9578 Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 2 Nov 2022 13:03:17 +0100 Subject: [PATCH 6/8] Refactoring --- .../server/controller/AuthController.java | 8 ++- .../server/controller/UserController.java | 3 +- .../security/auth/TokenOutdatingService.java | 67 ++++++++----------- .../RefreshTokenAuthenticationProvider.java | 2 +- .../security/model/token/JwtTokenFactory.java | 8 ++- .../security/auth/TokenOutdatingTest.java | 41 ++++++++++-- .../UserUpdateTimeRedisCache.java | 5 +- .../UsersUpdateTimeCacheEvictEvent.java | 4 +- .../UsersUpdateTimeCaffeineCache.java | 5 +- .../event/UserAuthDataChangedEvent.java | 17 +---- .../UserCredentialsInvalidationEvent.java | 42 ++++++++++++ .../event/UserSessionInvalidationEvent.java | 39 +++++++++++ .../server/dao/user/UserServiceImpl.java | 3 +- 13 files changed, 166 insertions(+), 78 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserSessionInvalidationEvent.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 84e8f3df9d..d098b1edec 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -43,6 +43,8 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; +import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; +import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.dao.audit.AuditLogService; @@ -125,7 +127,7 @@ public class AuthController extends BaseController { sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); ObjectNode response = JacksonUtil.newObjectNode(); response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken()); @@ -303,7 +305,7 @@ public class AuthController extends BaseController { String email = user.getEmail(); mailService.sendPasswordWasResetEmail(loginUrl, email); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); return tokenFactory.createTokenPair(securityUser); } else { @@ -359,7 +361,7 @@ public class AuthController extends BaseController { user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(user.getId(), user.getSessionId(), false)); + eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId())); } catch (Exception e) { throw handleException(e); } 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 6f19cde2e8..fe07c4b49c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; +import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.user.TbUserService; import org.thingsboard.server.service.security.model.JwtTokenPair; @@ -371,7 +372,7 @@ public class UserController extends BaseController { userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); if (!userCredentialsEnabled) { - eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId, null, true)); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId)); } } catch (Exception e) { throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index 6bb74ed758..d5f62037af 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -19,16 +19,14 @@ import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import org.springframework.transaction.event.TransactionalEventListener; -import org.thingsboard.server.cache.usersUpdateTime.UsersUpdateTimeCacheEvictEvent; +import org.thingsboard.server.cache.TbCacheValueWrapper; +import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.config.JwtSettings; -import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; -import java.util.HashMap; import java.util.Optional; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -36,13 +34,14 @@ import static java.util.concurrent.TimeUnit.SECONDS; @Service @RequiredArgsConstructor -public class TokenOutdatingService extends AbstractCachedEntityService, UsersUpdateTimeCacheEvictEvent> { +public class TokenOutdatingService { + private final TbTransactionalCache cache; private final JwtTokenFactory tokenFactory; private final JwtSettings jwtSettings; @EventListener(classes = UserAuthDataChangedEvent.class) public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { - processUserSessions(event); + cache.put(event.getId(), event.getTs()); } public boolean isOutdated(JwtToken token, UserId userId) { @@ -50,48 +49,36 @@ public class TokenOutdatingService extends AbstractCachedEntityService { - if (outdatageTime.get().get(sessionId) != null && System.currentTimeMillis() - outdatageTime.get().get(sessionId) <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) { - return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime.get().get(sessionId)); + + Boolean isUserIdOutdated = Optional.ofNullable(cache.get(userId.toString())) + .map(outdatageTimeByUserId -> { + if (refreshTokenNotExpired(outdatageTimeByUserId.get(), System.currentTimeMillis())) { + return accessTokenNotExpired(issueTime, outdatageTimeByUserId.get()); } else { - /* - * Means that since the outdating has passed more than - * the lifetime of refresh token (the longest lived) - * and there is no need to store outdatage time anymore - * as all the tokens issued before the outdatage time - * are now expired by themselves - * */ - handleEvictEvent(new UsersUpdateTimeCacheEvictEvent(userId, sessionId)); return false; } }) .orElse(false); - } - @TransactionalEventListener(classes = UsersUpdateTimeCacheEvictEvent.class) - @Override - public void handleEvictEvent(UsersUpdateTimeCacheEvictEvent event) { - HashMap userSessions = cache.get(event.getUserId()).get(); - if (userSessions != null) { - userSessions.remove(event.getSessionId()); - cache.put(event.getUserId(), userSessions); + if (!isUserIdOutdated) { + return Optional.ofNullable(cache.get(sessionId)).map(outdatageTimeBySessionId -> { + if (refreshTokenNotExpired(outdatageTimeBySessionId.get(), System.currentTimeMillis())) { + return accessTokenNotExpired(issueTime, outdatageTimeBySessionId.get()); + } else { + return false; + } + } + ).orElse(false); } + + return isUserIdOutdated; } - private void processUserSessions(UserAuthDataChangedEvent event) { - if (cache.get(event.getUserId()) != null) { - HashMap userSessions = cache.get(event.getUserId()).get(); - if (event.isDropAllSessions()) { - userSessions.replaceAll((k, v) -> event.getTs()); - } else { - userSessions.put(event.getSessionId(), event.getTs()); - } - cache.put(event.getUserId(), userSessions); - } else { - cache.put(event.getUserId(), new HashMap<>() {{ - put(event.getSessionId(), event.getTs()); - }}); - } + private boolean accessTokenNotExpired(long issueTime, Long outdatageTime) { + return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); + } + + private boolean refreshTokenNotExpired(Long outdatageTime, long currentTime) { + return currentTime - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 8003cfd012..700bbd7f74 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -66,7 +66,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide } else { securityUser = authenticateByPublicId(principal.getValue()); } - + securityUser.setSessionId(unsafeUser.getSessionId()); if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) { throw new CredentialsExpiredException("Token is outdated"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 110ed5702b..dca366df18 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -120,7 +120,9 @@ public class JwtTokenFactory { if (customerId != null) { securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId))); } - securityUser.setSessionId(claims.get(SESSION_ID, String.class)); + if (claims.get(SESSION_ID, String.class) != null) { + securityUser.setSessionId(claims.get(SESSION_ID, String.class)); + } UserPrincipal principal; if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { @@ -163,7 +165,9 @@ public class JwtTokenFactory { UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); securityUser.setUserPrincipal(principal); - securityUser.setSessionId(claims.get(SESSION_ID, String.class)); + if (claims.get(SESSION_ID, String.class) != null) { + securityUser.setSessionId(claims.get(SESSION_ID, String.class)); + } return securityUser; } diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index 187b77fd75..52371e1755 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -33,6 +33,8 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; +import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; +import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -106,7 +108,7 @@ public class TokenOutdatingTest { JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); SECONDS.sleep(1); // need to wait before outdating so that outdatage time is strictly after token issue time - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); SECONDS.sleep(1); @@ -124,7 +126,7 @@ public class TokenOutdatingTest { }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); assertThrows(JwtExpiredTokenException.class, () -> { accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); @@ -140,7 +142,7 @@ public class TokenOutdatingTest { }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); assertThrows(CredentialsExpiredException.class, () -> { refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); @@ -152,7 +154,7 @@ public class TokenOutdatingTest { JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); SECONDS.sleep(1); @@ -175,7 +177,8 @@ public class TokenOutdatingTest { }); SECONDS.sleep(1); - tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false)); + + tokenOutdatingService.onUserAuthDataChanged(new UserSessionInvalidationEvent(securityUser.getSessionId())); assertThrows(JwtExpiredTokenException.class, () -> { accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); @@ -186,6 +189,34 @@ public class TokenOutdatingTest { }); } + @Test + public void testResetAllSessions() throws InterruptedException { + JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser); + + SecurityUser anotherSecurityUser = new SecurityUser(securityUser, securityUser.isEnabled(), securityUser.getUserPrincipal()); + JwtToken anotherJwtToken = tokenFactory.createAccessJwtToken(anotherSecurityUser); + + assertDoesNotThrow(() -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); + }); + + assertDoesNotThrow(() -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken))); + }); + + SECONDS.sleep(1); + + tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId())); + + assertThrows(JwtExpiredTokenException.class, () -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken))); + }); + + assertThrows(JwtExpiredTokenException.class, () -> { + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken))); + }); + } + private RawAccessJwtToken getRawJwtToken(JwtToken token) { return new RawAccessJwtToken(token.getToken()); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java index 3d96d4c2a7..c4084773a2 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java @@ -24,13 +24,10 @@ import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbFSTRedisSerializer; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.id.UserId; - -import java.util.HashMap; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("UsersUpdateTimeCache") -public class UserUpdateTimeRedisCache extends RedisTbTransactionalCache> { +public class UserUpdateTimeRedisCache extends RedisTbTransactionalCache { @Autowired public UserUpdateTimeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java index 16173e388f..1b3d980212 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java @@ -16,10 +16,8 @@ package org.thingsboard.server.cache.usersUpdateTime; import lombok.Data; -import org.thingsboard.server.common.data.id.UserId; @Data public class UsersUpdateTimeCacheEvictEvent { - private final UserId userId; - private final String sessionId; + private final String key; } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java index adb2117368..a2c07ecaf0 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java @@ -21,14 +21,11 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CaffeineTbTransactionalCache; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.id.UserId; - -import java.util.HashMap; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("UsersUpdateTimeCache") -public class UsersUpdateTimeCaffeineCache extends CaffeineTbTransactionalCache> { +public class UsersUpdateTimeCaffeineCache extends CaffeineTbTransactionalCache { @Autowired public UsersUpdateTimeCaffeineCache(CacheManager cacheManager) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java index 5d9ef3a1cd..9ceaa2386e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java @@ -20,18 +20,7 @@ import org.thingsboard.server.common.data.id.UserId; import java.io.Serializable; -@Data -public class UserAuthDataChangedEvent implements Serializable { - private final UserId userId; - private final String sessionId; - private final long ts; - private final boolean dropAllSessions; - - public UserAuthDataChangedEvent(UserId userId, String sessionId, boolean dropAllSessions) { - this.userId = userId; - this.sessionId = sessionId; - this.dropAllSessions = dropAllSessions; - this.ts = System.currentTimeMillis(); - } - +public abstract class UserAuthDataChangedEvent implements Serializable { + public abstract String getId(); + public abstract long getTs(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java new file mode 100644 index 0000000000..26eac46fc9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2022 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.common.data.security.event; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.thingsboard.server.common.data.id.UserId; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class UserCredentialsInvalidationEvent extends UserAuthDataChangedEvent { + private final UserId userId; + private final long ts; + + public UserCredentialsInvalidationEvent(UserId userId) { + this.userId = userId; + this.ts = System.currentTimeMillis(); + } + + @Override + public String getId() { + return userId.toString(); + } + + @Override + public long getTs() { + return ts; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserSessionInvalidationEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserSessionInvalidationEvent.java new file mode 100644 index 0000000000..2e6aea6bb9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserSessionInvalidationEvent.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2022 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.common.data.security.event; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public class UserSessionInvalidationEvent extends UserAuthDataChangedEvent { + private final String sessionId; + private final long ts; + + public UserSessionInvalidationEvent(String sessionId) { + this.sessionId = sessionId; + this.ts = System.currentTimeMillis(); + } + + @Override + public String getId() { + return sessionId; + } + + @Override + public long getTs() { + return ts; + } +} 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 78ad93b68c..42aac0f59e 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 @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; +import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; @@ -211,7 +212,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userAuthSettingsDao.removeByUserId(userId); deleteEntityRelations(tenantId, userId); userDao.removeById(tenantId, userId.getId()); - eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId, null, true)); + eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId)); } @Override From 3ab1f34594fc4595134523ac7e50e4ab9b56cbad Mon Sep 17 00:00:00 2001 From: oyurov Date: Wed, 2 Nov 2022 14:07:49 +0100 Subject: [PATCH 7/8] Clean code --- .../security/auth/TokenOutdatingService.java | 44 +++++-------------- .../src/main/resources/thingsboard.yml | 3 +- .../security/auth/TokenOutdatingTest.java | 6 +-- ...sersSessionInvalidationCaffeineCache.java} | 8 ++-- ...> UsersSessionInvalidationRedisCache.java} | 8 ++-- .../UsersUpdateTimeCacheEvictEvent.java | 23 ---------- .../server/common/data/CacheConstants.java | 2 +- .../event/UserAuthDataChangedEvent.java | 3 -- 8 files changed, 24 insertions(+), 73 deletions(-) rename common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/{UsersUpdateTimeCaffeineCache.java => UsersSessionInvalidationCaffeineCache.java} (79%) rename common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/{UserUpdateTimeRedisCache.java => UsersSessionInvalidationRedisCache.java} (75%) delete mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index d5f62037af..101455f68e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -19,66 +19,42 @@ import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import org.thingsboard.server.cache.TbCacheValueWrapper; +import org.springframework.util.StringUtils; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import java.util.Optional; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; @Service @RequiredArgsConstructor public class TokenOutdatingService { private final TbTransactionalCache cache; private final JwtTokenFactory tokenFactory; - private final JwtSettings jwtSettings; @EventListener(classes = UserAuthDataChangedEvent.class) public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { - cache.put(event.getId(), event.getTs()); + if (StringUtils.hasText(event.getId())) { + cache.put(event.getId(), event.getTs()); + } } public boolean isOutdated(JwtToken token, UserId userId) { Claims claims = tokenFactory.parseTokenClaims(token).getBody(); long issueTime = claims.getIssuedAt().getTime(); - - String sessionId = claims.get("sessionId", String.class); - - Boolean isUserIdOutdated = Optional.ofNullable(cache.get(userId.toString())) - .map(outdatageTimeByUserId -> { - if (refreshTokenNotExpired(outdatageTimeByUserId.get(), System.currentTimeMillis())) { - return accessTokenNotExpired(issueTime, outdatageTimeByUserId.get()); - } else { - return false; - } - }) - .orElse(false); - - if (!isUserIdOutdated) { - return Optional.ofNullable(cache.get(sessionId)).map(outdatageTimeBySessionId -> { - if (refreshTokenNotExpired(outdatageTimeBySessionId.get(), System.currentTimeMillis())) { - return accessTokenNotExpired(issueTime, outdatageTimeBySessionId.get()); - } else { - return false; - } - } - ).orElse(false); - } - - return isUserIdOutdated; + String sessionId = claims.get("sessionId", String.class) == null ? "" : claims.get("sessionId", String.class); + return isTokenOutdated(issueTime, userId.toString()) || isTokenOutdated(issueTime, sessionId); } - private boolean accessTokenNotExpired(long issueTime, Long outdatageTime) { - return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); + private Boolean isTokenOutdated(long issueTime, String sessionId) { + return Optional.ofNullable(cache.get(sessionId)).map(outdatageTime -> isTokenOutdated(issueTime, outdatageTime.get())).orElse(false); } - private boolean refreshTokenNotExpired(Long outdatageTime, long currentTime) { - return currentTime - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime()); + private boolean isTokenOutdated(long issueTime, Long outdatageTime) { + return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f5ac40afd5..75703309f0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -421,7 +421,8 @@ cache: timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}" maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}" usersUpdateTime: - timeToLiveInMinutes: "${CACHE_SPECS_USERS_UPDATE_TIME_TTL:20000}" + # MUST be the same as jwt refresh token expiration time, the value here represents 604800 seconds in minutes + timeToLiveInMinutes: "${CACHE_SPECS_USERS_UPDATE_TIME_TTL:10080}" maxSize: "${CACHE_SPECS_USERS_UPDATE_TIME_MAX_SIZE:10000}" otaPackages: timeToLiveInMinutes: "${CACHE_SPECS_OTA_PACKAGES_TTL:60}" diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index 52371e1755..dddd1f8a71 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -32,7 +32,6 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.JwtToken; @@ -70,7 +69,8 @@ import static org.mockito.Mockito.when; "security.jwt.tokenIssuer=test.io", "security.jwt.tokenSigningKey=secret", "security.jwt.tokenExpirationTime=600", - "security.jwt.refreshTokenExpTime=10" + "security.jwt.refreshTokenExpTime=60", + "cache.specs.usersUpdateTime.timeToLiveInMinutes=1" }) public class TokenOutdatingTest { private JwtAuthenticationProvider accessTokenAuthenticationProvider; @@ -160,7 +160,7 @@ public class TokenOutdatingTest { assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); - SECONDS.sleep(10); + SECONDS.sleep(60); assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId())); } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationCaffeineCache.java similarity index 79% rename from common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationCaffeineCache.java index a2c07ecaf0..c052627099 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationCaffeineCache.java @@ -24,11 +24,11 @@ import org.thingsboard.server.common.data.CacheConstants; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) -@Service("UsersUpdateTimeCache") -public class UsersUpdateTimeCaffeineCache extends CaffeineTbTransactionalCache { +@Service("UsersSessionInvalidation") +public class UsersSessionInvalidationCaffeineCache extends CaffeineTbTransactionalCache { @Autowired - public UsersUpdateTimeCaffeineCache(CacheManager cacheManager) { - super(cacheManager, CacheConstants.USERS_UPDATE_TIME_CACHE); + public UsersSessionInvalidationCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.USERS_SESSION_INVALIDATION_CACHE); } } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationRedisCache.java similarity index 75% rename from common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationRedisCache.java index c4084773a2..61d1d21516 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationRedisCache.java @@ -26,11 +26,11 @@ import org.thingsboard.server.cache.TbFSTRedisSerializer; import org.thingsboard.server.common.data.CacheConstants; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") -@Service("UsersUpdateTimeCache") -public class UserUpdateTimeRedisCache extends RedisTbTransactionalCache { +@Service("UsersSessionInvalidation") +public class UsersSessionInvalidationRedisCache extends RedisTbTransactionalCache { @Autowired - public UserUpdateTimeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.USERS_UPDATE_TIME_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + public UsersSessionInvalidationRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.USERS_SESSION_INVALIDATION_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); } } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java b/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java deleted file mode 100644 index 1b3d980212..0000000000 --- a/common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright © 2016-2022 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.cache.usersUpdateTime; - -import lombok.Data; - -@Data -public class UsersUpdateTimeCacheEvictEvent { - private final String key; -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 0c5ed80cd1..3199b1c0cc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -32,7 +32,7 @@ public class CacheConstants { public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; - public static final String USERS_UPDATE_TIME_CACHE = "usersUpdateTime"; + public static final String USERS_SESSION_INVALIDATION_CACHE = "usersUpdateTime"; public static final String OTA_PACKAGE_CACHE = "otaPackages"; public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData"; public static final String REPOSITORY_SETTINGS_CACHE = "repositorySettings"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java index 9ceaa2386e..0436627033 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.common.data.security.event; -import lombok.Data; -import org.thingsboard.server.common.data.id.UserId; - import java.io.Serializable; public abstract class UserAuthDataChangedEvent implements Serializable { From 4fad5879404d3b2f56bc831c4d65fc79faf3010d Mon Sep 17 00:00:00 2001 From: oyurov Date: Thu, 3 Nov 2022 16:52:15 +0100 Subject: [PATCH 8/8] Refactoring --- .../auth/DefaultTokenOutdatingService.java | 65 +++++++++++++++++++ .../security/auth/TokenOutdatingService.java | 40 +----------- .../auth/jwt/JwtAuthenticationProvider.java | 2 +- .../RefreshTokenAuthenticationProvider.java | 2 +- .../UserCredentialsInvalidationEvent.java | 2 - 5 files changed, 70 insertions(+), 41 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java new file mode 100644 index 0000000000..fecc17d33b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2022 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.auth; + +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; +import org.thingsboard.server.common.data.security.model.JwtToken; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; + +import java.util.Optional; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +@Service +@RequiredArgsConstructor +public class DefaultTokenOutdatingService implements TokenOutdatingService { + private final TbTransactionalCache cache; + private final JwtTokenFactory tokenFactory; + + @EventListener(classes = UserAuthDataChangedEvent.class) + public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { + if (StringUtils.hasText(event.getId())) { + cache.put(event.getId(), event.getTs()); + } + } + + @Override + public boolean isOutdated(JwtToken token, UserId userId) { + Claims claims = tokenFactory.parseTokenClaims(token).getBody(); + long issueTime = claims.getIssuedAt().getTime(); + String sessionId = claims.get("sessionId", String.class); + if (sessionId == null) { + return isTokenOutdated(issueTime, userId.toString()); + } else { + return isTokenOutdated(issueTime, userId.toString()) || isTokenOutdated(issueTime, sessionId); + } + } + + private Boolean isTokenOutdated(long issueTime, String sessionId) { + return Optional.ofNullable(cache.get(sessionId)).map(outdatageTime -> isTokenOutdated(issueTime, outdatageTime.get())).orElse(false); + } + + private boolean isTokenOutdated(long issueTime, Long outdatageTime) { + return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java index 101455f68e..9013a85553 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java @@ -15,46 +15,12 @@ */ package org.thingsboard.server.service.security.auth; -import io.jsonwebtoken.Claims; -import lombok.RequiredArgsConstructor; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; -import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; import org.thingsboard.server.common.data.security.model.JwtToken; -import org.thingsboard.server.service.security.model.token.JwtTokenFactory; -import java.util.Optional; +public interface TokenOutdatingService { + void onUserAuthDataChanged(UserAuthDataChangedEvent event); -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -@Service -@RequiredArgsConstructor -public class TokenOutdatingService { - private final TbTransactionalCache cache; - private final JwtTokenFactory tokenFactory; - - @EventListener(classes = UserAuthDataChangedEvent.class) - public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { - if (StringUtils.hasText(event.getId())) { - cache.put(event.getId(), event.getTs()); - } - } - - public boolean isOutdated(JwtToken token, UserId userId) { - Claims claims = tokenFactory.parseTokenClaims(token).getBody(); - long issueTime = claims.getIssuedAt().getTime(); - String sessionId = claims.get("sessionId", String.class) == null ? "" : claims.get("sessionId", String.class); - return isTokenOutdated(issueTime, userId.toString()) || isTokenOutdated(issueTime, sessionId); - } - - private Boolean isTokenOutdated(long issueTime, String sessionId) { - return Optional.ofNullable(cache.get(sessionId)).map(outdatageTime -> isTokenOutdated(issueTime, outdatageTime.get())).orElse(false); - } - - private boolean isTokenOutdated(long issueTime, Long outdatageTime) { - return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); - } + boolean isOutdated(JwtToken token, UserId userId); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java index dcdce946e0..9ce2ae75fc 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java @@ -20,8 +20,8 @@ import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; -import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.auth.JwtAuthenticationToken; +import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 700bbd7f74..27549752e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -33,11 +33,11 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; +import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java index 26eac46fc9..63ee49e53b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java @@ -16,10 +16,8 @@ package org.thingsboard.server.common.data.security.event; import lombok.EqualsAndHashCode; -import lombok.Getter; import org.thingsboard.server.common.data.id.UserId; -@Getter @EqualsAndHashCode(callSuper = true) public class UserCredentialsInvalidationEvent extends UserAuthDataChangedEvent { private final UserId userId;