jwt settings validation except install, warn on each reload if token key is default
This commit is contained in:
parent
b95f1c95e0
commit
27d26ea27c
@ -25,6 +25,4 @@ public interface JwtSettingsService {
|
||||
|
||||
JwtSettings saveJwtSettings(JwtSettings jwtSettings);
|
||||
|
||||
void validateJwtTokenSigningKey();
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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)) {
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user