Merge remote-tracking branch 'upstream/master' into AD/imp/paste-hex-with-prefix

This commit is contained in:
Artem Dzhereleiko 2024-05-07 10:22:27 +03:00
commit 0d23f7b321
30 changed files with 223 additions and 227 deletions

View File

@ -136,6 +136,7 @@ public class ThingsboardInstallService {
dataUpdateService.updateData("3.6.4");
entityDatabaseSchemaService.createCustomerTitleUniqueConstraintIfNotExists();
systemDataLoaderService.updateDefaultNotificationConfigs(false);
systemDataLoaderService.updateJwtSettings();
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
default:

View File

@ -19,13 +19,17 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@ -81,6 +85,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
@ -100,14 +105,11 @@ import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;
@ -117,79 +119,51 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE;
import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault;
import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.validateTokenSigningKeyLength;
@Service
@Profile("install")
@Slf4j
@RequiredArgsConstructor
public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
public static final String CUSTOMER_CRED = "customer";
public static final String ACTIVITY_STATE = "active";
@Autowired
private InstallScripts installScripts;
private final InstallScripts installScripts;
private final UserService userService;
private final AdminSettingsService adminSettingsService;
private final TenantService tenantService;
private final TenantProfileService tenantProfileService;
private final CustomerService customerService;
private final DeviceService deviceService;
private final DeviceProfileService deviceProfileService;
private final AttributesService attributesService;
private final DeviceCredentialsService deviceCredentialsService;
private final RuleChainService ruleChainService;
private final TimeseriesService tsService;
private final DeviceConnectivityConfiguration connectivityConfiguration;
private final QueueService queueService;
private final JwtSettingsService jwtSettingsService;
private final NotificationSettingsService notificationSettingsService;
private final NotificationTargetService notificationTargetService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private UserService userService;
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
private WidgetTypeService widgetTypeService;
@Autowired
private WidgetsBundleService widgetsBundleService;
@Autowired
private TenantService tenantService;
@Autowired
private TenantProfileService tenantProfileService;
@Autowired
private CustomerService customerService;
@Autowired
private DeviceService deviceService;
@Autowired
private DeviceProfileService deviceProfileService;
@Autowired
private AttributesService attributesService;
@Autowired
private DeviceCredentialsService deviceCredentialsService;
@Autowired
private RuleChainService ruleChainService;
@Autowired
private TimeseriesService tsService;
@Autowired
private DeviceConnectivityConfiguration connectivityConfiguration;
@Value("${state.persistToTelemetry:false}")
@Getter
private boolean persistActivityToTelemetry;
@Lazy
@Autowired
private QueueService queueService;
@Autowired
private JwtSettingsService jwtSettingsService;
@Autowired
private NotificationSettingsService notificationSettingsService;
@Autowired
private NotificationTargetService notificationTargetService;
@Value("${security.jwt.tokenExpirationTime:9000}")
private Integer tokenExpirationTime;
@Value("${security.jwt.refreshTokenExpTime:604800}")
private Integer refreshTokenExpTime;
@Value("${security.jwt.tokenIssuer:thingsboard.io}")
private String tokenIssuer;
@Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}")
private String tokenSigningKey;
@Bean
protected BCryptPasswordEncoder passwordEncoder() {
@ -295,7 +269,42 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Override
public void createRandomJwtSettings() throws Exception {
jwtSettingsService.createRandomJwtSettings();
if (jwtSettingsService.getJwtSettings() == null) {
log.info("Creating JWT admin settings...");
var jwtSettings = new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey);
if (isSigningKeyDefault(jwtSettings) || !validateTokenSigningKeyLength(jwtSettings)) {
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
}
jwtSettingsService.saveJwtSettings(jwtSettings);
} else {
log.info("Skip creating JWT admin settings because they already exist.");
}
}
@Override
public void updateJwtSettings() {
JwtSettings jwtSettings = jwtSettingsService.getJwtSettings();
boolean invalidSignKey = false;
String warningMessage = null;
if (isSigningKeyDefault(jwtSettings)) {
warningMessage = "The platform is using the default JWT Signing Key, which is a security risk.";
invalidSignKey = true;
} else if (!validateTokenSigningKeyLength(jwtSettings)) {
warningMessage = "The JWT Signing Key is shorter than 512 bits, which is a security risk.";
invalidSignKey = true;
}
if (invalidSignKey) {
log.warn("WARNING: {}. A new JWT Signing Key has been added automatically. " +
"You can change the JWT Signing Key using the Web UI: " +
"Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage);
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
jwtSettingsService.saveJwtSettings(jwtSettings);
}
}
@Override

View File

@ -84,13 +84,17 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema
}
protected void executeQuery(String query) {
executeQuery(query, null);
}
protected void executeQuery(String query, String logQuery) {
logQuery = logQuery != null ? logQuery : query;
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
conn.createStatement().execute(query); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
log.info("Successfully executed query: {}", query);
log.info("Successfully executed query: {}", logQuery);
Thread.sleep(5000);
} catch (InterruptedException | SQLException e) {
log.error("Failed to execute query: {} due to: {}", query, e.getMessage());
throw new RuntimeException("Failed to execute query: " + query, e);
throw new RuntimeException("Failed to execute query: " + logQuery, e);
}
}

View File

@ -56,6 +56,7 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
@Override
public void createCustomerTitleUniqueConstraintIfNotExists() {
executeQuery("DO $$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'customer_title_unq_key') THEN " +
"ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;");
"ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;",
"create 'customer_title_unq_key' constraint if it doesn't already exist!");
}
}

View File

@ -25,6 +25,8 @@ public interface SystemDataLoaderService {
void createRandomJwtSettings() throws Exception;
void updateJwtSettings() throws Exception;
void createOAuth2Templates() throws Exception;
void loadSystemWidgets() throws Exception;

View File

@ -175,7 +175,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
NotificationSchedulerService notificationSchedulerService,
NotificationRuleProcessor notificationRuleProcessor,
TbImageService imageService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService,
eventPublisher, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer();

View File

@ -56,6 +56,7 @@ import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

View File

@ -48,6 +48,8 @@ import org.thingsboard.server.service.queue.TbPackProcessingContext;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import jakarta.annotation.PreDestroy;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

View File

@ -17,25 +17,24 @@ package org.thingsboard.server.service.security.auth.jwt.settings;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.NotificationCenter;
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.notification.targets.platform.SystemAdministratorsFilter;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
import static org.thingsboard.server.service.security.model.token.JwtTokenFactory.KEY_LENGTH;
@Service
@RequiredArgsConstructor
@Slf4j
@ -43,49 +42,11 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
private final AdminSettingsService adminSettingsService;
private final Optional<TbClusterService> tbClusterService;
private final Optional<NotificationCenter> notificationCenter;
private final JwtSettingsValidator jwtSettingsValidator;
@Value("${security.jwt.tokenExpirationTime:9000}")
private Integer tokenExpirationTime;
@Value("${security.jwt.refreshTokenExpTime:604800}")
private Integer refreshTokenExpTime;
@Value("${security.jwt.tokenIssuer:thingsboard.io}")
private String tokenIssuer;
@Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}")
private String tokenSigningKey;
private final Optional<JwtTokenFactory> jwtTokenFactory;
private volatile JwtSettings jwtSettings = null; //lazy init
/**
* Create JWT admin settings is intended to be called from Install scripts only
*/
@Override
public void createRandomJwtSettings() {
if (getJwtSettingsFromDb() == null) {
log.info("Creating JWT admin settings...");
this.jwtSettings = getJwtSettingsFromYml();
if (isSigningKeyDefault(jwtSettings)) {
this.jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
}
saveJwtSettings(jwtSettings);
} else {
log.info("Skip creating JWT admin settings because they already exist.");
}
}
/**
* Create JWT admin settings is intended to be called from Upgrade scripts only
*/
@Override
public void saveLegacyYmlSettings() {
log.info("Saving legacy JWT admin settings from YML...");
if (getJwtSettingsFromDb() == null) {
saveJwtSettings(getJwtSettingsFromYml());
}
}
@Override
public JwtSettings saveJwtSettings(JwtSettings jwtSettings) {
jwtSettingsValidator.validate(jwtSettings);
@ -105,7 +66,9 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
@Override
public JwtSettings reloadJwtSettings() {
log.trace("Executing reloadJwtSettings");
return getJwtSettings(true);
var settings = getJwtSettings(true);
jwtTokenFactory.ifPresent(JwtTokenFactory::reload);
return settings;
}
@Override
@ -118,30 +81,13 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
if (this.jwtSettings == null || forceReload) {
synchronized (this) {
if (this.jwtSettings == null || forceReload) {
JwtSettings result = getJwtSettingsFromDb();
if (result == null) {
result = getJwtSettingsFromYml();
log.warn("Loading the JWT settings from YML since there are no settings in DB. Looks like the upgrade script was not applied.");
}
if (isSigningKeyDefault(result)) {
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.");
notificationCenter.ifPresent(notificationCenter -> {
notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(), DefaultNotifications.jwtSigningKeyIssue.toTemplate());
});
}
this.jwtSettings = result;
jwtSettings = getJwtSettingsFromDb();
}
}
}
return this.jwtSettings;
}
private JwtSettings getJwtSettingsFromYml() {
return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey);
}
private JwtSettings getJwtSettingsFromDb() {
AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY);
return adminJwtSettings != null ? mapAdminToJwtSettings(adminJwtSettings) : null;
@ -161,8 +107,12 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
return adminJwtSettings;
}
private boolean isSigningKeyDefault(JwtSettings settings) {
public static boolean isSigningKeyDefault(JwtSettings settings) {
return TOKEN_SIGNING_KEY_DEFAULT.equals(settings.getTokenSigningKey());
}
public static boolean validateTokenSigningKeyLength(JwtSettings settings) {
return Base64.getDecoder().decode(settings.getTokenSigningKey()).length * Byte.SIZE >= KEY_LENGTH;
}
}

View File

@ -27,6 +27,9 @@ import java.util.Base64;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault;
import static org.thingsboard.server.service.security.model.token.JwtTokenFactory.KEY_LENGTH;
@Component
@RequiredArgsConstructor
public class DefaultJwtSettingsValidator implements JwtSettingsValidator {
@ -59,8 +62,8 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator {
if (Arrays.isNullOrEmpty(decodedKey)) {
throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!");
}
if (decodedKey.length * Byte.SIZE < 256 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) {
throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!");
if (decodedKey.length * Byte.SIZE < KEY_LENGTH && !isSigningKeyDefault(jwtSettings)) {
throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 512 bits of data!");
}
System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory

View File

@ -26,10 +26,6 @@ public interface JwtSettingsService {
JwtSettings reloadJwtSettings();
void createRandomJwtSettings();
void saveLegacyYmlSettings();
JwtSettings saveJwtSettings(JwtSettings jwtSettings);
}

View File

@ -16,16 +16,19 @@
package org.thingsboard.server.service.security.model.token;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ClaimsBuilder;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
@ -41,7 +44,10 @@ import org.thingsboard.server.service.security.exception.JwtExpiredTokenExceptio
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@ -53,6 +59,8 @@ import java.util.stream.Collectors;
@Slf4j
public class JwtTokenFactory {
public static int KEY_LENGTH = Jwts.SIG.HS512.getKeyBitLength();
private static final String SCOPES = "scopes";
private static final String USER_ID = "userId";
private static final String FIRST_NAME = "firstName";
@ -63,8 +71,12 @@ public class JwtTokenFactory {
private static final String CUSTOMER_ID = "customerId";
private static final String SESSION_ID = "sessionId";
@Lazy
private final JwtSettingsService jwtSettingsService;
private volatile JwtParser jwtParser;
private volatile SecretKey secretKey;
/**
* Factory method for issuing new JWT Tokens.
*/
@ -95,7 +107,7 @@ public class JwtTokenFactory {
public SecurityUser parseAccessJwtToken(String token) {
Jws<Claims> jwsClaims = parseTokenClaims(token);
Claims claims = jwsClaims.getBody();
Claims claims = jwsClaims.getPayload();
String subject = claims.getSubject();
@SuppressWarnings("unchecked")
List<String> scopes = claims.get(SCOPES, List.class);
@ -140,14 +152,14 @@ public class JwtTokenFactory {
String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), jwtSettingsService.getJwtSettings().getRefreshTokenExpTime())
.claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID)
.setId(UUID.randomUUID().toString()).compact();
.id(UUID.randomUUID().toString()).compact();
return new AccessJwtToken(token);
}
public SecurityUser parseRefreshToken(String token) {
Jws<Claims> jwsClaims = parseTokenClaims(token);
Claims claims = jwsClaims.getBody();
Claims claims = jwsClaims.getPayload();
String subject = claims.getSubject();
@SuppressWarnings("unchecked")
List<String> scopes = claims.get(SCOPES, List.class);
@ -176,6 +188,11 @@ public class JwtTokenFactory {
return new AccessJwtToken(jwtBuilder.compact());
}
public void reload() {
getSecretKey(true);
getJwtParser(true);
}
private JwtBuilder setUpToken(SecurityUser securityUser, List<String> scopes, long expirationTime) {
if (StringUtils.isBlank(securityUser.getEmail())) {
throw new IllegalArgumentException("Cannot create JWT Token without username/email");
@ -183,28 +200,27 @@ public class JwtTokenFactory {
UserPrincipal principal = securityUser.getUserPrincipal();
Claims claims = Jwts.claims().setSubject(principal.getValue());
claims.put(USER_ID, securityUser.getId().getId().toString());
claims.put(SCOPES, scopes);
ClaimsBuilder claimsBuilder = Jwts.claims()
.subject(principal.getValue())
.add(USER_ID, securityUser.getId().getId().toString())
.add(SCOPES, scopes);
if (securityUser.getSessionId() != null) {
claims.put(SESSION_ID, securityUser.getSessionId());
claimsBuilder.add(SESSION_ID, securityUser.getSessionId());
}
ZonedDateTime currentTime = ZonedDateTime.now();
return Jwts.builder()
.setClaims(claims)
.setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer())
.setIssuedAt(Date.from(currentTime.toInstant()))
.setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
.signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey());
.claims(claimsBuilder.build())
.issuer(jwtSettingsService.getJwtSettings().getTokenIssuer())
.issuedAt(Date.from(currentTime.toInstant()))
.expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
.signWith(getSecretKey(false), Jwts.SIG.HS512);
}
public Jws<Claims> parseTokenClaims(String token) {
try {
return Jwts.parser()
.setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey())
.parseClaimsJws(token);
return getJwtParser(false).parseSignedClaims(token);
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) {
log.debug("Invalid JWT Token", ex);
throw new BadCredentialsException("Invalid JWT token: ", ex);
@ -220,4 +236,28 @@ public class JwtTokenFactory {
return new JwtPair(accessToken.getToken(), refreshToken.getToken());
}
private SecretKey getSecretKey(boolean forceReload) {
if (secretKey == null || forceReload) {
synchronized (this) {
if (secretKey == null || forceReload) {
byte[] decodedToken = Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey());
secretKey = new SecretKeySpec(decodedToken, "HmacSHA512");
}
}
}
return secretKey;
}
private JwtParser getJwtParser(boolean forceReload) {
if (jwtParser == null || forceReload) {
synchronized (this) {
if (jwtParser == null || forceReload) {
jwtParser = Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey())))
.build();
}
}
}
return jwtParser;
}
}

View File

@ -22,10 +22,12 @@ import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@ -40,14 +42,14 @@ public class OAuth2AppTokenFactory {
public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) {
Jws<Claims> jwsClaims;
try {
jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken);
jwsClaims = Jwts.parser().verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(appSecret))).build().parseSignedClaims(appToken);
}
catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) {
throw new IllegalArgumentException("Invalid Application token: ", ex);
} catch (ExpiredJwtException expiredEx) {
throw new IllegalArgumentException("Application token expired", expiredEx);
}
Claims claims = jwsClaims.getBody();
Claims claims = jwsClaims.getPayload();
Date expiration = claims.getExpiration();
if (expiration == null) {
throw new IllegalArgumentException("Application token must have expiration date");

View File

@ -21,9 +21,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Jws;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
@ -121,6 +119,7 @@ import org.thingsboard.server.queue.memory.InMemoryStorage;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
@ -241,6 +240,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
@Autowired
protected ClaimDevicesService claimDevicesService;
@Autowired
private JwtTokenFactory jwtTokenFactory;
@SpyBean
protected MailService mailService;
@ -561,13 +563,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
protected void validateJwtToken(String token, String username) {
Assert.assertNotNull(token);
Assert.assertFalse(token.isEmpty());
int i = token.lastIndexOf('.');
Assert.assertTrue(i > 0);
String withoutSignature = token.substring(0, i + 1);
Jwt<Header, Claims> jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature);
Claims claims = jwsClaims.getBody();
Jws<Claims> jwsClaims = jwtTokenFactory.parseTokenClaims(token);
Claims claims = jwsClaims.getPayload();
String subject = claims.getSubject();
Assert.assertEquals(username, subject);
}

View File

@ -42,7 +42,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Slf4j
@DaoSqlTest
public class AdminControllerTest extends AbstractControllerTest {
final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey");
final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "QmlicmJkZk9tSzZPVFozcWY0Sm94UVhybmtBWXZ5YmZMOUZSZzZvcUFiOVhsb3VHUThhUWJGaXp3UHhtcGZ6Tw==");
@Test
public void testFindAdminSettingsByKey() throws Exception {
@ -168,7 +168,7 @@ public class AdminControllerTest extends AbstractControllerTest {
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(256 / Byte.SIZE).getBytes(StandardCharsets.UTF_8)));
RandomStringUtils.randomAlphanumeric(512 / Byte.SIZE).getBytes(StandardCharsets.UTF_8)));
doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk());

View File

@ -640,7 +640,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
}
private void verifyMsgProcessed(TbMsg tbMsg) {
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
await().atMost(15, TimeUnit.SECONDS).untilAsserted(() -> {
verify(actorContext, atLeastOnce()).tell(argThat(msg -> {
return ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId());
}));

View File

@ -16,16 +16,14 @@
package org.thingsboard.server.service.security.auth;
import io.jsonwebtoken.Claims;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.common.data.security.model.JwtToken;
@ -38,6 +36,8 @@ import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.AccessJwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.Optional;
@ -45,19 +45,13 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JwtTokenFactoryTest {
private JwtTokenFactory tokenFactory;
private AdminSettingsService adminSettingsService;
private NotificationCenter notificationCenter;
private JwtSettingsService jwtSettingsService;
private JwtSettings jwtSettings;
@ -66,12 +60,11 @@ public class JwtTokenFactoryTest {
public void beforeEach() {
jwtSettings = new JwtSettings();
jwtSettings.setTokenIssuer("tb");
jwtSettings.setTokenSigningKey("abewafaf");
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2));
jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7));
adminSettingsService = mock(AdminSettingsService.class);
notificationCenter = mock(NotificationCenter.class);
jwtSettingsService = mockJwtSettingsService();
mockJwtSettings(jwtSettings);
@ -169,21 +162,6 @@ public class JwtTokenFactoryTest {
});
}
@Test
public void testJwtSigningKeyIssueNotification() {
JwtSettings badJwtSettings = jwtSettings;
badJwtSettings.setTokenSigningKey(JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT);
mockJwtSettings(badJwtSettings);
jwtSettingsService = mockJwtSettingsService();
for (int i = 0; i < 5; i++) { // to check if notification is not sent twice
jwtSettingsService.getJwtSettings();
}
verify(notificationCenter, times(1)).sendGeneralWebNotification(eq(TenantId.SYS_TENANT_ID),
isA(SystemAdministratorsFilter.class), argThat(template -> template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.WEB)
.getBody().contains("The platform is configured to use default JWT Signing Key")));
}
private void mockJwtSettings(JwtSettings settings) {
AdminSettings adminJwtSettings = new AdminSettings();
adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(settings));
@ -192,12 +170,11 @@ public class JwtTokenFactoryTest {
}
private DefaultJwtSettingsService mockJwtSettingsService() {
return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(),
Optional.of(notificationCenter), new DefaultJwtSettingsValidator());
return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator(), Optional.empty());
}
private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) {
Claims claims = tokenFactory.parseTokenClaims(jwtToken.getToken()).getBody();
Claims claims = tokenFactory.parseTokenClaims(jwtToken.getToken()).getPayload();
assertThat(claims.getExpiration()).matches(actualExpirationTime -> {
Calendar expirationTime = Calendar.getInstance();
expirationTime.setTime(new Date());

View File

@ -63,12 +63,21 @@ public class DefaultRateLimitService implements RateLimitService {
@Override
public boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level) {
return checkRateLimit(api, tenantId, level, false);
}
@Override
public boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level, boolean ignoreTenantNotFound) {
if (tenantId.isSysTenantId()) {
return true;
}
TenantProfile tenantProfile = tenantProfileProvider.get(tenantId);
if (tenantProfile == null) {
throw new TenantProfileNotFoundException(tenantId);
if (ignoreTenantNotFound) {
return true;
} else {
throw new TenantProfileNotFoundException(tenantId);
}
}
String rateLimitConfig = tenantProfile.getProfileConfiguration()

View File

@ -24,6 +24,8 @@ public interface RateLimitService {
boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level);
boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level, boolean ignoreTenantNotFound);
boolean checkRateLimit(LimitedApi api, Object level, String rateLimitConfig);
void cleanUp(LimitedApi api, Object level);

View File

@ -48,7 +48,7 @@ public class JwtSettings {
* Key is used to sign {@link JwtToken}.
* Base64 encoded
*/
@Schema(description = "The JWT key is used to sing token. Base64 encoded.", example = "cTU4WnNqemI2aU5wbWVjdm1vYXRzanhjNHRUcXliMjE=")
@Schema(description = "The JWT key is used to sing token. Base64 encoded.", example = "dkVTUzU2M2VMWUNwVVltTUhQU2o5SUM0Tkc3M0k2Ykdwcm85QTl6R0RaQ252OFlmVDk2OEptZXBNcndGeExFZg==")
private String tokenSigningKey;
}

View File

@ -372,15 +372,6 @@ public class DefaultNotifications {
.build())
.build();
public static final DefaultNotification jwtSigningKeyIssue = DefaultNotification.builder()
.name("JWT Signing Key issue notification")
.type(NotificationType.GENERAL)
.subject("WARNING: security issue")
.text("The platform is configured to use default JWT Signing Key. Please change it on the security settings page")
.icon("warning").color(YELLOW_COLOR)
.button("Go to settings").link("/security-settings/general")
.build();
private final NotificationTemplateService templateService;
private final NotificationRuleService ruleService;

View File

@ -27,20 +27,20 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.stats.DefaultCounter;
import org.thingsboard.server.common.stats.StatsCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.cache.limits.RateLimitService;
import jakarta.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ -114,7 +114,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
boolean perTenantLimitReached = false;
TenantId tenantId = task.getTenantId();
if (tenantId != null && !tenantId.isSysTenantId()) {
if (!rateLimitService.checkRateLimit(LimitedApi.CASSANDRA_QUERIES, tenantId)) {
if (!rateLimitService.checkRateLimit(LimitedApi.CASSANDRA_QUERIES, tenantId, tenantId, true)) {
stats.incrementRateLimitedTenant(tenantId);
stats.getTotalRateLimited().increment();
settableFuture.setException(new TenantRateLimitException());

View File

@ -53,6 +53,14 @@ VALUES ( '23199d80-6e7e-11ee-8829-ef9fd52a6141', 1697719852888, '13814000-1dd2-1
"coaps":{"enabled":false,"host":"","port":"5684"}
}' );
INSERT INTO admin_settings ( id, created_time, tenant_id, key, json_value )
VALUES ( '1e33c6f0-061e-11ef-b5b7-dba0ee077a1b', 1714391189727, '13814000-1dd2-11b2-8080-808080808080', 'jwt', '{
"tokenExpirationTime": "9000",
"refreshTokenExpTime": "604800",
"tokenIssuer": "thingsboard.io",
"tokenSigningKey": "QmlicmJkZk9tSzZPVFozcWY0Sm94UVhybmtBWXZ5YmZMOUZSZzZvcUFiOVhsb3VHUThhUWJGaXp3UHhtcGZ6Tw=="
}' );
INSERT INTO queue ( id, created_time, tenant_id, name, topic, poll_interval, partitions, consumer_per_partition, pack_processing_timeout, submit_strategy, processing_strategy )
VALUES ( '6eaaefa6-4612-11e7-a919-92ebcb67fe33', 1592576748000 ,'13814000-1dd2-11b2-8080-808080808080', 'Main' ,'tb_rule_engine.main', 25, 10, true, 2000,
'{"type": "BURST", "batchSize": 1000}',

View File

@ -49,7 +49,7 @@
<spring-redis.version>6.2.4</spring-redis.version>
<spring-security.version>6.2.4</spring-security.version>
<jedis.version>5.1.2</jedis.version>
<jjwt.version>0.9.1</jjwt.version> <!-- 0.12.5 requires JWT usage refactoring-->
<jjwt.version>0.12.5</jjwt.version>
<slf4j.version>2.0.13</slf4j.version>
<log4j.version>2.23.1</log4j.version>
<logback.version>1.5.5</logback.version>

View File

@ -482,9 +482,9 @@
"issuer-name": "اسم الجهة المصدرة",
"issuer-name-required": "اسم الجهة المصدرة مطلوب.",
"signings-key": "مفتاح التوقيع",
"signings-key-hint": "سلسلة مشفرة بتنسيق Base64 تمثل ما لا يقل عن 256 بت من البيانات.",
"signings-key-hint": "سلسلة مشفرة بتنسيق Base64 تمثل ما لا يقل عن 512 بت من البيانات.",
"signings-key-required": "مفتاح التوقيع مطلوب.",
"signings-key-min-length": "يجب أن يكون مفتاح التوقيع ما لا يقل عن 256 بت من البيانات.",
"signings-key-min-length": "يجب أن يكون مفتاح التوقيع ما لا يقل عن 512 بت من البيانات.",
"signings-key-base64": "يجب أن يكون مفتاح التوقيع بتنسيق base64.",
"expiration-time": "وقت انتهاء صلاحية الرمز (ثانية)",
"expiration-time-required": "وقت انتهاء صلاحية الرمز مطلوب.",

View File

@ -457,9 +457,9 @@
"issuer-name": "Issuer name",
"issuer-name-required": "Issuer name is required.",
"signings-key": "Signing key",
"signings-key-hint": "Base64 encoded string representing at least 256 bits of data.",
"signings-key-hint": "Base64 encoded string representing at least 512 bits of data.",
"signings-key-required": "Signing key is required.",
"signings-key-min-length": "Signing key must be at least 256 bits of data.",
"signings-key-min-length": "Signing key must be at least 512 bits of data.",
"signings-key-base64": "Signing key must be base64 format.",
"expiration-time": "Token expiration time (sec)",
"expiration-time-required": "Token expiration time is required.",

View File

@ -431,9 +431,9 @@
"issuer-name": "Nombre del emisor",
"issuer-name-required": "Se requiere nombre del emisor.",
"signings-key": "Clave de firma",
"signings-key-hint": "Una string codificada en Base64 representando por lo menos 256 bits de datos.",
"signings-key-hint": "Una string codificada en Base64 representando por lo menos 512 bits de datos.",
"signings-key-required": "Se requiere clave de firma.",
"signings-key-min-length": "La clave de firma debe tener al menos 256 bits de datos.",
"signings-key-min-length": "La clave de firma debe tener al menos 512 bits de datos.",
"signings-key-base64": "La clave de firma debe estar en formato base64.",
"expiration-time": "Caducidad del token (en segundos)",
"expiration-time-required": "Se requiere caducidad del token.",

View File

@ -425,9 +425,9 @@
"issuer-name": "Naam van de uitgever",
"issuer-name-required": "De naam van de uitgever is vereist.",
"signings-key": "Sleutel ondertekenen",
"signings-key-hint": "Base64-gecodeerde tekenreeks die ten minste 256 bits aan gegevens vertegenwoordigt.",
"signings-key-hint": "Base64-gecodeerde tekenreeks die ten minste 512 bits aan gegevens vertegenwoordigt.",
"signings-key-required": "Ondertekeningssleutel is vereist.",
"signings-key-min-length": "De ondertekeningssleutel moet ten minste 256 bits aan gegevens bevatten.",
"signings-key-min-length": "De ondertekeningssleutel moet ten minste 512 bits aan gegevens bevatten.",
"signings-key-base64": "De ondertekeningssleutel moet de base64-indeling hebben.",
"expiration-time": "Vervaltijd token (sec)",
"expiration-time-required": "De vervaltijd van het token is vereist.",

View File

@ -457,9 +457,9 @@
"issuer-name": "Nazwa emitenta",
"issuer-name-required": "Nazwa emitenta jest wymagana.",
"signings-key": "Klucz do podpisu",
"signings-key-hint": "Ciąg zakodowany w formacie Base64 reprezentujący co najmniej 256 bitów danych.",
"signings-key-hint": "Ciąg zakodowany w formacie Base64 reprezentujący co najmniej 512 bitów danych.",
"signings-key-required": "Klucz do podpisu jest wymagany.",
"signings-key-min-length": "Klucz podpisujący musi mieć co najmniej 256 bitów danych.",
"signings-key-min-length": "Klucz podpisujący musi mieć co najmniej 512 bitów danych.",
"signings-key-base64": "Klucz podpisujący musi być w formacie base64.",
"expiration-time": "Czas ważności tokena (s)",
"expiration-time-required": "Czas ważności tokena jest wymagany.",

View File

@ -451,9 +451,9 @@
"issuer-name": "发行者名称",
"issuer-name-required": "发行者名称必填。",
"signings-key": "签名密钥",
"signings-key-hint": "Base64编码的字符串至少256位数据。",
"signings-key-hint": "Base64编码的字符串至少512位数据。",
"signings-key-required": "签名密钥必填。",
"signings-key-min-length": "签名密钥必须至少为256位的数据。",
"signings-key-min-length": "签名密钥必须至少为512位的数据。",
"signings-key-base64": "签名密钥必须是Base64格式。",
"expiration-time": "令牌过期时间(秒)",
"expiration-time-required": "令牌过期时间是必填。",