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 bbb1bdba8b..fb673fe50c 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 @@ -15,108 +15,14 @@ */ package org.thingsboard.server.config.jwt; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.stereotype.Service; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.settings.AdminSettingsService; +public interface JwtSettingsService { -import javax.annotation.PostConstruct; -import javax.validation.ValidationException; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Objects; + JwtSettings getJwtSettings(); -@Service -@RequiredArgsConstructor -@Slf4j -public class JwtSettingsService { + void createJwtAdminSettings(); - static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; - static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; - static final String TB_ALLOW_DEFAULT_JWT_SIGNING_KEY = "TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"; + JwtSettings saveJwtSettings(JwtSettings jwtSettings); - private final AdminSettingsService adminSettingsService; - - @Getter - private final JwtSettings jwtSettings; - - @PostConstruct - public void init() { - AdminSettings adminJwtSettings = findJwtAdminSettings(); - if (adminJwtSettings != null) { - log.debug("Loading the JWT admin settings from database"); - JwtSettings jwtLoaded = JacksonUtil.treeToValue(adminJwtSettings.getJsonValue(), JwtSettings.class); - jwtSettings.setRefreshTokenExpTime(jwtLoaded.getRefreshTokenExpTime()); - jwtSettings.setTokenExpirationTime(jwtLoaded.getTokenExpirationTime()); - jwtSettings.setTokenIssuer(jwtLoaded.getTokenIssuer()); - jwtSettings.setTokenSigningKey(jwtLoaded.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(jwtSettings.getTokenSigningKey()); - } - - public void createJwtAdminSettings() { - log.debug("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))); - } - } - AdminSettings 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); - } - } - - public boolean isJwtAdminSettingsNotExists() { - return findJwtAdminSettings() == null; - } - - AdminSettings findJwtAdminSettings() { - try { - return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); - } catch (InvalidDataAccessResourceUsageException ignored) { - log.debug("findAdminSettingsByKey is returning InvalidDataAccessResourceUsageException. This is an installation case when the database is not initialized yet"); - return null; - } - } - - /* - * Allowing default JWT signing key is not secure - * */ - public boolean isAllowedDefaultJwtSigningKey() { - String allowDefaultJwtSigningKey = System.getenv(TB_ALLOW_DEFAULT_JWT_SIGNING_KEY); - return "true".equalsIgnoreCase(allowDefaultJwtSigningKey); - } - - 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 = "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); - } - } - } + 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 new file mode 100644 index 0000000000..5c172c9c2d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsServiceDefault.java @@ -0,0 +1,154 @@ +/** + * 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.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +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.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; + +@Service +@RequiredArgsConstructor +@Slf4j +public class JwtSettingsServiceDefault implements JwtSettingsService { + + static final String ADMIN_SETTINGS_JWT_KEY = "jwt"; + static final String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; + static final String TB_ALLOW_DEFAULT_JWT_SIGNING_KEY = "TB_ALLOW_DEFAULT_JWT_SIGNING_KEY"; + + private final AdminSettingsService adminSettingsService; + private final TbClusterService tbClusterService; + + private final JwtSettingsValidator jwtSettingsValidator; + + @Getter + private final JwtSettings jwtSettings; + + @PostConstruct + public void init() { + reloadJwtSettings(); + } + + void reloadJwtSettings() { + AdminSettings adminJwtSettings = findJwtAdminSettings(); + if (adminJwtSettings != null) { + log.debug("Loading the JWT admin settings from database"); + JwtSettings jwtLoaded = mapAdminToJwtSettings(adminJwtSettings); + jwtSettings.setRefreshTokenExpTime(jwtLoaded.getRefreshTokenExpTime()); + jwtSettings.setTokenExpirationTime(jwtLoaded.getTokenExpirationTime()); + jwtSettings.setTokenIssuer(jwtLoaded.getTokenIssuer()); + jwtSettings.setTokenSigningKey(jwtLoaded.getTokenSigningKey()); + } + + if (hasDefaultTokenSigningKey()) { + log.warn("JWT token signing key is default. This is a security issue. Please, consider to set unique value"); + } + } + + JwtSettings mapAdminToJwtSettings(AdminSettings adminSettings) { + Objects.requireNonNull(adminSettings, "adminSettings for JWT is null"); + return JacksonUtil.treeToValue(adminSettings.getJsonValue(), JwtSettings.class); + } + + AdminSettings mapJwtToAdminSettings(JwtSettings jwtSettings) { + Objects.requireNonNull(jwtSettings, "jwtSettings is null"); + AdminSettings adminJwtSettings = new AdminSettings(); + adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); + adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); + adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings)); + return adminJwtSettings; + } + + boolean hasDefaultTokenSigningKey() { + return TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey()); + } + + @Override + public void createJwtAdminSettings() { + log.debug("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))); + } + } + saveJwtSettings(jwtSettings); + } + } + + @Override + public JwtSettings saveJwtSettings(JwtSettings jwtSettings){ + jwtSettingsValidator.validate(jwtSettings); + AdminSettings adminJwtSettings = mapJwtToAdminSettings(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); + tbClusterService.broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, ComponentLifecycleEvent.UPDATED); + reloadJwtSettings(); + return getJwtSettings(); + } + + boolean isJwtAdminSettingsNotExists() { + return findJwtAdminSettings() == null; + } + + AdminSettings findJwtAdminSettings() { + try { + return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); + } catch (InvalidDataAccessResourceUsageException ignored) { + log.debug("findAdminSettingsByKey is returning InvalidDataAccessResourceUsageException. This is an installation case when the database is not initialized yet"); + return null; + } + } + + /* + * 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 = "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/config/jwt/JwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java new file mode 100644 index 0000000000..4e91c654e0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/jwt/JwtSettingsValidator.java @@ -0,0 +1,58 @@ +/** + * 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 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; + +@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) <= 0) { + throw new DataValidationException("JWT refresh token expiration time should be specified!"); + } + if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= 0) { + throw new DataValidationException("JWT token expiration time should be specified!"); + } + 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!"); + } + Arrays.fill(decodedKey, (byte) 0); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 54cbdd9b0f..09f552d0fd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -23,6 +23,7 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.async.DeferredResult; @@ -36,6 +37,8 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.sms.config.TestSmsRequest; import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings; +import org.thingsboard.server.config.jwt.JwtSettings; +import org.thingsboard.server.config.jwt.JwtSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; @@ -64,6 +67,9 @@ public class AdminController extends BaseController { @Autowired private SystemSecurityService systemSecurityService; + @Autowired + private JwtSettingsService jwtSettingsService; + @Autowired private EntitiesVersionControlService versionControlService; @@ -151,6 +157,39 @@ public class AdminController extends BaseController { } } + @ApiOperation(value = "Get the JWT Settings object (getJwtSettings)", + notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/jwtSettings", method = RequestMethod.GET) + @ResponseBody + public JwtSettings getJwtSettings() throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); + return checkNotNull(jwtSettingsService.getJwtSettings()); + } catch (Exception e) { + throw handleException(e); + } + } + + @ApiOperation(value = "Update JWT Settings (saveSecuritySettings)", + notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/jwtSettings", method = RequestMethod.POST) + @ResponseBody + public JwtSettings saveJwtSettings( + @ApiParam(value = "A JSON value representing the JWT Settings.") + @RequestBody JwtSettings jwtSettings) throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); + jwtSettings = checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings)); + return jwtSettings; + } catch (Exception e) { + throw handleException(e); + } + } + @ApiOperation(value = "Send test email (sendTestMail)", notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " + "You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH)