From e32797400c522591dd4525867ed05f4928fe34f5 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 29 Apr 2024 15:18:37 +0200 Subject: [PATCH 01/15] updated jjwt version --- .../install/ThingsboardInstallService.java | 1 + .../DefaultSystemDataLoaderService.java | 117 ++++++++---------- .../install/SystemDataLoaderService.java | 2 + .../settings/DefaultJwtSettingsService.java | 24 +--- .../settings/DefaultJwtSettingsValidator.java | 4 +- .../auth/jwt/settings/JwtSettingsService.java | 2 - .../security/model/token/JwtTokenFactory.java | 40 +++--- .../model/token/OAuth2AppTokenFactory.java | 6 +- .../server/controller/AbstractWebTest.java | 15 +-- .../controller/AdminControllerTest.java | 4 +- .../security/auth/JwtTokenFactoryTest.java | 35 +----- .../data/security/model/JwtSettings.java | 2 +- .../notification/DefaultNotifications.java | 9 -- dao/src/test/resources/sql/system-data.sql | 8 ++ pom.xml | 2 +- 15 files changed, 115 insertions(+), 156 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index eb9d908bab..cdb808ade9 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -133,6 +133,7 @@ public class ThingsboardInstallService { case "3.6.4": log.info("Upgrading ThingsBoard from version 3.6.4 to 3.7.0 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.6.4"); + systemDataLoaderService.updateJwtSettings(); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 4da6a383e8..4cd1b7a12f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -19,13 +19,17 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Profile; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -81,6 +85,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; @@ -100,14 +105,11 @@ import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.dao.widget.WidgetTypeService; -import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; -import jakarta.annotation.Nullable; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.TreeMap; @@ -117,80 +119,42 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; +import static org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT; @Service @Profile("install") @Slf4j +@RequiredArgsConstructor public class DefaultSystemDataLoaderService implements SystemDataLoaderService { public static final String CUSTOMER_CRED = "customer"; public static final String ACTIVITY_STATE = "active"; - @Autowired - private InstallScripts installScripts; + private final InstallScripts installScripts; + private final UserService userService; + private final AdminSettingsService adminSettingsService; + private final TenantService tenantService; + private final TenantProfileService tenantProfileService; + private final CustomerService customerService; + private final DeviceService deviceService; + private final DeviceProfileService deviceProfileService; + private final AttributesService attributesService; + private final DeviceCredentialsService deviceCredentialsService; + private final RuleChainService ruleChainService; + private final TimeseriesService tsService; + private final DeviceConnectivityConfiguration connectivityConfiguration; + private final QueueService queueService; + private final JwtSettingsService jwtSettingsService; + private final NotificationSettingsService notificationSettingsService; + private final NotificationTargetService notificationTargetService; @Autowired private BCryptPasswordEncoder passwordEncoder; - @Autowired - private UserService userService; - - @Autowired - private AdminSettingsService adminSettingsService; - - @Autowired - private WidgetTypeService widgetTypeService; - - @Autowired - private WidgetsBundleService widgetsBundleService; - - @Autowired - private TenantService tenantService; - - @Autowired - private TenantProfileService tenantProfileService; - - @Autowired - private CustomerService customerService; - - @Autowired - private DeviceService deviceService; - - @Autowired - private DeviceProfileService deviceProfileService; - - @Autowired - private AttributesService attributesService; - - @Autowired - private DeviceCredentialsService deviceCredentialsService; - - @Autowired - private RuleChainService ruleChainService; - - @Autowired - private TimeseriesService tsService; - - @Autowired - private DeviceConnectivityConfiguration connectivityConfiguration; - @Value("${state.persistToTelemetry:false}") @Getter private boolean persistActivityToTelemetry; - @Lazy - @Autowired - private QueueService queueService; - - @Autowired - private JwtSettingsService jwtSettingsService; - - @Autowired - private NotificationSettingsService notificationSettingsService; - - @Autowired - private NotificationTargetService notificationTargetService; - @Bean protected BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -298,6 +262,33 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { jwtSettingsService.createRandomJwtSettings(); } + @Override + public void updateJwtSettings() { + JwtSettings jwtSettings = jwtSettingsService.getJwtSettings(); + + boolean invalidSignKey = false; + + if (TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { + log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + + "Added new temporary JWT Signing Key. " + + "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator."); + invalidSignKey = true; + } else if (Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()).length * Byte.SIZE < 512) { + log.warn("WARNING: The platform is configured to use JWT Signing Key with length less then 512 bits of data. " + + "Added new temporary JWT Signing Key. " + + "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator."); + invalidSignKey = true; + } + + if (invalidSignKey) { + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + jwtSettingsService.saveJwtSettings(jwtSettings); + } + } + @Override public void createOAuth2Templates() throws Exception { installScripts.createOAuth2Templates(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index eeac1b6aa4..366ae0bda6 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -25,6 +25,8 @@ public interface SystemDataLoaderService { void createRandomJwtSettings() throws Exception; + void updateJwtSettings() throws Exception; + void createOAuth2Templates() throws Exception; void loadSystemWidgets() throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index e5143a6023..dd7cd8ae04 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -21,14 +21,11 @@ import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.model.JwtSettings; -import org.thingsboard.server.dao.notification.DefaultNotifications; import org.thingsboard.server.dao.settings.AdminSettingsService; import java.nio.charset.StandardCharsets; @@ -43,7 +40,6 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final AdminSettingsService adminSettingsService; private final Optional tbClusterService; - private final Optional notificationCenter; private final JwtSettingsValidator jwtSettingsValidator; @Value("${security.jwt.tokenExpirationTime:9000}") @@ -75,17 +71,6 @@ public class DefaultJwtSettingsService implements JwtSettingsService { } } - /** - * Create JWT admin settings is intended to be called from Upgrade scripts only - */ - @Override - public void saveLegacyYmlSettings() { - log.info("Saving legacy JWT admin settings from YML..."); - if (getJwtSettingsFromDb() == null) { - saveJwtSettings(getJwtSettingsFromYml()); - } - } - @Override public JwtSettings saveJwtSettings(JwtSettings jwtSettings) { jwtSettingsValidator.validate(jwtSettings); @@ -123,14 +108,6 @@ public class DefaultJwtSettingsService implements JwtSettingsService { result = getJwtSettingsFromYml(); log.warn("Loading the JWT settings from YML since there are no settings in DB. Looks like the upgrade script was not applied."); } - if (isSigningKeyDefault(result)) { - log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + - "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + - "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator."); - notificationCenter.ifPresent(notificationCenter -> { - notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(), DefaultNotifications.jwtSigningKeyIssue.toTemplate()); - }); - } this.jwtSettings = result; } } @@ -138,6 +115,7 @@ public class DefaultJwtSettingsService implements JwtSettingsService { return this.jwtSettings; } + @Deprecated(since = "3.7.0", forRemoval = true) private JwtSettings getJwtSettingsFromYml() { return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index b807f65d36..4ca02a1add 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -59,8 +59,8 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator { if (Arrays.isNullOrEmpty(decodedKey)) { throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); } - if (decodedKey.length * Byte.SIZE < 256 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { - throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!"); + if (decodedKey.length * Byte.SIZE < 512 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { + throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 512 bits of data!"); } System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java index 19095ad0b9..12b1d017c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java @@ -28,8 +28,6 @@ public interface JwtSettingsService { void createRandomJwtSettings(); - void saveLegacyYmlSettings(); - JwtSettings saveJwtSettings(JwtSettings jwtSettings); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 08622f578a..abdcf76888 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -16,14 +16,15 @@ package org.thingsboard.server.service.security.model.token; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ClaimsBuilder; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.BadCredentialsException; @@ -41,7 +42,10 @@ import org.thingsboard.server.service.security.exception.JwtExpiredTokenExceptio import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import java.time.ZonedDateTime; +import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.List; @@ -95,7 +99,7 @@ public class JwtTokenFactory { public SecurityUser parseAccessJwtToken(String token) { Jws jwsClaims = parseTokenClaims(token); - Claims claims = jwsClaims.getBody(); + Claims claims = jwsClaims.getPayload(); String subject = claims.getSubject(); @SuppressWarnings("unchecked") List scopes = claims.get(SCOPES, List.class); @@ -140,14 +144,14 @@ public class JwtTokenFactory { String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), jwtSettingsService.getJwtSettings().getRefreshTokenExpTime()) .claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID) - .setId(UUID.randomUUID().toString()).compact(); + .id(UUID.randomUUID().toString()).compact(); return new AccessJwtToken(token); } public SecurityUser parseRefreshToken(String token) { Jws jwsClaims = parseTokenClaims(token); - Claims claims = jwsClaims.getBody(); + Claims claims = jwsClaims.getPayload(); String subject = claims.getSubject(); @SuppressWarnings("unchecked") List scopes = claims.get(SCOPES, List.class); @@ -183,28 +187,29 @@ public class JwtTokenFactory { UserPrincipal principal = securityUser.getUserPrincipal(); - Claims claims = Jwts.claims().setSubject(principal.getValue()); - claims.put(USER_ID, securityUser.getId().getId().toString()); - claims.put(SCOPES, scopes); + ClaimsBuilder claimsBuilder = Jwts.claims().subject(principal.getValue()); + claimsBuilder.add(USER_ID, securityUser.getId().getId().toString()); + claimsBuilder.add(SCOPES, scopes); if (securityUser.getSessionId() != null) { - claims.put(SESSION_ID, securityUser.getSessionId()); + claimsBuilder.add(SESSION_ID, securityUser.getSessionId()); } ZonedDateTime currentTime = ZonedDateTime.now(); return Jwts.builder() - .setClaims(claims) - .setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) - .setIssuedAt(Date.from(currentTime.toInstant())) - .setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) - .signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey()); + .claims(claimsBuilder.build()) + .issuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) + .issuedAt(Date.from(currentTime.toInstant())) + .expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) + .signWith(toSecretKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()), Jwts.SIG.HS512); } public Jws parseTokenClaims(String token) { try { return Jwts.parser() - .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()) - .parseClaimsJws(token); + .verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey()))) + .build() + .parseSignedClaims(token); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { log.debug("Invalid JWT Token", ex); throw new BadCredentialsException("Invalid JWT token: ", ex); @@ -220,4 +225,9 @@ public class JwtTokenFactory { return new JwtPair(accessToken.getToken(), refreshToken.getToken()); } + private SecretKey toSecretKey(String base64Key) { + byte[] decodedToken = Base64.getDecoder().decode(base64Key); + return new SecretKeySpec(decodedToken, "HmacSHA512"); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java index 7f956f6970..cf01dec208 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java @@ -22,10 +22,12 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; +import java.util.Base64; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -40,14 +42,14 @@ public class OAuth2AppTokenFactory { public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) { Jws jwsClaims; try { - jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken); + jwsClaims = Jwts.parser().verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(appSecret))).build().parseSignedClaims(appToken); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { throw new IllegalArgumentException("Invalid Application token: ", ex); } catch (ExpiredJwtException expiredEx) { throw new IllegalArgumentException("Application token expired", expiredEx); } - Claims claims = jwsClaims.getBody(); + Claims claims = jwsClaims.getPayload(); Date expiration = claims.getExpiration(); if (expiration == null) { throw new IllegalArgumentException("Application token must have expiration date"); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index e14b041e62..b626f7f959 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; @@ -120,7 +121,9 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; +import javax.crypto.SecretKey; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; @@ -237,6 +240,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired protected ClaimDevicesService claimDevicesService; + @Autowired + private JwtTokenFactory jwtTokenFactory; + @SpyBean protected MailService mailService; @@ -558,13 +564,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected void validateJwtToken(String token, String username) { - Assert.assertNotNull(token); - Assert.assertFalse(token.isEmpty()); - int i = token.lastIndexOf('.'); - Assert.assertTrue(i > 0); - String withoutSignature = token.substring(0, i + 1); - Jwt jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature); - Claims claims = jwsClaims.getBody(); + Jws jwsClaims = jwtTokenFactory.parseTokenClaims(token); + Claims claims = jwsClaims.getPayload(); String subject = claims.getSubject(); Assert.assertEquals(username, subject); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java index 430ff2eb09..8876bf8c24 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java @@ -42,7 +42,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Slf4j @DaoSqlTest public class AdminControllerTest extends AbstractControllerTest { - final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey"); + final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "QmlicmJkZk9tSzZPVFozcWY0Sm94UVhybmtBWXZ5YmZMOUZSZzZvcUFiOVhsb3VHUThhUWJGaXp3UHhtcGZ6Tw=="); @Test public void testFindAdminSettingsByKey() throws Exception { @@ -168,7 +168,7 @@ public class AdminControllerTest extends AbstractControllerTest { assertThat(jwtSettings).isEqualTo(defaultJwtSettings); jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( - RandomStringUtils.randomAlphanumeric(256 / Byte.SIZE).getBytes(StandardCharsets.UTF_8))); + RandomStringUtils.randomAlphanumeric(512 / Byte.SIZE).getBytes(StandardCharsets.UTF_8))); doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk()); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 2d39dd9905..75868e6508 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -16,16 +16,14 @@ package org.thingsboard.server.service.security.auth; import io.jsonwebtoken.Claims; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.Before; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; -import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.security.model.JwtToken; @@ -38,6 +36,8 @@ import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.AccessJwtToken; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Calendar; import java.util.Date; import java.util.Optional; @@ -45,19 +45,13 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class JwtTokenFactoryTest { private JwtTokenFactory tokenFactory; private AdminSettingsService adminSettingsService; - private NotificationCenter notificationCenter; private JwtSettingsService jwtSettingsService; private JwtSettings jwtSettings; @@ -66,12 +60,11 @@ public class JwtTokenFactoryTest { public void beforeEach() { jwtSettings = new JwtSettings(); jwtSettings.setTokenIssuer("tb"); - jwtSettings.setTokenSigningKey("abewafaf"); + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2)); jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7)); adminSettingsService = mock(AdminSettingsService.class); - notificationCenter = mock(NotificationCenter.class); jwtSettingsService = mockJwtSettingsService(); mockJwtSettings(jwtSettings); @@ -169,21 +162,6 @@ public class JwtTokenFactoryTest { }); } - @Test - public void testJwtSigningKeyIssueNotification() { - JwtSettings badJwtSettings = jwtSettings; - badJwtSettings.setTokenSigningKey(JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT); - mockJwtSettings(badJwtSettings); - jwtSettingsService = mockJwtSettingsService(); - - for (int i = 0; i < 5; i++) { // to check if notification is not sent twice - jwtSettingsService.getJwtSettings(); - } - verify(notificationCenter, times(1)).sendGeneralWebNotification(eq(TenantId.SYS_TENANT_ID), - isA(SystemAdministratorsFilter.class), argThat(template -> template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.WEB) - .getBody().contains("The platform is configured to use default JWT Signing Key"))); - } - private void mockJwtSettings(JwtSettings settings) { AdminSettings adminJwtSettings = new AdminSettings(); adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(settings)); @@ -192,12 +170,11 @@ public class JwtTokenFactoryTest { } private DefaultJwtSettingsService mockJwtSettingsService() { - return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), - Optional.of(notificationCenter), new DefaultJwtSettingsValidator()); + return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator()); } private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) { - Claims claims = tokenFactory.parseTokenClaims(jwtToken.getToken()).getBody(); + Claims claims = tokenFactory.parseTokenClaims(jwtToken.getToken()).getPayload(); assertThat(claims.getExpiration()).matches(actualExpirationTime -> { Calendar expirationTime = Calendar.getInstance(); expirationTime.setTime(new Date()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java index d8368f247b..07c158830f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java @@ -48,7 +48,7 @@ public class JwtSettings { * Key is used to sign {@link JwtToken}. * Base64 encoded */ - @Schema(description = "The JWT key is used to sing token. Base64 encoded.", example = "cTU4WnNqemI2aU5wbWVjdm1vYXRzanhjNHRUcXliMjE=") + @Schema(description = "The JWT key is used to sing token. Base64 encoded.", example = "dkVTUzU2M2VMWUNwVVltTUhQU2o5SUM0Tkc3M0k2Ykdwcm85QTl6R0RaQ252OFlmVDk2OEptZXBNcndGeExFZg==") private String tokenSigningKey; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index fd17618bd1..22b8b39f2b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -358,15 +358,6 @@ public class DefaultNotifications { .build()) .build(); - public static final DefaultNotification jwtSigningKeyIssue = DefaultNotification.builder() - .name("JWT Signing Key issue notification") - .type(NotificationType.GENERAL) - .subject("WARNING: security issue") - .text("The platform is configured to use default JWT Signing Key. Please change it on the security settings page") - .icon("warning").color(YELLOW_COLOR) - .button("Go to settings").link("/security-settings/general") - .build(); - private final NotificationTemplateService templateService; private final NotificationRuleService ruleService; diff --git a/dao/src/test/resources/sql/system-data.sql b/dao/src/test/resources/sql/system-data.sql index 7a79f1114d..0b17d4f108 100644 --- a/dao/src/test/resources/sql/system-data.sql +++ b/dao/src/test/resources/sql/system-data.sql @@ -53,6 +53,14 @@ VALUES ( '23199d80-6e7e-11ee-8829-ef9fd52a6141', 1697719852888, '13814000-1dd2-1 "coaps":{"enabled":false,"host":"","port":"5684"} }' ); +INSERT INTO admin_settings ( id, created_time, tenant_id, key, json_value ) +VALUES ( '1e33c6f0-061e-11ef-b5b7-dba0ee077a1b', 1714391189727, '13814000-1dd2-11b2-8080-808080808080', 'jwt', '{ + "tokenExpirationTime": "9000", + "refreshTokenExpTime": "604800", + "tokenIssuer": "thingsboard.io", + "tokenSigningKey": "QmlicmJkZk9tSzZPVFozcWY0Sm94UVhybmtBWXZ5YmZMOUZSZzZvcUFiOVhsb3VHUThhUWJGaXp3UHhtcGZ6Tw==" +}' ); + INSERT INTO queue ( id, created_time, tenant_id, name, topic, poll_interval, partitions, consumer_per_partition, pack_processing_timeout, submit_strategy, processing_strategy ) VALUES ( '6eaaefa6-4612-11e7-a919-92ebcb67fe33', 1592576748000 ,'13814000-1dd2-11b2-8080-808080808080', 'Main' ,'tb_rule_engine.main', 25, 10, true, 2000, '{"type": "BURST", "batchSize": 1000}', diff --git a/pom.xml b/pom.xml index c13c25566b..1053768af3 100755 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ 6.2.4 6.2.4 5.1.2 - 0.9.1 + 0.12.5 2.0.13 2.23.1 1.5.5 From 997aba3f856b59862e72b5c25171e4a15f9a1b15 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 29 Apr 2024 15:43:06 +0200 Subject: [PATCH 02/15] added length check for createRandomJwtSettings --- .../security/auth/jwt/settings/DefaultJwtSettingsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index dd7cd8ae04..f7922f6a68 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -61,7 +61,7 @@ public class DefaultJwtSettingsService implements JwtSettingsService { if (getJwtSettingsFromDb() == null) { log.info("Creating JWT admin settings..."); this.jwtSettings = getJwtSettingsFromYml(); - if (isSigningKeyDefault(jwtSettings)) { + if (isSigningKeyDefault(jwtSettings) || Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()).length * Byte.SIZE < 512) { this.jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); } From 1a61e4d0649416b33a20546e6b2e2cc74ea664ae Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 29 Apr 2024 15:46:20 +0200 Subject: [PATCH 03/15] removed unnecessary comment --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1053768af3..2a9afbba7a 100755 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ 6.2.4 6.2.4 5.1.2 - 0.12.5 + 0.12.5 2.0.13 2.23.1 1.5.5 From 6fd0e13516def708c834096bea0cb178ca27d142 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 29 Apr 2024 16:18:49 +0200 Subject: [PATCH 04/15] removed deprecation from getJwtSettingsFromYml --- .../security/auth/jwt/settings/DefaultJwtSettingsService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index f7922f6a68..cfb540fbf1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -115,7 +115,6 @@ public class DefaultJwtSettingsService implements JwtSettingsService { return this.jwtSettings; } - @Deprecated(since = "3.7.0", forRemoval = true) private JwtSettings getJwtSettingsFromYml() { return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); } From e993ec8aa483b31f073a1e0d64fb281886c65b16 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 2 May 2024 13:42:47 +0200 Subject: [PATCH 05/15] minor refactoring --- .../DefaultSystemDataLoaderService.java | 28 +++++++++-- .../settings/DefaultJwtSettingsService.java | 48 +++---------------- .../settings/DefaultJwtSettingsValidator.java | 4 +- .../auth/jwt/settings/JwtSettingsService.java | 2 - .../security/model/token/JwtTokenFactory.java | 7 +-- .../server/controller/AbstractWebTest.java | 4 -- 6 files changed, 38 insertions(+), 55 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index fbbc73ab82..fbfbd9e87b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -119,7 +119,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; -import static org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT; +import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault; +import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.validateTokenSigningKeyLength; @Service @Profile("install") @@ -155,6 +156,15 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Getter private boolean persistActivityToTelemetry; + @Value("${security.jwt.tokenExpirationTime:9000}") + private Integer tokenExpirationTime; + @Value("${security.jwt.refreshTokenExpTime:604800}") + private Integer refreshTokenExpTime; + @Value("${security.jwt.tokenIssuer:thingsboard.io}") + private String tokenIssuer; + @Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}") + private String tokenSigningKey; + @Bean protected BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -259,7 +269,17 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Override public void createRandomJwtSettings() throws Exception { - jwtSettingsService.createRandomJwtSettings(); + if (jwtSettingsService.getJwtSettings() == null) { + log.info("Creating JWT admin settings..."); + var jwtSettings = new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); + if (isSigningKeyDefault(jwtSettings) || !validateTokenSigningKeyLength(jwtSettings)) { + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + } + jwtSettingsService.saveJwtSettings(jwtSettings); + } else { + log.info("Skip creating JWT admin settings because they already exist."); + } } @Override @@ -268,13 +288,13 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { boolean invalidSignKey = false; - if (TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { + if (isSigningKeyDefault(jwtSettings)) { log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + "Added new temporary JWT Signing Key. " + "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator."); invalidSignKey = true; - } else if (Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()).length * Byte.SIZE < 512) { + } else if (!validateTokenSigningKeyLength(jwtSettings)) { log.warn("WARNING: The platform is configured to use JWT Signing Key with length less then 512 bits of data. " + "Added new temporary JWT Signing Key. " + "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index cfb540fbf1..92019341c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -15,10 +15,9 @@ */ package org.thingsboard.server.service.security.auth.jwt.settings; +import io.jsonwebtoken.Jwts; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; @@ -28,7 +27,6 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.settings.AdminSettingsService; -import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Objects; import java.util.Optional; @@ -42,35 +40,8 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final Optional tbClusterService; private final JwtSettingsValidator jwtSettingsValidator; - @Value("${security.jwt.tokenExpirationTime:9000}") - private Integer tokenExpirationTime; - @Value("${security.jwt.refreshTokenExpTime:604800}") - private Integer refreshTokenExpTime; - @Value("${security.jwt.tokenIssuer:thingsboard.io}") - private String tokenIssuer; - @Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}") - private String tokenSigningKey; - private volatile JwtSettings jwtSettings = null; //lazy init - /** - * Create JWT admin settings is intended to be called from Install scripts only - */ - @Override - public void createRandomJwtSettings() { - if (getJwtSettingsFromDb() == null) { - log.info("Creating JWT admin settings..."); - this.jwtSettings = getJwtSettingsFromYml(); - if (isSigningKeyDefault(jwtSettings) || Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()).length * Byte.SIZE < 512) { - this.jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( - RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); - } - saveJwtSettings(jwtSettings); - } else { - log.info("Skip creating JWT admin settings because they already exist."); - } - } - @Override public JwtSettings saveJwtSettings(JwtSettings jwtSettings) { jwtSettingsValidator.validate(jwtSettings); @@ -103,22 +74,13 @@ public class DefaultJwtSettingsService implements JwtSettingsService { if (this.jwtSettings == null || forceReload) { synchronized (this) { if (this.jwtSettings == null || forceReload) { - JwtSettings result = getJwtSettingsFromDb(); - if (result == null) { - result = getJwtSettingsFromYml(); - log.warn("Loading the JWT settings from YML since there are no settings in DB. Looks like the upgrade script was not applied."); - } - this.jwtSettings = result; + jwtSettings = getJwtSettingsFromDb(); } } } return this.jwtSettings; } - private JwtSettings getJwtSettingsFromYml() { - return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); - } - private JwtSettings getJwtSettingsFromDb() { AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); return adminJwtSettings != null ? mapAdminToJwtSettings(adminJwtSettings) : null; @@ -138,8 +100,12 @@ public class DefaultJwtSettingsService implements JwtSettingsService { return adminJwtSettings; } - private boolean isSigningKeyDefault(JwtSettings settings) { + public static boolean isSigningKeyDefault(JwtSettings settings) { return TOKEN_SIGNING_KEY_DEFAULT.equals(settings.getTokenSigningKey()); } + public static boolean validateTokenSigningKeyLength(JwtSettings settings) { + return Base64.getDecoder().decode(settings.getTokenSigningKey()).length * Byte.SIZE >= Jwts.SIG.HS512.getKeyBitLength(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index 4ca02a1add..1de9e2c0c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -27,6 +27,8 @@ import java.util.Base64; import java.util.Optional; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault; + @Component @RequiredArgsConstructor public class DefaultJwtSettingsValidator implements JwtSettingsValidator { @@ -59,7 +61,7 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator { if (Arrays.isNullOrEmpty(decodedKey)) { throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); } - if (decodedKey.length * Byte.SIZE < 512 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { + if (decodedKey.length * Byte.SIZE < 512 && !isSigningKeyDefault(jwtSettings)) { throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 512 bits of data!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java index 12b1d017c2..d3aa261b00 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java @@ -26,8 +26,6 @@ public interface JwtSettingsService { JwtSettings reloadJwtSettings(); - void createRandomJwtSettings(); - JwtSettings saveJwtSettings(JwtSettings jwtSettings); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index abdcf76888..6be020b1c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -187,9 +187,10 @@ public class JwtTokenFactory { UserPrincipal principal = securityUser.getUserPrincipal(); - ClaimsBuilder claimsBuilder = Jwts.claims().subject(principal.getValue()); - claimsBuilder.add(USER_ID, securityUser.getId().getId().toString()); - claimsBuilder.add(SCOPES, scopes); + ClaimsBuilder claimsBuilder = Jwts.claims() + .subject(principal.getValue()) + .add(USER_ID, securityUser.getId().getId().toString()) + .add(SCOPES, scopes); if (securityUser.getSessionId() != null) { claimsBuilder.add(SESSION_ID, securityUser.getSessionId()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index ffc37dc7a1..b83a1b567c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -21,10 +21,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Header; import io.jsonwebtoken.Jws; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; import org.awaitility.Awaitility; import org.hamcrest.Matcher; @@ -124,7 +121,6 @@ import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; -import javax.crypto.SecretKey; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; From 1f648806f3f05771bc094a569126fcf5d725dc97 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 2 May 2024 16:13:46 +0200 Subject: [PATCH 06/15] refactored creationing Jwt SecretKey and JwtParcer --- .../queue/DefaultTbCoreConsumerService.java | 5 ++- .../DefaultTbRuleEngineConsumerService.java | 7 ++- .../processing/AbstractConsumerService.java | 4 ++ .../settings/DefaultJwtSettingsService.java | 5 ++- .../settings/DefaultJwtSettingsValidator.java | 3 +- .../security/model/token/JwtTokenFactory.java | 43 +++++++++++++++---- 6 files changed, 53 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index cdb3f19601..445d1a6bba 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -94,6 +94,7 @@ import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; @@ -172,10 +173,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> nfConsumer; protected final JwtSettingsService jwtSettingsService; + protected final JwtTokenFactory jwtTokenFactory; public void init(String nfConsumerThreadName) { this.notificationsConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(nfConsumerThreadName)); @@ -163,6 +166,7 @@ public abstract class AbstractConsumerService= Jwts.SIG.HS512.getKeyBitLength(); + return Base64.getDecoder().decode(settings.getTokenSigningKey()).length * Byte.SIZE >= KEY_LENGTH; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index 1de9e2c0c2..ea8c67e3dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -28,6 +28,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault; +import static org.thingsboard.server.service.security.model.token.JwtTokenFactory.KEY_LENGTH; @Component @RequiredArgsConstructor @@ -61,7 +62,7 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator { if (Arrays.isNullOrEmpty(decodedKey)) { throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); } - if (decodedKey.length * Byte.SIZE < 512 && !isSigningKeyDefault(jwtSettings)) { + if (decodedKey.length * Byte.SIZE < KEY_LENGTH && !isSigningKeyDefault(jwtSettings)) { throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 512 bits of data!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 6be020b1c5..9542a9de2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -20,6 +20,7 @@ import io.jsonwebtoken.ClaimsBuilder; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureException; @@ -57,6 +58,8 @@ import java.util.stream.Collectors; @Slf4j public class JwtTokenFactory { + public static int KEY_LENGTH = Jwts.SIG.HS512.getKeyBitLength(); + private static final String SCOPES = "scopes"; private static final String USER_ID = "userId"; private static final String FIRST_NAME = "firstName"; @@ -69,6 +72,9 @@ public class JwtTokenFactory { private final JwtSettingsService jwtSettingsService; + private volatile JwtParser jwtParser; + private volatile SecretKey secretKey; + /** * Factory method for issuing new JWT Tokens. */ @@ -180,6 +186,11 @@ public class JwtTokenFactory { return new AccessJwtToken(jwtBuilder.compact()); } + public void reload() { + getSecretKey(true); + getJwtParser(true); + } + private JwtBuilder setUpToken(SecurityUser securityUser, List scopes, long expirationTime) { if (StringUtils.isBlank(securityUser.getEmail())) { throw new IllegalArgumentException("Cannot create JWT Token without username/email"); @@ -202,15 +213,12 @@ public class JwtTokenFactory { .issuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) .issuedAt(Date.from(currentTime.toInstant())) .expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) - .signWith(toSecretKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()), Jwts.SIG.HS512); + .signWith(getSecretKey(false), Jwts.SIG.HS512); } public Jws parseTokenClaims(String token) { try { - return Jwts.parser() - .verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey()))) - .build() - .parseSignedClaims(token); + return getJwtParser(false).parseSignedClaims(token); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { log.debug("Invalid JWT Token", ex); throw new BadCredentialsException("Invalid JWT token: ", ex); @@ -226,9 +234,28 @@ public class JwtTokenFactory { return new JwtPair(accessToken.getToken(), refreshToken.getToken()); } - private SecretKey toSecretKey(String base64Key) { - byte[] decodedToken = Base64.getDecoder().decode(base64Key); - return new SecretKeySpec(decodedToken, "HmacSHA512"); + private SecretKey getSecretKey(boolean forceReload) { + if (secretKey == null || forceReload) { + synchronized (this) { + if (secretKey == null || forceReload) { + byte[] decodedToken = Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey()); + secretKey = new SecretKeySpec(decodedToken, "HmacSHA512"); + } + } + } + return secretKey; } + private JwtParser getJwtParser(boolean forceReload) { + if (jwtParser == null || forceReload) { + synchronized (this) { + if (jwtParser == null || forceReload) { + jwtParser = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey()))) + .build(); + } + } + } + return jwtParser; + } } From 2d1ed7871f24e618f82e54196dcde4909a556e4a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 2 May 2024 16:44:12 +0200 Subject: [PATCH 07/15] updated jwt translation --- ui-ngx/src/assets/locale/locale.constant-ar_AE.json | 4 ++-- ui-ngx/src/assets/locale/locale.constant-en_US.json | 4 ++-- ui-ngx/src/assets/locale/locale.constant-es_ES.json | 4 ++-- ui-ngx/src/assets/locale/locale.constant-nl_BE.json | 4 ++-- ui-ngx/src/assets/locale/locale.constant-pl_PL.json | 4 ++-- ui-ngx/src/assets/locale/locale.constant-zh_CN.json | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index 104eb18abd..2c552d6ab6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json @@ -482,9 +482,9 @@ "issuer-name": "اسم الجهة المصدرة", "issuer-name-required": "اسم الجهة المصدرة مطلوب.", "signings-key": "مفتاح التوقيع", - "signings-key-hint": "سلسلة مشفرة بتنسيق Base64 تمثل ما لا يقل عن 256 بت من البيانات.", + "signings-key-hint": "سلسلة مشفرة بتنسيق Base64 تمثل ما لا يقل عن 512 بت من البيانات.", "signings-key-required": "مفتاح التوقيع مطلوب.", - "signings-key-min-length": "يجب أن يكون مفتاح التوقيع ما لا يقل عن 256 بت من البيانات.", + "signings-key-min-length": "يجب أن يكون مفتاح التوقيع ما لا يقل عن 512 بت من البيانات.", "signings-key-base64": "يجب أن يكون مفتاح التوقيع بتنسيق base64.", "expiration-time": "وقت انتهاء صلاحية الرمز (ثانية)", "expiration-time-required": "وقت انتهاء صلاحية الرمز مطلوب.", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 1ba94ab8bb..0d755bcf19 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -457,9 +457,9 @@ "issuer-name": "Issuer name", "issuer-name-required": "Issuer name is required.", "signings-key": "Signing key", - "signings-key-hint": "Base64 encoded string representing at least 256 bits of data.", + "signings-key-hint": "Base64 encoded string representing at least 512 bits of data.", "signings-key-required": "Signing key is required.", - "signings-key-min-length": "Signing key must be at least 256 bits of data.", + "signings-key-min-length": "Signing key must be at least 512 bits of data.", "signings-key-base64": "Signing key must be base64 format.", "expiration-time": "Token expiration time (sec)", "expiration-time-required": "Token expiration time is required.", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index e80fbf6c3e..00035bc3cc 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -431,9 +431,9 @@ "issuer-name": "Nombre del emisor", "issuer-name-required": "Se requiere nombre del emisor.", "signings-key": "Clave de firma", - "signings-key-hint": "Una string codificada en Base64 representando por lo menos 256 bits de datos.", + "signings-key-hint": "Una string codificada en Base64 representando por lo menos 512 bits de datos.", "signings-key-required": "Se requiere clave de firma.", - "signings-key-min-length": "La clave de firma debe tener al menos 256 bits de datos.", + "signings-key-min-length": "La clave de firma debe tener al menos 512 bits de datos.", "signings-key-base64": "La clave de firma debe estar en formato base64.", "expiration-time": "Caducidad del token (en segundos)", "expiration-time-required": "Se requiere caducidad del token.", diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json index 4cd3bb1e49..f62f352723 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json +++ b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json @@ -425,9 +425,9 @@ "issuer-name": "Naam van de uitgever", "issuer-name-required": "De naam van de uitgever is vereist.", "signings-key": "Sleutel ondertekenen", - "signings-key-hint": "Base64-gecodeerde tekenreeks die ten minste 256 bits aan gegevens vertegenwoordigt.", + "signings-key-hint": "Base64-gecodeerde tekenreeks die ten minste 512 bits aan gegevens vertegenwoordigt.", "signings-key-required": "Ondertekeningssleutel is vereist.", - "signings-key-min-length": "De ondertekeningssleutel moet ten minste 256 bits aan gegevens bevatten.", + "signings-key-min-length": "De ondertekeningssleutel moet ten minste 512 bits aan gegevens bevatten.", "signings-key-base64": "De ondertekeningssleutel moet de base64-indeling hebben.", "expiration-time": "Vervaltijd token (sec)", "expiration-time-required": "De vervaltijd van het token is vereist.", diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index 58d4a0168f..d5d9ac8f83 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -456,9 +456,9 @@ "issuer-name":"Nazwa wydawcy", "issuer-name-required":"Nazwa wydawcy jest wymagana.", "signings-key":"Klucz podpisujący", - "signings-key-hint":"Kodowany Base64 ciąg reprezentujący co najmniej 256 bitów danych.", + "signings-key-hint":"Kodowany Base64 ciąg reprezentujący co najmniej 512 bitów danych.", "signings-key-required":"Klucz podpisujący jest wymagany.", - "signings-key-min-length":"Klucz podpisujący musi reprezentować co najmniej 256 bitów danych.", + "signings-key-min-length":"Klucz podpisujący musi reprezentować co najmniej 512 bitów danych.", "signings-key-base64":"Klucz podpisujący musi być w formacie base64.", "expiration-time":"Czas wygaśnięcia tokena (sek)", "expiration-time-required":"Czas wygaśnięcia tokena jest wymagany.", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 886151fdf2..c691bbea76 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -451,9 +451,9 @@ "issuer-name": "发行者名称", "issuer-name-required": "发行者名称必填。", "signings-key": "签名密钥", - "signings-key-hint": "Base64编码的字符串,至少256位数据。", + "signings-key-hint": "Base64编码的字符串,至少512位数据。", "signings-key-required": "签名密钥必填。", - "signings-key-min-length": "签名密钥必须至少为256位的数据。", + "signings-key-min-length": "签名密钥必须至少为512位的数据。", "signings-key-base64": "签名密钥必须是Base64格式。", "expiration-time": "令牌过期时间(秒)", "expiration-time-required": "令牌过期时间是必填。", From 4df0a41178082762f8bf546bd867f9e6486ed4b0 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 2 May 2024 17:48:31 +0200 Subject: [PATCH 08/15] UpdateJwtSettings warn msg improvements --- .../install/DefaultSystemDataLoaderService.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index fbfbd9e87b..bdd0494443 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -285,24 +285,22 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Override public void updateJwtSettings() { JwtSettings jwtSettings = jwtSettingsService.getJwtSettings(); - boolean invalidSignKey = false; + String warningMessage = null; if (isSigningKeyDefault(jwtSettings)) { - log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + - "Added new temporary JWT Signing Key. " + - "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + - "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator."); + warningMessage = "The platform is using the default JWT Signing Key, which is a security risk."; invalidSignKey = true; } else if (!validateTokenSigningKeyLength(jwtSettings)) { - log.warn("WARNING: The platform is configured to use JWT Signing Key with length less then 512 bits of data. " + - "Added new temporary JWT Signing Key. " + - "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + - "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator."); + warningMessage = "The JWT Signing Key is shorter than 512 bits, which is a security risk."; invalidSignKey = true; } if (invalidSignKey) { + log.warn("WARNING: {}. A new JWT Signing Key has been added automatically. " + + "You can change the JWT Signing Key using the Web UI: " + + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage); + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); jwtSettingsService.saveJwtSettings(jwtSettings); From d25f271d8965529df40bbb46fa7b602b253b622a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 2 May 2024 22:58:44 +0200 Subject: [PATCH 09/15] JwtFactory reload --- .../auth/jwt/settings/DefaultJwtSettingsService.java | 8 +++++++- .../server/service/security/auth/JwtTokenFactoryTest.java | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index 537f063bf2..df822812ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.jwt.settings; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; @@ -25,6 +26,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import java.util.Base64; import java.util.Objects; @@ -40,6 +42,8 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final AdminSettingsService adminSettingsService; private final Optional tbClusterService; private final JwtSettingsValidator jwtSettingsValidator; + @Lazy + private final JwtTokenFactory jwtTokenFactory; private volatile JwtSettings jwtSettings = null; //lazy init @@ -62,7 +66,9 @@ public class DefaultJwtSettingsService implements JwtSettingsService { @Override public JwtSettings reloadJwtSettings() { log.trace("Executing reloadJwtSettings"); - return getJwtSettings(true); + var settings = getJwtSettings(true); + jwtTokenFactory.reload(); + return settings; } @Override diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 75868e6508..264bb87979 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -53,6 +53,7 @@ public class JwtTokenFactoryTest { private JwtTokenFactory tokenFactory; private AdminSettingsService adminSettingsService; private JwtSettingsService jwtSettingsService; + private JwtTokenFactory jwtTokenFactory; private JwtSettings jwtSettings; @@ -170,7 +171,7 @@ public class JwtTokenFactoryTest { } private DefaultJwtSettingsService mockJwtSettingsService() { - return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator()); + return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator(), jwtTokenFactory); } private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) { From e59c4a950c4bf203082768727ac403679532f280 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 3 May 2024 09:36:12 +0200 Subject: [PATCH 10/15] refactoring --- .../server/service/queue/DefaultTbCoreConsumerService.java | 4 +--- .../service/queue/DefaultTbRuleEngineConsumerService.java | 6 ++---- .../service/queue/processing/AbstractConsumerService.java | 2 -- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 445d1a6bba..42c516a8a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -94,7 +94,6 @@ import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; -import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; @@ -173,12 +172,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> nfConsumer; protected final JwtSettingsService jwtSettingsService; - protected final JwtTokenFactory jwtTokenFactory; public void init(String nfConsumerThreadName) { this.notificationsConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(nfConsumerThreadName)); @@ -166,7 +165,6 @@ public abstract class AbstractConsumerService Date: Fri, 3 May 2024 13:09:52 +0300 Subject: [PATCH 11/15] updated log entry for adding customer_title_unq_key --- .../install/SqlAbstractDatabaseSchemaService.java | 10 +++++++--- .../install/SqlEntityDatabaseSchemaService.java | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java index 9058db4172..3ba938bd90 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java @@ -84,13 +84,17 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema } protected void executeQuery(String query) { + executeQuery(query, null); + } + + protected void executeQuery(String query, String logQuery) { + logQuery = logQuery != null ? logQuery : query; try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { conn.createStatement().execute(query); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script - log.info("Successfully executed query: {}", query); + log.info("Successfully executed query: {}", logQuery); Thread.sleep(5000); } catch (InterruptedException | SQLException e) { - log.error("Failed to execute query: {} due to: {}", query, e.getMessage()); - throw new RuntimeException("Failed to execute query: " + query, e); + throw new RuntimeException("Failed to execute query: " + logQuery, e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java index b1a41aadcd..5e1357a48e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java @@ -56,6 +56,7 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer @Override public void createCustomerTitleUniqueConstraintIfNotExists() { executeQuery("DO $$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'customer_title_unq_key') THEN " + - "ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;"); + "ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;", + "create 'customer_title_unq_key' constraint if it doesn't already exist!"); } } From 510e4b14fe39dcae7853b9fadc7494f1f115e62a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 3 May 2024 15:27:21 +0200 Subject: [PATCH 12/15] fixed instalation --- .../security/auth/jwt/settings/DefaultJwtSettingsService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index df822812ec..139b177926 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -43,7 +43,7 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final Optional tbClusterService; private final JwtSettingsValidator jwtSettingsValidator; @Lazy - private final JwtTokenFactory jwtTokenFactory; + private final Optional jwtTokenFactory; private volatile JwtSettings jwtSettings = null; //lazy init @@ -67,7 +67,7 @@ public class DefaultJwtSettingsService implements JwtSettingsService { public JwtSettings reloadJwtSettings() { log.trace("Executing reloadJwtSettings"); var settings = getJwtSettings(true); - jwtTokenFactory.reload(); + jwtTokenFactory.ifPresent(JwtTokenFactory::reload); return settings; } From 3cd8c60518b06edbab45cfa96df424b0a95cfd49 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 3 May 2024 21:19:08 +0200 Subject: [PATCH 13/15] fixed circular dependency --- .../security/auth/jwt/settings/DefaultJwtSettingsService.java | 2 +- .../server/service/security/model/token/JwtTokenFactory.java | 2 ++ .../server/service/security/auth/JwtTokenFactoryTest.java | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index 139b177926..c5c04ac312 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.jwt.settings; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; @@ -42,7 +43,6 @@ public class DefaultJwtSettingsService implements JwtSettingsService { private final AdminSettingsService adminSettingsService; private final Optional tbClusterService; private final JwtSettingsValidator jwtSettingsValidator; - @Lazy private final Optional jwtTokenFactory; private volatile JwtSettings jwtSettings = null; //lazy init diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 9542a9de2d..f2e276a0ef 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -28,6 +28,7 @@ import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; @@ -70,6 +71,7 @@ public class JwtTokenFactory { private static final String CUSTOMER_ID = "customerId"; private static final String SESSION_ID = "sessionId"; + @Lazy private final JwtSettingsService jwtSettingsService; private volatile JwtParser jwtParser; diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 264bb87979..e6c6cfbac4 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -53,7 +53,6 @@ public class JwtTokenFactoryTest { private JwtTokenFactory tokenFactory; private AdminSettingsService adminSettingsService; private JwtSettingsService jwtSettingsService; - private JwtTokenFactory jwtTokenFactory; private JwtSettings jwtSettings; @@ -171,7 +170,7 @@ public class JwtTokenFactoryTest { } private DefaultJwtSettingsService mockJwtSettingsService() { - return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator(), jwtTokenFactory); + return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator(), Optional.empty()); } private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) { From c554246218979c2fb7a7646847834aec6cd02aec Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 6 May 2024 14:21:40 +0300 Subject: [PATCH 14/15] Ignore tenant not found for Cassandra rate limits --- .../server/cache/limits/DefaultRateLimitService.java | 11 ++++++++++- .../server/cache/limits/RateLimitService.java | 2 ++ .../server/dao/util/AbstractBufferedRateExecutor.java | 8 ++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java b/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java index f3530e99d2..9680fd2b96 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java @@ -63,12 +63,21 @@ public class DefaultRateLimitService implements RateLimitService { @Override public boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level) { + return checkRateLimit(api, tenantId, level, false); + } + + @Override + public boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level, boolean ignoreTenantNotFound) { if (tenantId.isSysTenantId()) { return true; } TenantProfile tenantProfile = tenantProfileProvider.get(tenantId); if (tenantProfile == null) { - throw new TenantProfileNotFoundException(tenantId); + if (ignoreTenantNotFound) { + return true; + } else { + throw new TenantProfileNotFoundException(tenantId); + } } String rateLimitConfig = tenantProfile.getProfileConfiguration() diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java b/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java index 84c22c514b..3573ee2b8c 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java @@ -24,6 +24,8 @@ public interface RateLimitService { boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level); + boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level, boolean ignoreTenantNotFound); + boolean checkRateLimit(LimitedApi api, Object level, String rateLimitConfig); void cleanUp(LimitedApi api, Object level); diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index 48b2471217..34316bc615 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java @@ -27,20 +27,20 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; -import org.thingsboard.server.common.data.limit.LimitedApi; -import org.thingsboard.server.cache.limits.RateLimitService; -import jakarta.annotation.Nullable; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -114,7 +114,7 @@ public abstract class AbstractBufferedRateExecutor Date: Mon, 6 May 2024 14:30:19 +0300 Subject: [PATCH 15/15] Increase test timeout for verifyMsgProcessed --- .../queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 6bd0cd9c0e..8e0b294241 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -640,7 +640,7 @@ public class TbRuleEngineQueueConsumerManagerTest { } private void verifyMsgProcessed(TbMsg tbMsg) { - await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(15, TimeUnit.SECONDS).untilAsserted(() -> { verify(actorContext, atLeastOnce()).tell(argThat(msg -> { return ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId()); }));