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