JwtSettings API added to the admin controller
This commit is contained in:
parent
c313e1cf9c
commit
ea80f9838e
@ -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();
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user