From 9b519d33a19532f4cca8973e521d5ee1186d5b35 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 19 Sep 2022 12:43:43 +0300 Subject: [PATCH] jwt settings install and upgrade --- .../server/config/JwtSettings.java | 28 ++++----- .../install/ThingsboardInstallService.java | 12 ++++ .../ConditionValidatorUpgradeService.java | 22 +++++++ .../ConditionValidatorUpgradeServiceImpl.java | 63 +++++++++++++++++++ .../DefaultSystemDataLoaderService.java | 33 ++++++++++ .../install/SystemDataLoaderService.java | 2 + .../update/DefaultDataUpdateService.java | 4 ++ .../src/main/resources/thingsboard.yml | 2 +- 8 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java diff --git a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java index 1fc0b4bd8c..14e228bde8 100644 --- a/application/src/main/java/org/thingsboard/server/config/JwtSettings.java +++ b/application/src/main/java/org/thingsboard/server/config/JwtSettings.java @@ -18,7 +18,6 @@ package org.thingsboard.server.config; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -29,15 +28,13 @@ import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.dao.settings.AdminSettingsService; import javax.annotation.PostConstruct; -import java.nio.charset.StandardCharsets; -import java.util.Base64; @Component @ConfigurationProperties(prefix = "security.jwt") @Data @Slf4j public class JwtSettings { - static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; + public static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; /** * {@link JwtToken} will expire after this time. @@ -67,25 +64,22 @@ public class JwtSettings { @PostConstruct public void init() { AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); - if (adminJwtSettings == null) { - if (TOKEN_SIGNING_KEY_DEFAULT.equals(tokenSigningKey)) { - log.warn("JWT token signing key is default. Generating a new random key"); - tokenSigningKey = Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)); - } - adminJwtSettings = new AdminSettings(); - adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); - adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); - adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(this)); - log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); - adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); - } else { - log.debug("Loading the JWT admin settings"); + if (adminJwtSettings != null) { + log.debug("Loading the JWT admin settings from database"); JwtSettings jwtSettings = JacksonUtil.treeToValue(adminJwtSettings.getJsonValue(), JwtSettings.class); this.setRefreshTokenExpTime(jwtSettings.getRefreshTokenExpTime()); this.setTokenExpirationTime(jwtSettings.getTokenExpirationTime()); this.setTokenIssuer(jwtSettings.getTokenIssuer()); this.setTokenSigningKey(jwtSettings.getTokenSigningKey()); } + + if (hasDefaultTokenSigningKey()) { + log.warn("JWT token signing key is default. This is a security issue. Please, consider to set unique value"); + } + } + + public boolean hasDefaultTokenSigningKey() { + return TOKEN_SIGNING_KEY_DEFAULT.equals(tokenSigningKey); } } 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 40e15c7b0e..c83552e777 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -33,6 +33,7 @@ import org.thingsboard.server.service.install.migrate.EntitiesMigrateService; import org.thingsboard.server.service.install.migrate.TsLatestMigrateService; import org.thingsboard.server.service.install.update.CacheCleanupService; import org.thingsboard.server.service.install.update.DataUpdateService; +import org.thingsboard.server.service.install.ConditionValidatorUpgradeService; @Service @Profile("install") @@ -84,11 +85,17 @@ public class ThingsboardInstallService { @Autowired(required = false) private TsLatestMigrateService latestMigrateService; + @Autowired + private ConditionValidatorUpgradeService conditionValidatorUpgradeService; + + public void performInstall() { try { if (isUpgrade) { log.info("Starting ThingsBoard Upgrade from version {} ...", upgradeFromVersion); + conditionValidatorUpgradeService.validateConditionsBeforeUpgrade(upgradeFromVersion); + cacheCleanupService.clearCache(upgradeFromVersion); if ("2.5.0-cassandra".equals(upgradeFromVersion)) { @@ -224,6 +231,10 @@ public class ThingsboardInstallService { log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; + case "3.4.0": + log.info("Upgrading ThingsBoard from version 3.4.0 to 3.5.0 ..."); + dataUpdateService.updateData("3.4.0"); + break; //TODO update CacheCleanupService on the next version upgrade @@ -257,6 +268,7 @@ public class ThingsboardInstallService { systemDataLoaderService.createSysAdmin(); systemDataLoaderService.createDefaultTenantProfiles(); systemDataLoaderService.createAdminSettings(); + systemDataLoaderService.createJwtAdminSettings(); systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.createOAuth2Templates(); systemDataLoaderService.createQueues(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java new file mode 100644 index 0000000000..ec98a2f37e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install; + +public interface ConditionValidatorUpgradeService { + + void validateConditionsBeforeUpgrade(String fromVersion) throws Exception; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java new file mode 100644 index 0000000000..f70086abee --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.install; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.config.JwtSettings; +import org.thingsboard.server.dao.settings.AdminSettingsService; + +import javax.validation.ValidationException; + +import static org.thingsboard.server.config.JwtSettings.ADMIN_SETTINGS_JWT_KEY; + +@Service +@Profile("install") +@RequiredArgsConstructor +@Slf4j +public class ConditionValidatorUpgradeServiceImpl implements ConditionValidatorUpgradeService { + + private final AdminSettingsService adminSettingsService; + + private final JwtSettings jwtSettings; + + @Override + public void validateConditionsBeforeUpgrade(String fromVersion) throws Exception { + log.info("Validating conditions before upgrade.."); + validateJwtTokenSigningKey(); + } + + void validateJwtTokenSigningKey() { + AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + if (adminJwtSettings == null) { + if (jwtSettings.hasDefaultTokenSigningKey()) { + String allowDefaultJwtSigningKey = System.getenv("TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"); + if ("true".equalsIgnoreCase(allowDefaultJwtSigningKey)) { + log.warn("Default JWT signing key is allowed. This is a security issue. Please, consider to set a strong key in admin settings"); + } else { + String message = "Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. Key is a Base64 encoded phrase. This will require to generate new tokens for all users and API that uses JWT tokens. To allow insecure JWS use TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true"; + log.error(message); + throw new ValidationException(message); + } + } + } + } + +} 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 78e03ffe1a..a113f1e6d1 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 @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.Getter; 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; @@ -29,6 +30,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Profile; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.Customer; @@ -82,6 +84,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -100,13 +103,18 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.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.Objects; import java.util.TreeMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import static org.thingsboard.server.config.JwtSettings.ADMIN_SETTINGS_JWT_KEY; + @Service @Profile("install") @Slf4j @@ -167,6 +175,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Autowired private QueueService queueService; + @Autowired + private JwtSettings jwtSettings; + @Bean protected BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -656,4 +667,26 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { queueService.saveQueue(sequentialByOriginatorQueue); } } + + @Override + public void createJwtAdminSettings() throws Exception { + Objects.requireNonNull(jwtSettings,"JWT settings is null"); + AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + if (adminJwtSettings == null) { + if (jwtSettings.hasDefaultTokenSigningKey()) { + String allowDefaultJwtSigningKey = System.getenv("TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"); + if (!"true".equalsIgnoreCase(allowDefaultJwtSigningKey)) { + log.warn("JWT token signing key is default. Generating a new random key"); + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + } + } + adminJwtSettings = new AdminSettings(); + adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); + adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); + adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings)); + log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); + adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); + } + } + } 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 1ceb1be289..4ef8241a3b 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 @@ -23,6 +23,8 @@ public interface SystemDataLoaderService { void createAdminSettings() throws Exception; + void createJwtAdminSettings() throws Exception; + void createOAuth2Templates() throws Exception; void loadSystemWidgets() throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index b90da586fc..862f0a92b8 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -159,6 +159,10 @@ public class DefaultDataUpdateService implements DataUpdateService { tenantsProfileQueueConfigurationUpdater.updateEntities(); rateLimitsUpdater.updateEntities(); break; + case "3.4.0": + log.info("Updating data from version 3.4.0 to 3.5.0 ..."); + systemDataLoaderService.createJwtAdminSettings(); + break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index a6e8637247..0e6a005e4d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -107,7 +107,7 @@ plugins: # Security parameters security: # JWT Token parameters - jwt: + jwt: # Since 3.5.0 values are persisted to the database during install or upgrade. On Install, the key will be generated randomly if no custom value set. tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours) refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week) tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"