Browse Source

Refactor

pull/7435/head
oyurov 4 years ago
parent
commit
d10289253f
  1. 18
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  2. 9
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  3. 54
      application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java
  4. 38
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenRepository.java
  5. 11
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  6. 5
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  7. 10
      application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
  8. 6
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  9. 101
      application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java
  10. 39
      common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UserUpdateTimeRedisCache.java
  11. 25
      common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCacheEvictEvent.java
  12. 37
      common/cache/src/main/java/org/thingsboard/server/cache/usersUpdateTime/UsersUpdateTimeCaffeineCache.java
  13. 10
      common/data/src/main/java/org/thingsboard/server/common/data/security/event/UserAuthDataChangedEvent.java
  14. 2
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

18
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.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.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 +71,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 +125,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 UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
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 +265,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 +303,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 UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
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,7 +359,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 UserAuthDataChangedEvent(user.getId())); eventPublisher.publishEvent(new UserAuthDataChangedEvent(user.getId(), user.getSessionId(), false));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }

9
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.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.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 +93,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 +160,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 +371,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 UserAuthDataChangedEvent(userId, null, true));
} }
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);

54
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 io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; 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.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.config.JwtSettings;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import javax.annotation.PostConstruct; import java.util.HashMap;
import java.util.Optional; import java.util.Optional;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
@ -36,30 +36,24 @@ import static java.util.concurrent.TimeUnit.SECONDS;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class TokenOutdatingService { public class TokenOutdatingService extends AbstractCachedEntityService<UserId, HashMap<String, Long>, UsersUpdateTimeCacheEvictEvent> {
private final CacheManager cacheManager;
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final JwtSettings jwtSettings; private final JwtSettings jwtSettings;
private Cache usersUpdateTimeCache;
@PostConstruct
protected void initCache() {
usersUpdateTimeCache = cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE);
}
@EventListener(classes = UserAuthDataChangedEvent.class) @EventListener(classes = UserAuthDataChangedEvent.class)
public void onUserAuthDataChanged(UserAuthDataChangedEvent event) { public void onUserAuthDataChanged(UserAuthDataChangedEvent event) {
usersUpdateTimeCache.put(toKey(event.getUserId()), event.getTs()); processUserSessions(event);
} }
public boolean isOutdated(JwtToken token, UserId userId) { public boolean isOutdated(JwtToken token, UserId userId) {
Claims claims = tokenFactory.parseTokenClaims(token).getBody(); Claims claims = tokenFactory.parseTokenClaims(token).getBody();
long issueTime = claims.getIssuedAt().getTime(); 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 -> { .map(outdatageTime -> {
if (System.currentTimeMillis() - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) { if (outdatageTime.get().get(sessionId) != null && System.currentTimeMillis() - outdatageTime.get().get(sessionId) <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) {
return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime.get().get(sessionId));
} else { } else {
/* /*
* Means that since the outdating has passed more than * 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 * as all the tokens issued before the outdatage time
* are now expired by themselves * are now expired by themselves
* */ * */
usersUpdateTimeCache.evict(toKey(userId)); handleEvictEvent(new UsersUpdateTimeCacheEvictEvent(userId, sessionId));
return false; return false;
} }
}) })
.orElse(false); .orElse(false);
} }
private String toKey(UserId userId) { @TransactionalEventListener(classes = UsersUpdateTimeCacheEvictEvent.class)
return userId.getId().toString(); @Override
public void handleEvictEvent(UsersUpdateTimeCacheEvictEvent event) {
HashMap<String, Long> 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<String, Long> 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());
}});
}
} }
} }

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;
@ -50,7 +49,6 @@ import java.util.UUID;
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;
@ -59,14 +57,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;
@ -97,11 +93,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;
}
} }

6
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,7 @@ public class JwtTokenFactory {
if (customerId != null) { if (customerId != null) {
securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId))); securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId)));
} }
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 +163,7 @@ 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);
securityUser.setSessionId(claims.get(SESSION_ID, String.class));
return securityUser; return securityUser;
} }
@ -183,6 +186,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();

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

@ -15,19 +15,27 @@
*/ */
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.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.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 +47,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 +57,33 @@ 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=10"
})
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 +103,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 UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
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 UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
assertThrows(JwtExpiredTokenException.class, () -> { assertThrows(JwtExpiredTokenException.class, () -> {
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken));
@ -127,14 +133,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 UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
assertThrows(CredentialsExpiredException.class, () -> { assertThrows(CredentialsExpiredException.class, () -> {
refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken));
@ -143,32 +149,20 @@ 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 UserAuthDataChangedEvent(securityUser.getId(), securityUser.getSessionId(), false));
int refreshTokenExpirationTime = 3;
jwtSettings.setRefreshTokenExpTime(refreshTokenExpirationTime);
SECONDS.sleep(refreshTokenExpirationTime - 2); SECONDS.sleep(1);
assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId));
assertNotNull(cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE).get(userId.getId().toString()));
SECONDS.sleep(3); assertTrue(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
assertFalse(tokenOutdatingService.isOutdated(jwtToken, userId)); SECONDS.sleep(10);
assertNull(cacheManager.getCache(CacheConstants.USERS_UPDATE_TIME_CACHE).get(userId.getId().toString()));
}
private JwtToken createAccessJwtToken(UserId userId) { assertFalse(tokenOutdatingService.isOutdated(jwtToken, securityUser.getId()));
return tokenFactory.createAccessJwtToken(createMockSecurityUser(userId));
} }
private JwtToken createRefreshJwtToken(UserId userId) {
return tokenFactory.createRefreshToken(createMockSecurityUser(userId));
}
private RawAccessJwtToken getRawJwtToken(JwtToken token) { private RawAccessJwtToken getRawJwtToken(JwtToken token) {
return new RawAccessJwtToken(token.getToken()); return new RawAccessJwtToken(token.getToken());
@ -180,6 +174,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;
} }
} }

39
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<UserId, HashMap<String, Long>> {
@Autowired
public UserUpdateTimeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.USERS_UPDATE_TIME_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>());
}
}

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

37
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<UserId, HashMap<String, Long>> {
@Autowired
public UsersUpdateTimeCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.USERS_UPDATE_TIME_CACHE);
}
}

10
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 lombok.Data;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import java.io.Serializable;
@Data @Data
public class UserAuthDataChangedEvent { public class UserAuthDataChangedEvent implements Serializable {
private final UserId userId; private final UserId userId;
private final String sessionId;
private final long ts; private final long ts;
private final boolean dropAllSessions;
public UserAuthDataChangedEvent(UserId userId) { public UserAuthDataChangedEvent(UserId userId, String sessionId, boolean dropAllSessions) {
this.userId = userId; this.userId = userId;
this.sessionId = sessionId;
this.dropAllSessions = dropAllSessions;
this.ts = System.currentTimeMillis(); this.ts = System.currentTimeMillis();
} }

2
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); 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 UserAuthDataChangedEvent(userId, null, true));
} }
@Override @Override

Loading…
Cancel
Save