diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java index 252b0a021c..bb0127a39e 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsService.java @@ -25,6 +25,4 @@ public interface JwtSettingsService { JwtSettings saveJwtSettings(JwtSettings jwtSettings); - void validateJwtTokenSigningKey(); - } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java index f64b7c4658..82657c5952 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java @@ -32,7 +32,6 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.dao.settings.AdminSettingsService; import javax.annotation.PostConstruct; -import javax.validation.ValidationException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Objects; @@ -84,8 +83,10 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { jwtSettings.setTokenSigningKey(jwtLoaded.getTokenSigningKey()); } - if (hasDefaultTokenSigningKey() && !isFirstInstall()) { - log.warn("JWT token signing key is default. This is a security issue. Please, consider to set unique value"); + if (hasDefaultTokenSigningKey()) { + 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."); } } @@ -107,17 +108,18 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { return TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey()); } + /** + * Create JWT admin settings is intended to be called from Install or Upgrade scripts + * */ @Override public void createJwtAdminSettings() { - log.debug("Creating JWT admin settings..."); + log.info("Creating JWT admin settings..."); Objects.requireNonNull(jwtSettings, "JWT settings is null"); if (isJwtAdminSettingsNotExists()) { - if (hasDefaultTokenSigningKey()) { - if (!isAllowedDefaultJwtSigningKey()) { - log.info("JWT token signing key is default. Generating a new random key"); - jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( - RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); - } + if (hasDefaultTokenSigningKey() && isFirstInstall()) { + log.info("JWT token signing key is default. Generating a new random key"); + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); } saveJwtSettings(jwtSettings); } @@ -150,29 +152,4 @@ public class JwtSettingsServiceDefault implements JwtSettingsService { return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); } - /* - * Allowing default JWT signing key is not secure - * */ - boolean isAllowedDefaultJwtSigningKey() { - String allowDefaultJwtSigningKey = System.getenv(TB_ALLOW_DEFAULT_JWT_SIGNING_KEY); - return "true".equalsIgnoreCase(allowDefaultJwtSigningKey); - } - - @Override - public void validateJwtTokenSigningKey() { - if (isJwtAdminSettingsNotExists() && hasDefaultTokenSigningKey()) { - if (isAllowedDefaultJwtSigningKey()) { - 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 = "UPGRADE ERROR. YOUR ACTION REQUIRED. Please, set a unique signing key with env variable JWT_TOKEN_SIGNING_KEY. " + - "The key should be a Base64 encoded string representing at least 256 bits of data. " + - "This will require to generate new tokens for all UI users and scripts that use JWT. " + - "To keep the default non-secure JWT signing key set TB_ALLOW_DEFAULT_JWT_SIGNING_KEY=true and restart the upgrade. " + - "You may change the JWT signing key later in the Admin Settings UI."; - log.error(message); - throw new ValidationException(message); - } - } - } - } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java index c6ec252d9f..8e0d4afe7a 100644 --- a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java @@ -15,53 +15,6 @@ */ package org.thingsboard.server.config.jwt; -import lombok.AllArgsConstructor; -import org.apache.commons.lang3.RandomUtils; -import org.apache.commons.lang3.StringUtils; -import org.bouncycastle.util.Arrays; -import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.exception.DataValidationException; - -import java.util.Base64; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -@Component -@AllArgsConstructor -public class JwtSettingsValidator { - - public void validate(JwtSettings jwtSettings) { - if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) { - throw new DataValidationException("JWT token issuer should be specified!"); - } - if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) { - throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!"); - } - if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) { - throw new DataValidationException("JWT token expiration time should be at least 1 minute!"); - } - if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) { - throw new DataValidationException("JWT token expiration time should greater than JWT refresh token expiration time!"); - } - if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) { - throw new DataValidationException("JWT token signing key should be specified!"); - } - - byte[] decodedKey; - try { - decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()); - } catch (Exception e) { - throw new DataValidationException("JWT token signing key should be valid Base64 encoded string! " + e.getCause()); - } - - if (Arrays.isNullOrEmpty(decodedKey)) { - throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); - } - if (decodedKey.length * Byte.SIZE < 256) { - throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!"); - } - - System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory - } - +public interface JwtSettingsValidator { + void validate(JwtSettings jwtSettings); } diff --git a/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java new file mode 100644 index 0000000000..ee60a80de6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorDefault.java @@ -0,0 +1,69 @@ +/** + * 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.config.jwt; + +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.util.Arrays; +import org.springframework.stereotype.Component; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.util.Base64; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class JwtSettingsValidatorDefault implements JwtSettingsValidator { + + @Override + public void validate(JwtSettings jwtSettings) { + if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) { + throw new DataValidationException("JWT token issuer should be specified!"); + } + if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) { + throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!"); + } + if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) { + throw new DataValidationException("JWT token expiration time should be at least 1 minute!"); + } + if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) { + throw new DataValidationException("JWT token expiration time should greater than JWT refresh token expiration time!"); + } + if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) { + throw new DataValidationException("JWT token signing key should be specified!"); + } + + byte[] decodedKey; + try { + decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()); + } catch (Exception e) { + throw new DataValidationException("JWT token signing key should be valid Base64 encoded string! " + e.getCause()); + } + + if (Arrays.isNullOrEmpty(decodedKey)) { + throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); + } + if (decodedKey.length * Byte.SIZE < 256) { + throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 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/install/ConditionValidatorUpgradeServiceImpl.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorInstall.java similarity index 58% rename from application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java rename to application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorInstall.java index 8dbfaab893..e353eb57a2 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidatorInstall.java @@ -13,26 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.install; +package org.thingsboard.server.config.jwt; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import org.thingsboard.server.config.jwt.JwtSettingsService; +import org.springframework.stereotype.Component; -@Service +@Primary @Profile("install") +@Component @RequiredArgsConstructor -@Slf4j -public class ConditionValidatorUpgradeServiceImpl implements ConditionValidatorUpgradeService { - - private final JwtSettingsService jwtSettingsService; +public class JwtSettingsValidatorInstall implements JwtSettingsValidator { + /** + * During Install or upgrade the validation is suppressed to keep existing data + * */ @Override - public void validateConditionsBeforeUpgrade(String fromVersion) throws Exception { - log.info("Validating conditions before upgrade..."); - jwtSettingsService.validateJwtTokenSigningKey(); + public void validate(JwtSettings jwtSettings) { + } } 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 a100c61035..277e0b4d38 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -34,7 +34,6 @@ 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") @@ -89,16 +88,11 @@ 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)) { 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 deleted file mode 100644 index ec98a2f37e..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/install/ConditionValidatorUpgradeService.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.install; - -public interface ConditionValidatorUpgradeService { - - void validateConditionsBeforeUpgrade(String fromVersion) throws Exception; - -}