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