JwtSettings API added to the admin controller

This commit is contained in:
Sergey Matvienko 2022-11-01 19:42:38 +02:00
parent c313e1cf9c
commit ea80f9838e
4 changed files with 256 additions and 99 deletions

View File

@ -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();
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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)