Browse Source

Merge branch 'feature/jwt-token-outdate-on-logout' of github.com:CooL16/thingsboard

pull/7571/head
Andrii Shvaika 4 years ago
parent
commit
6e33b09205
  1. 19
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  2. 10
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  3. 65
      application/src/main/java/org/thingsboard/server/service/security/auth/DefaultTokenOutdatingService.java
  4. 61
      application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtAuthenticationProvider.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
  7. 38
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java
  8. 11
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  9. 5
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  10. 10
      application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
  11. 10
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  12. 3
      application/src/main/resources/thingsboard.yml
  13. 12
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  14. 4
      application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java
  15. 12
      application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java
  16. 8
      application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
  17. 151
      application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java
  18. 34
      common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationCaffeineCache.java
  19. 36
      common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationRedisCache.java
  20. 2
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  21. 16
      common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java
  22. 40
      common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java
  23. 39
      common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserSessionInvalidationEvent.java
  24. 3
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

19
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -43,12 +43,12 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials; 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.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.model.JwtToken; 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.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent; 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.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ActivateUserRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest;
@ -73,7 +73,6 @@ import java.net.URISyntaxException;
public class AuthController extends BaseController { public class AuthController extends BaseController {
private final BCryptPasswordEncoder passwordEncoder; private final BCryptPasswordEncoder passwordEncoder;
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final RefreshTokenRepository refreshTokenRepository;
private final MailService mailService; private final MailService mailService;
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final AuditLogService auditLogService; private final AuditLogService auditLogService;
@ -128,7 +127,7 @@ public class AuthController extends BaseController {
sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED); sendEntityNotificationMsg(getTenantId(), userCredentials.getUserId(), EdgeEventActionType.CREDENTIALS_UPDATED);
eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId())); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
ObjectNode response = JacksonUtil.newObjectNode(); ObjectNode response = JacksonUtil.newObjectNode();
response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken());
response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken()); response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken());
@ -268,10 +267,7 @@ public class AuthController extends BaseController {
sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED); sendEntityNotificationMsg(user.getTenantId(), user.getId(), EdgeEventActionType.CREDENTIALS_UPDATED);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); return tokenFactory.createTokenPair(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -309,11 +305,9 @@ public class AuthController extends BaseController {
String email = user.getEmail(); String email = user.getEmail();
mailService.sendPasswordWasResetEmail(loginUrl, email); mailService.sendPasswordWasResetEmail(loginUrl, email);
eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId())); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); return tokenFactory.createTokenPair(securityUser);
} else { } else {
throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
@ -367,6 +361,7 @@ public class AuthController extends BaseController {
user.getTenantId(), user.getCustomerId(), user.getId(), user.getTenantId(), user.getCustomerId(), user.getId(),
user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device); user.getName(), user.getId(), null, ActionType.LOGOUT, null, clientAddress, browser, os, device);
eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId()));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }

10
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -44,10 +44,9 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials; 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.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.user.TbUserService; 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.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
@ -95,7 +94,6 @@ public class UserController extends BaseController {
private final MailService mailService; private final MailService mailService;
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final RefreshTokenRepository refreshTokenRepository;
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private final TbUserService tbUserService; private final TbUserService tbUserService;
@ -163,9 +161,7 @@ public class UserController extends BaseController {
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
UserCredentials credentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), userId); UserCredentials credentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), userId);
SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); return tokenFactory.createTokenPair(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -376,7 +372,7 @@ public class UserController extends BaseController {
userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled);
if (!userCredentialsEnabled) { if (!userCredentialsEnabled) {
eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId)); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId));
} }
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);

65
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<String, Long> 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);
}
}

61
application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java

@ -15,67 +15,12 @@
*/ */
package org.thingsboard.server.service.security.auth; 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.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; 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.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import javax.annotation.PostConstruct; public interface TokenOutdatingService {
import java.util.Optional; void onUserAuthDataChanged(UserAuthDataChangedEvent event);
import static java.util.concurrent.TimeUnit.MILLISECONDS; boolean isOutdated(JwtToken token, UserId userId);
import static java.util.concurrent.TimeUnit.SECONDS;
@Service
@RequiredArgsConstructor
public class TokenOutdatingService {
private final CacheManager cacheManager;
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());
}
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))
.map(outdatageTime -> {
if (System.currentTimeMillis() - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) {
return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime);
} 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
* */
usersUpdateTimeCache.evict(toKey(userId));
return false;
}
})
.orElse(false);
}
private String toKey(UserId userId) {
return userId.getId().toString();
}
} }

2
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.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component; 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.JwtAuthenticationToken;
import org.thingsboard.server.service.security.auth.TokenOutdatingService;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;

4
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.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; 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.common.data.security.UserCredentials;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; 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.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -66,7 +66,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
} else { } else {
securityUser = authenticateByPublicId(principal.getValue()); securityUser = authenticateByPublicId(principal.getValue());
} }
securityUser.setSessionId(unsafeUser.getSessionId());
if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) { if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) {
throw new CredentialsExpiredException("Token is outdated"); throw new CredentialsExpiredException("Token is outdated");
} }

38
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java

@ -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);
}
}

11
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.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration; 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.dao.oauth2.OAuth2Service;
import org.thingsboard.server.queue.util.TbCoreComponent; 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.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -54,7 +53,6 @@ import static org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAut
public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final RefreshTokenRepository refreshTokenRepository;
private final OAuth2ClientMapperProvider oauth2ClientMapperProvider; private final OAuth2ClientMapperProvider oauth2ClientMapperProvider;
private final OAuth2Service oAuth2Service; private final OAuth2Service oAuth2Service;
private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
@ -63,14 +61,12 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
@Autowired @Autowired
public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory, public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory,
final RefreshTokenRepository refreshTokenRepository,
final OAuth2ClientMapperProvider oauth2ClientMapperProvider, final OAuth2ClientMapperProvider oauth2ClientMapperProvider,
final OAuth2Service oAuth2Service, final OAuth2Service oAuth2Service,
final OAuth2AuthorizedClientService oAuth2AuthorizedClientService, final OAuth2AuthorizedClientService oAuth2AuthorizedClientService,
final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository, final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository,
final SystemSecurityService systemSecurityService) { final SystemSecurityService systemSecurityService) {
this.tokenFactory = tokenFactory; this.tokenFactory = tokenFactory;
this.refreshTokenRepository = refreshTokenRepository;
this.oauth2ClientMapperProvider = oauth2ClientMapperProvider; this.oauth2ClientMapperProvider = oauth2ClientMapperProvider;
this.oAuth2Service = oAuth2Service; this.oAuth2Service = oAuth2Service;
this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService;
@ -106,11 +102,10 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
registration); registration);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtTokenPair tokenPair = tokenFactory.createTokenPair(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
clearAuthenticationAttributes(request, response); 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) { } catch (Exception e) {
log.debug("Error occurred during processing authentication success result. " + log.debug("Error occurred during processing authentication success result. " +
"request [{}], response [{}], authentication [{}]", request, response, authentication, e); "request [{}], response [{}], authentication [{}]", request, response, authentication, e);

5
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.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; 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.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -45,7 +44,6 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
private final ObjectMapper mapper; private final ObjectMapper mapper;
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final TwoFaConfigManager twoFaConfigManager; private final TwoFaConfigManager twoFaConfigManager;
private final RefreshTokenRepository refreshTokenRepository;
@Override @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
@ -62,8 +60,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
tokenPair.setRefreshToken(null); tokenPair.setRefreshToken(null);
tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN);
} else { } else {
tokenPair.setToken(tokenFactory.createAccessJwtToken(securityUser).getToken()); tokenPair = tokenFactory.createTokenPair(securityUser);
tokenPair.setRefreshToken(refreshTokenRepository.requestRefreshToken(securityUser).getToken());
} }
response.setStatus(HttpStatus.OK.value()); response.setStatus(HttpStatus.OK.value());

10
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 org.thingsboard.server.common.data.id.UserId;
import java.util.Collection; import java.util.Collection;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -31,6 +32,7 @@ public class SecurityUser extends User {
private Collection<GrantedAuthority> authorities; private Collection<GrantedAuthority> authorities;
private boolean enabled; private boolean enabled;
private UserPrincipal userPrincipal; private UserPrincipal userPrincipal;
private String sessionId;
public SecurityUser() { public SecurityUser() {
super(); super();
@ -44,6 +46,7 @@ public class SecurityUser extends User {
super(user); super(user);
this.enabled = enabled; this.enabled = enabled;
this.userPrincipal = userPrincipal; this.userPrincipal = userPrincipal;
this.sessionId = UUID.randomUUID().toString();
} }
public Collection<GrantedAuthority> getAuthorities() { public Collection<GrantedAuthority> getAuthorities() {
@ -71,4 +74,11 @@ public class SecurityUser extends User {
this.userPrincipal = userPrincipal; this.userPrincipal = userPrincipal;
} }
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
} }

10
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 IS_PUBLIC = "isPublic";
private static final String TENANT_ID = "tenantId"; private static final String TENANT_ID = "tenantId";
private static final String CUSTOMER_ID = "customerId"; private static final String CUSTOMER_ID = "customerId";
private static final String SESSION_ID = "sessionId";
private final JwtSettings settings; private final JwtSettings settings;
@ -119,6 +120,9 @@ public class JwtTokenFactory {
if (customerId != null) { if (customerId != null) {
securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId))); securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId)));
} }
if (claims.get(SESSION_ID, String.class) != null) {
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
}
UserPrincipal principal; UserPrincipal principal;
if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) {
@ -161,6 +165,9 @@ public class JwtTokenFactory {
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); 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 securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
securityUser.setUserPrincipal(principal); securityUser.setUserPrincipal(principal);
if (claims.get(SESSION_ID, String.class) != null) {
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
}
return securityUser; return securityUser;
} }
@ -183,6 +190,9 @@ public class JwtTokenFactory {
Claims claims = Jwts.claims().setSubject(principal.getValue()); Claims claims = Jwts.claims().setSubject(principal.getValue());
claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(USER_ID, securityUser.getId().getId().toString());
claims.put(SCOPES, scopes); claims.put(SCOPES, scopes);
if (securityUser.getSessionId() != null) {
claims.put(SESSION_ID, securityUser.getSessionId());
}
ZonedDateTime currentTime = ZonedDateTime.now(); ZonedDateTime currentTime = ZonedDateTime.now();

3
application/src/main/resources/thingsboard.yml

@ -421,7 +421,8 @@ cache:
timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}" timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}"
maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}" maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}"
usersUpdateTime: 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}" maxSize: "${CACHE_SPECS_USERS_UPDATE_TIME_MAX_SIZE:10000}"
otaPackages: otaPackages:
timeToLiveInMinutes: "${CACHE_SPECS_OTA_PACKAGES_TTL:60}" timeToLiveInMinutes: "${CACHE_SPECS_OTA_PACKAGES_TTL:60}"

12
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); customerUser = createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD);
customerUserId = customerUser.getId(); customerUserId = customerUser.getId();
logout(); resetTokens();
log.info("Executed web test setup"); log.info("Executed web test setup");
} }
@ -336,7 +336,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Assert.assertNotNull(savedDifferentCustomer); Assert.assertNotNull(savedDifferentCustomer);
differentCustomerId = savedDifferentCustomer.getId(); differentCustomerId = savedDifferentCustomer.getId();
logout(); resetTokens();
} }
protected void deleteDifferentTenant() throws Exception { protected void deleteDifferentTenant() throws Exception {
@ -350,7 +350,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected User createUserAndLogin(User user, String password) throws Exception { protected User createUserAndLogin(User user, String password) throws Exception {
User savedUser = doPost("/api/user", user, User.class); User savedUser = doPost("/api/user", user, User.class);
logout(); resetTokens();
JsonNode activateRequest = getActivateRequest(password); JsonNode activateRequest = getActivateRequest(password);
JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenInfo, user.getEmail()); validateAndSetJwtToken(tokenInfo, user.getEmail());
@ -411,12 +411,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Assert.assertEquals(username, subject); Assert.assertEquals(username, subject);
} }
protected void logout() throws Exception { protected void resetTokens() throws Exception {
this.token = null; this.token = null;
this.refreshToken = null; this.refreshToken = null;
this.username = null; this.username = null;
} }
protected void logout() throws Exception {
doPost("/api/auth/logout").andExpect(status().isOk());
}
protected void setJwtToken(MockHttpServletRequestBuilder request) { protected void setJwtToken(MockHttpServletRequestBuilder request) {
if (this.token != null) { if (this.token != null) {
request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token);

4
application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java

@ -77,7 +77,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
device.setCustomerId(customerId); device.setCustomerId(customerId);
customerDevice = doPost("/api/device", device, Device.class); customerDevice = doPost("/api/device", device, Device.class);
logout(); resetTokens();
} }
@After @After
@ -423,7 +423,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
testNotifyEntityNeverMsgToEdgeServiceOneTime(alarm, alarm.getId(), tenantId, ActionType.ADDED); testNotifyEntityNeverMsgToEdgeServiceOneTime(alarm, alarm.getId(), tenantId, ActionType.ADDED);
logout(); resetTokens();
JsonNode publicLoginRequest = JacksonUtil.toJsonNode("{\"publicId\": \"" + publicId + "\"}"); JsonNode publicLoginRequest = JacksonUtil.toJsonNode("{\"publicId\": \"" + publicId + "\"}");
JsonNode tokens = doPost("/api/auth/login/public", publicLoginRequest, JsonNode.class); JsonNode tokens = doPost("/api/auth/login/public", publicLoginRequest, JsonNode.class);

12
application/src/test/java/org/thingsboard/server/controller/BaseAuthControllerTest.java

@ -15,13 +15,15 @@
*/ */
package org.thingsboard.server.controller; 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.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 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 { public abstract class BaseAuthControllerTest extends AbstractControllerTest {
@Test @Test
@ -57,9 +59,13 @@ public abstract class BaseAuthControllerTest extends AbstractControllerTest {
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name()))) .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL))); .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(); logout();
doGet("/api/auth/user") doGet("/api/auth/user")
.andExpect(status().isUnauthorized()); .andExpect(status().isUnauthorized());
resetTokens();
} }
@Test @Test

8
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); ActionType.ADDED, ActionType.ADDED, 1, 1, 1);
Mockito.reset(tbClusterService, auditLogService); Mockito.reset(tbClusterService, auditLogService);
logout(); resetTokens();
doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
.andExpect(status().isSeeOther()) .andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); .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("$.authority", is(Authority.TENANT_ADMIN.name())))
.andExpect(jsonPath("$.email", is(email))); .andExpect(jsonPath("$.email", is(email)));
logout(); resetTokens();
login(email, "testPassword"); login(email, "testPassword");
@ -218,7 +218,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
user.setLastName("Downs"); user.setLastName("Downs");
User savedUser = createUserAndLogin(user, "testPassword1"); User savedUser = createUserAndLogin(user, "testPassword1");
logout(); resetTokens();
JsonNode resetPasswordByEmailRequest = new ObjectMapper().createObjectNode() JsonNode resetPasswordByEmailRequest = new ObjectMapper().createObjectNode()
.put("email", email); .put("email", email);
@ -244,7 +244,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
.andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name()))) .andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name())))
.andExpect(jsonPath("$.email", is(email))); .andExpect(jsonPath("$.email", is(email)));
logout(); resetTokens();
login(email, "testPassword2"); login(email, "testPassword2");
doGet("/api/auth/user") doGet("/api/auth/user")

151
application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java

@ -15,19 +15,28 @@
*/ */
package org.thingsboard.server.service.security.auth; package org.thingsboard.server.service.security.auth;
import org.junit.jupiter.api.BeforeEach; import org.junit.Before;
import org.junit.jupiter.api.Test; import org.junit.Test;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager; 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.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.User;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials; 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.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.dao.customer.CustomerService; 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.dao.user.UserService;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider;
@ -39,13 +48,9 @@ import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
import java.util.UUID; 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 java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse; 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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -53,31 +58,34 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; 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=60",
"cache.specs.usersUpdateTime.timeToLiveInMinutes=1"
})
public class TokenOutdatingTest { public class TokenOutdatingTest {
private JwtAuthenticationProvider accessTokenAuthenticationProvider; private JwtAuthenticationProvider accessTokenAuthenticationProvider;
private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider;
@Autowired
private TokenOutdatingService tokenOutdatingService; private TokenOutdatingService tokenOutdatingService;
private ConcurrentMapCacheManager cacheManager; @Autowired
private JwtTokenFactory tokenFactory; private JwtTokenFactory tokenFactory;
private JwtSettings jwtSettings; private SecurityUser securityUser;
private UserId userId; @Before
@BeforeEach
public void setUp() { public void setUp() {
jwtSettings = new JwtSettings(); UserId userId = new UserId(UUID.randomUUID());
jwtSettings.setTokenIssuer("test.io"); securityUser = createMockSecurityUser(userId);
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());
UserService userService = mock(UserService.class); UserService userService = mock(UserService.class);
@ -97,28 +105,28 @@ public class TokenOutdatingTest {
@Test @Test
public void testOutdateOldUserTokens() throws Exception { 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 SECONDS.sleep(1); // need to wait before outdating so that outdatage time is strictly after token issue time
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId)); assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
SECONDS.sleep(1); SECONDS.sleep(1);
JwtToken newJwtToken = tokenFactory.createAccessJwtToken(createMockSecurityUser(userId)); JwtToken newJwtToken = tokenFactory.createAccessJwtToken(securityUser);
assertFalse(tokenOutdatingService.isOutdated(newJwtToken, userId)); assertFalse(tokenOutdatingService.isOutdated(newJwtToken, securityUser.getId()));
} }
@Test @Test
public void testAuthenticateWithOutdatedAccessToken() throws InterruptedException { public void testAuthenticateWithOutdatedAccessToken() throws InterruptedException {
RawAccessJwtToken accessJwtToken = getRawJwtToken(createAccessJwtToken(userId)); RawAccessJwtToken accessJwtToken = getRawJwtToken(tokenFactory.createAccessJwtToken(securityUser));
assertDoesNotThrow(() -> { assertDoesNotThrow(() -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken));
}); });
SECONDS.sleep(1); SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertThrows(JwtExpiredTokenException.class, () -> { assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken));
@ -127,14 +135,14 @@ public class TokenOutdatingTest {
@Test @Test
public void testAuthenticateWithOutdatedRefreshToken() throws InterruptedException { public void testAuthenticateWithOutdatedRefreshToken() throws InterruptedException {
RawAccessJwtToken refreshJwtToken = getRawJwtToken(createRefreshJwtToken(userId)); RawAccessJwtToken refreshJwtToken = getRawJwtToken(tokenFactory.createRefreshToken(securityUser));
assertDoesNotThrow(() -> { assertDoesNotThrow(() -> {
refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken));
}); });
SECONDS.sleep(1); SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId()));
assertThrows(CredentialsExpiredException.class, () -> { assertThrows(CredentialsExpiredException.class, () -> {
refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken));
@ -143,33 +151,73 @@ public class TokenOutdatingTest {
@Test @Test
public void testTokensOutdatageTimeRemovalFromCache() throws Exception { public void testTokensOutdatageTimeRemovalFromCache() throws Exception {
JwtToken jwtToken = createAccessJwtToken(userId); JwtToken jwtToken = tokenFactory.createAccessJwtToken(securityUser);
SECONDS.sleep(1); SECONDS.sleep(1);
tokenOutdatingService.onUserAuthDataChanged(new UserAuthDataChangedEvent(userId)); tokenOutdatingService.onUserAuthDataChanged(new UserCredentialsInvalidationEvent(securityUser.getId()));
int refreshTokenExpirationTime = 3; SECONDS.sleep(1);
jwtSettings.setRefreshTokenExpTime(refreshTokenExpirationTime);
SECONDS.sleep(refreshTokenExpirationTime - 2);
assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId)); assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
assertNotNull(cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE).get(userId.getId().toString()));
SECONDS.sleep(3); SECONDS.sleep(60);
assertFalse(tokenOutdatingService.isOutdated(jwtToken, userId)); assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
assertNull(cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE).get(userId.getId().toString()));
} }
private JwtToken createAccessJwtToken(UserId userId) { @Test
return tokenFactory.createAccessJwtToken(createMockSecurityUser(userId)); 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 UserSessionInvalidationEvent(securityUser.getSessionId()));
assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(jwtToken)));
});
assertDoesNotThrow(() -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(getRawJwtToken(anotherJwtToken)));
});
} }
private JwtToken createRefreshJwtToken(UserId userId) { @Test
return tokenFactory.createRefreshToken(createMockSecurityUser(userId)); 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) { private RawAccessJwtToken getRawJwtToken(JwtToken token) {
return new RawAccessJwtToken(token.getToken()); return new RawAccessJwtToken(token.getToken());
} }
@ -180,6 +228,7 @@ public class TokenOutdatingTest {
securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail()));
securityUser.setAuthority(Authority.CUSTOMER_USER); securityUser.setAuthority(Authority.CUSTOMER_USER);
securityUser.setId(userId); securityUser.setId(userId);
securityUser.setSessionId(UUID.randomUUID().toString());
return securityUser; return securityUser;
} }
} }

34
common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationCaffeineCache.java

@ -0,0 +1,34 @@
/**
* 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;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("UsersSessionInvalidation")
public class UsersSessionInvalidationCaffeineCache extends CaffeineTbTransactionalCache<String, Long> {
@Autowired
public UsersSessionInvalidationCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.USERS_SESSION_INVALIDATION_CACHE);
}
}

36
common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersSessionInvalidationRedisCache.java

@ -0,0 +1,36 @@
/**
* 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;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("UsersSessionInvalidation")
public class UsersSessionInvalidationRedisCache extends RedisTbTransactionalCache<String, Long> {
@Autowired
public UsersSessionInvalidationRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.USERS_SESSION_INVALIDATION_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>());
}
}

2
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 ASSET_PROFILE_CACHE = "assetProfiles";
public static final String ATTRIBUTES_CACHE = "attributes"; 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_CACHE = "otaPackages";
public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData"; public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData";
public static final String REPOSITORY_SETTINGS_CACHE = "repositorySettings"; public static final String REPOSITORY_SETTINGS_CACHE = "repositorySettings";

16
common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java

@ -15,17 +15,9 @@
*/ */
package org.thingsboard.server.common.data.security.event; package org.thingsboard.server.common.data.security.event;
import lombok.Data; import java.io.Serializable;
import org.thingsboard.server.common.data.id.UserId;
@Data
public class UserAuthDataChangedEvent {
private final UserId userId;
private final long ts;
public UserAuthDataChangedEvent(UserId userId) {
this.userId = userId;
this.ts = System.currentTimeMillis();
}
public abstract class UserAuthDataChangedEvent implements Serializable {
public abstract String getId();
public abstract long getTs();
} }

40
common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserCredentialsInvalidationEvent.java

@ -0,0 +1,40 @@
/**
* 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 org.thingsboard.server.common.data.id.UserId;
@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;
}
}

39
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;
}
}

3
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.page.PageLink;
import org.thingsboard.server.common.data.security.UserCredentials; 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.UserAuthDataChangedEvent;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
@ -219,7 +220,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
userAuthSettingsDao.removeByUserId(userId); userAuthSettingsDao.removeByUserId(userId);
deleteEntityRelations(tenantId, userId); deleteEntityRelations(tenantId, userId);
userDao.removeById(tenantId, userId.getId()); userDao.removeById(tenantId, userId.getId());
eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId)); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId));
} }
@Override @Override

Loading…
Cancel
Save