Merge branch 'jwt-random' of github.com:smatvienko-tb/thingsboard
This commit is contained in:
		
						commit
						0d1412cd46
					
				@ -29,6 +29,7 @@ import java.util.Arrays;
 | 
			
		||||
@ComponentScan({"org.thingsboard.server.install",
 | 
			
		||||
        "org.thingsboard.server.service.component",
 | 
			
		||||
        "org.thingsboard.server.service.install",
 | 
			
		||||
        "org.thingsboard.server.service.security.auth.jwt.settings",
 | 
			
		||||
        "org.thingsboard.server.dao",
 | 
			
		||||
        "org.thingsboard.server.common.stats",
 | 
			
		||||
        "org.thingsboard.server.common.transport.config.ssl",
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,9 @@ import com.google.common.util.concurrent.MoreExecutors;
 | 
			
		||||
import io.swagger.annotations.ApiOperation;
 | 
			
		||||
import io.swagger.annotations.ApiParam;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.context.annotation.Lazy;
 | 
			
		||||
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;
 | 
			
		||||
@ -37,8 +39,13 @@ 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.common.data.sync.vc.RepositorySettingsInfo;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtSettings;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
 | 
			
		||||
import org.thingsboard.server.dao.settings.AdminSettingsService;
 | 
			
		||||
import org.thingsboard.server.queue.util.TbCoreComponent;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtPair;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
 | 
			
		||||
import org.thingsboard.server.service.security.permission.Operation;
 | 
			
		||||
import org.thingsboard.server.service.security.permission.Resource;
 | 
			
		||||
import org.thingsboard.server.service.security.system.SystemSecurityService;
 | 
			
		||||
@ -65,6 +72,14 @@ public class AdminController extends BaseController {
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private SystemSecurityService systemSecurityService;
 | 
			
		||||
 | 
			
		||||
    @Lazy
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private JwtSettingsService jwtSettingsService;
 | 
			
		||||
 | 
			
		||||
    @Lazy
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private JwtTokenFactory tokenFactory;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private EntitiesVersionControlService versionControlService;
 | 
			
		||||
 | 
			
		||||
@ -152,6 +167,40 @@ 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 (saveJwtSettings)",
 | 
			
		||||
            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 JwtPair saveJwtSettings(
 | 
			
		||||
            @ApiParam(value = "A JSON value representing the JWT Settings.")
 | 
			
		||||
            @RequestBody JwtSettings jwtSettings) throws ThingsboardException {
 | 
			
		||||
        try {
 | 
			
		||||
            SecurityUser securityUser = getCurrentUser();
 | 
			
		||||
            accessControlService.checkPermission(securityUser, Resource.ADMIN_SETTINGS, Operation.WRITE);
 | 
			
		||||
            checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings));
 | 
			
		||||
            return tokenFactory.createTokenPair(securityUser);
 | 
			
		||||
        } 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)
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
 | 
			
		||||
import org.thingsboard.server.service.security.model.ActivateUserRequest;
 | 
			
		||||
import org.thingsboard.server.service.security.model.ChangePasswordRequest;
 | 
			
		||||
import org.thingsboard.server.service.security.model.JwtTokenPair;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtPair;
 | 
			
		||||
import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest;
 | 
			
		||||
import org.thingsboard.server.service.security.model.ResetPasswordRequest;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
@ -236,7 +236,7 @@ public class AuthController extends BaseController {
 | 
			
		||||
    @RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
 | 
			
		||||
    @ResponseStatus(value = HttpStatus.OK)
 | 
			
		||||
    @ResponseBody
 | 
			
		||||
    public JwtTokenPair activateUser(
 | 
			
		||||
    public JwtPair activateUser(
 | 
			
		||||
            @ApiParam(value = "Activate user request.")
 | 
			
		||||
            @RequestBody ActivateUserRequest activateRequest,
 | 
			
		||||
            @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
 | 
			
		||||
@ -278,7 +278,7 @@ public class AuthController extends BaseController {
 | 
			
		||||
    @RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
 | 
			
		||||
    @ResponseStatus(value = HttpStatus.OK)
 | 
			
		||||
    @ResponseBody
 | 
			
		||||
    public JwtTokenPair resetPassword(
 | 
			
		||||
    public JwtPair resetPassword(
 | 
			
		||||
            @ApiParam(value = "Reset password request.")
 | 
			
		||||
            @RequestBody ResetPasswordRequest resetPasswordRequest,
 | 
			
		||||
            HttpServletRequest request) throws ThingsboardException {
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
 | 
			
		||||
import org.thingsboard.server.service.security.model.JwtTokenPair;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtPair;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
 | 
			
		||||
import org.thingsboard.server.service.security.system.SystemSecurityService;
 | 
			
		||||
@ -87,7 +87,7 @@ public class TwoFactorAuthController extends BaseController {
 | 
			
		||||
                    "and Too Many Requests error if rate limits are exceeded.")
 | 
			
		||||
    @PostMapping("/verification/check")
 | 
			
		||||
    @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
 | 
			
		||||
    public JwtTokenPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType,
 | 
			
		||||
    public JwtPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType,
 | 
			
		||||
                                              @RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
        boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
 | 
			
		||||
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
 | 
			
		||||
import org.thingsboard.server.queue.util.TbCoreComponent;
 | 
			
		||||
import org.thingsboard.server.service.entitiy.user.TbUserService;
 | 
			
		||||
import org.thingsboard.server.service.security.model.JwtTokenPair;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtPair;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
import org.thingsboard.server.service.security.model.UserPrincipal;
 | 
			
		||||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
 | 
			
		||||
@ -145,7 +145,7 @@ public class UserController extends BaseController {
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
 | 
			
		||||
    @RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET)
 | 
			
		||||
    @ResponseBody
 | 
			
		||||
    public JwtTokenPair getUserToken(
 | 
			
		||||
    public JwtPair getUserToken(
 | 
			
		||||
            @ApiParam(value = USER_ID_PARAM_DESCRIPTION)
 | 
			
		||||
            @PathVariable(USER_ID) String strUserId) throws ThingsboardException {
 | 
			
		||||
        checkParameter(USER_ID, strUserId);
 | 
			
		||||
 | 
			
		||||
@ -273,6 +273,7 @@ public class ThingsboardInstallService {
 | 
			
		||||
                systemDataLoaderService.createSysAdmin();
 | 
			
		||||
                systemDataLoaderService.createDefaultTenantProfiles();
 | 
			
		||||
                systemDataLoaderService.createAdminSettings();
 | 
			
		||||
                systemDataLoaderService.createRandomJwtSettings();
 | 
			
		||||
                systemDataLoaderService.loadSystemWidgets();
 | 
			
		||||
                systemDataLoaderService.createOAuth2Templates();
 | 
			
		||||
                systemDataLoaderService.createQueues();
 | 
			
		||||
 | 
			
		||||
@ -82,6 +82,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
 | 
			
		||||
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
 | 
			
		||||
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
 | 
			
		||||
import org.thingsboard.server.common.data.widget.WidgetsBundle;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
 | 
			
		||||
import org.thingsboard.server.dao.attributes.AttributesService;
 | 
			
		||||
import org.thingsboard.server.dao.customer.CustomerService;
 | 
			
		||||
import org.thingsboard.server.dao.device.DeviceCredentialsService;
 | 
			
		||||
@ -167,6 +168,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private QueueService queueService;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    private JwtSettingsService jwtSettingsService;
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    protected BCryptPasswordEncoder passwordEncoder() {
 | 
			
		||||
        return new BCryptPasswordEncoder();
 | 
			
		||||
@ -263,6 +267,16 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
 | 
			
		||||
        adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void createRandomJwtSettings() throws Exception {
 | 
			
		||||
        jwtSettingsService.createRandomJwtSettings();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void saveLegacyYmlSettings() throws Exception {
 | 
			
		||||
        jwtSettingsService.saveLegacyYmlSettings();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void createOAuth2Templates() throws Exception {
 | 
			
		||||
        installScripts.createOAuth2Templates();
 | 
			
		||||
@ -656,4 +670,5 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
 | 
			
		||||
            queueService.saveQueue(sequentialByOriginatorQueue);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,10 @@ public interface SystemDataLoaderService {
 | 
			
		||||
 | 
			
		||||
    void createAdminSettings() throws Exception;
 | 
			
		||||
 | 
			
		||||
    void createRandomJwtSettings() throws Exception;
 | 
			
		||||
 | 
			
		||||
    void saveLegacyYmlSettings() throws Exception;
 | 
			
		||||
 | 
			
		||||
    void createOAuth2Templates() throws Exception;
 | 
			
		||||
 | 
			
		||||
    void loadSystemWidgets() throws Exception;
 | 
			
		||||
 | 
			
		||||
@ -186,6 +186,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
 | 
			
		||||
                break;
 | 
			
		||||
            case "3.4.1":
 | 
			
		||||
                log.info("Updating data from version 3.4.1 to 3.4.2 ...");
 | 
			
		||||
                systemDataLoaderService.saveLegacyYmlSettings();
 | 
			
		||||
                boolean skipAuditLogsMigration = getEnv("TB_SKIP_AUDIT_LOGS_MIGRATION", false);
 | 
			
		||||
                if (!skipAuditLogsMigration) {
 | 
			
		||||
                    log.info("Starting audit logs migration. Can be skipped with TB_SKIP_AUDIT_LOGS_MIGRATION env variable set to true");
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
 | 
			
		||||
import org.thingsboard.server.common.msg.queue.TbCallback;
 | 
			
		||||
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
 | 
			
		||||
import org.thingsboard.server.common.stats.StatsFactory;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
 | 
			
		||||
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
 | 
			
		||||
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
 | 
			
		||||
import org.thingsboard.server.gen.transport.TransportProtos;
 | 
			
		||||
@ -143,8 +144,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
 | 
			
		||||
                                        EdgeNotificationService edgeNotificationService,
 | 
			
		||||
                                        OtaPackageStateService firmwareStateService,
 | 
			
		||||
                                        GitVersionControlQueueService vcQueueService,
 | 
			
		||||
                                        PartitionService partitionService) {
 | 
			
		||||
        super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer());
 | 
			
		||||
                                        PartitionService partitionService,
 | 
			
		||||
                                        Optional<JwtSettingsService> jwtSettingsService) {
 | 
			
		||||
        super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
 | 
			
		||||
        this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
 | 
			
		||||
        this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
 | 
			
		||||
        this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer();
 | 
			
		||||
 | 
			
		||||
@ -70,6 +70,7 @@ import java.util.Collections;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
@ -126,7 +127,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
 | 
			
		||||
                                              TbTenantProfileCache tenantProfileCache,
 | 
			
		||||
                                              TbApiUsageStateService apiUsageStateService,
 | 
			
		||||
                                              PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, QueueService queueService) {
 | 
			
		||||
        super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer());
 | 
			
		||||
        super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), Optional.empty());
 | 
			
		||||
        this.statisticsService = statisticsService;
 | 
			
		||||
        this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory;
 | 
			
		||||
        this.submitStrategyFactory = submitStrategyFactory;
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.TbActorMsg;
 | 
			
		||||
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
 | 
			
		||||
import org.thingsboard.server.common.msg.queue.ServiceType;
 | 
			
		||||
import org.thingsboard.server.common.msg.queue.TbCallback;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
 | 
			
		||||
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
 | 
			
		||||
import org.thingsboard.server.queue.TbQueueConsumer;
 | 
			
		||||
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
 | 
			
		||||
@ -76,11 +77,13 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
 | 
			
		||||
    protected final PartitionService partitionService;
 | 
			
		||||
 | 
			
		||||
    protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer;
 | 
			
		||||
    protected final Optional<JwtSettingsService> jwtSettingsService;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
 | 
			
		||||
                                   TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache,
 | 
			
		||||
                                   TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService,
 | 
			
		||||
                                   PartitionService partitionService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer) {
 | 
			
		||||
                                   PartitionService partitionService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer, Optional<JwtSettingsService> jwtSettingsService) {
 | 
			
		||||
        this.actorContext = actorContext;
 | 
			
		||||
        this.encodingService = encodingService;
 | 
			
		||||
        this.tenantProfileCache = tenantProfileCache;
 | 
			
		||||
@ -89,6 +92,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
 | 
			
		||||
        this.apiUsageStateService = apiUsageStateService;
 | 
			
		||||
        this.partitionService = partitionService;
 | 
			
		||||
        this.nfConsumer = nfConsumer;
 | 
			
		||||
        this.jwtSettingsService = jwtSettingsService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void init(String mainConsumerThreadName, String nfConsumerThreadName) {
 | 
			
		||||
@ -172,6 +176,10 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
 | 
			
		||||
                        apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
 | 
			
		||||
                    if (TenantId.SYS_TENANT_ID.equals(componentLifecycleMsg.getTenantId())) {
 | 
			
		||||
                        jwtSettingsService.ifPresent(JwtSettingsService::reloadJwtSettings);
 | 
			
		||||
                        return;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        tenantProfileCache.evict(componentLifecycleMsg.getTenantId());
 | 
			
		||||
                        partitionService.removeTenant(componentLifecycleMsg.getTenantId());
 | 
			
		||||
                        if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
 | 
			
		||||
@ -179,6 +187,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
 | 
			
		||||
                        } else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
 | 
			
		||||
                            apiUsageStateService.onTenantDelete((TenantId) componentLifecycleMsg.getEntityId());
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
 | 
			
		||||
                    deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
 | 
			
		||||
                } else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,168 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2022 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.service.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.context.annotation.Lazy;
 | 
			
		||||
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.common.data.security.model.JwtSettings;
 | 
			
		||||
import org.thingsboard.server.dao.settings.AdminSettingsService;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.PostConstruct;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.Base64;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsValidator.ADMIN_SETTINGS_JWT_KEY;
 | 
			
		||||
import static org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsValidator.TOKEN_SIGNING_KEY_DEFAULT;
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class DefaultJwtSettingsService implements JwtSettingsService {
 | 
			
		||||
 | 
			
		||||
    @Lazy
 | 
			
		||||
    private final AdminSettingsService adminSettingsService;
 | 
			
		||||
    @Lazy
 | 
			
		||||
    private final Optional<TbClusterService> tbClusterService;
 | 
			
		||||
    private final JwtSettingsValidator jwtSettingsValidator;
 | 
			
		||||
    private volatile JwtSettings jwtSettings = null; //lazy init
 | 
			
		||||
    @Value("${install.upgrade:false}")
 | 
			
		||||
    private boolean isUpgrade;
 | 
			
		||||
 | 
			
		||||
    @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;
 | 
			
		||||
 | 
			
		||||
    @PostConstruct
 | 
			
		||||
    public void init() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void reloadJwtSettings() {
 | 
			
		||||
        AdminSettings adminJwtSettings = findJwtAdminSettings();
 | 
			
		||||
        if (adminJwtSettings != null) {
 | 
			
		||||
            log.info("Reloading the JWT admin settings from database");
 | 
			
		||||
            synchronized (this) {
 | 
			
		||||
                this.jwtSettings = mapAdminToJwtSettings(adminJwtSettings);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (hasDefaultTokenSigningKey()) {
 | 
			
		||||
            log.warn("WARNING: The platform is configured to use default JWT Signing Key. " +
 | 
			
		||||
                    "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " +
 | 
			
		||||
                    "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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(getJwtSettings().getTokenSigningKey());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create JWT admin settings is intended to be called from Install scripts only
 | 
			
		||||
     * */
 | 
			
		||||
    @Override
 | 
			
		||||
    public void createRandomJwtSettings() {
 | 
			
		||||
        log.info("Creating JWT admin settings...");
 | 
			
		||||
        Objects.requireNonNull(getJwtSettings(), "JWT settings is null");
 | 
			
		||||
 | 
			
		||||
        if (hasDefaultTokenSigningKey()) {
 | 
			
		||||
            log.info("JWT token signing key is default. Generating a new random key");
 | 
			
		||||
            getJwtSettings().setTokenSigningKey(Base64.getEncoder().encodeToString(
 | 
			
		||||
                    RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
 | 
			
		||||
        }
 | 
			
		||||
        saveJwtSettings(getJwtSettings());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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...");
 | 
			
		||||
        Objects.requireNonNull(getJwtSettings(), "JWT settings is null");
 | 
			
		||||
        if (isJwtAdminSettingsNotExists()) {
 | 
			
		||||
            saveJwtSettings(getJwtSettings());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public JwtSettings saveJwtSettings(JwtSettings jwtSettings) {
 | 
			
		||||
        jwtSettingsValidator.validate(jwtSettings);
 | 
			
		||||
        final AdminSettings adminJwtSettings = mapJwtToAdminSettings(jwtSettings);
 | 
			
		||||
        final AdminSettings existedSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY);
 | 
			
		||||
        if (existedSettings != null) {
 | 
			
		||||
            adminJwtSettings.setId(existedSettings.getId());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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.ifPresent(cs -> cs.broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, ComponentLifecycleEvent.UPDATED));
 | 
			
		||||
        reloadJwtSettings();
 | 
			
		||||
        return getJwtSettings();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean isJwtAdminSettingsNotExists() {
 | 
			
		||||
        return findJwtAdminSettings() == null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    AdminSettings findJwtAdminSettings() {
 | 
			
		||||
        return adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public JwtSettings getJwtSettings() {
 | 
			
		||||
        if (this.jwtSettings == null) {
 | 
			
		||||
            synchronized (this) {
 | 
			
		||||
                if (this.jwtSettings == null) {
 | 
			
		||||
                    this.jwtSettings = new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey);
 | 
			
		||||
                    reloadJwtSettings();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return this.jwtSettings;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,69 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2022 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.service.security.auth.jwt.settings;
 | 
			
		||||
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import org.apache.commons.lang3.RandomUtils;
 | 
			
		||||
import org.apache.commons.lang3.StringUtils;
 | 
			
		||||
import org.bouncycastle.util.Arrays;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtSettings;
 | 
			
		||||
import org.thingsboard.server.dao.exception.DataValidationException;
 | 
			
		||||
 | 
			
		||||
import java.util.Base64;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
@Component
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class DefaultJwtSettingsValidator implements JwtSettingsValidator {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void validate(JwtSettings jwtSettings) {
 | 
			
		||||
        if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) {
 | 
			
		||||
            throw new DataValidationException("JWT token issuer should be specified!");
 | 
			
		||||
        }
 | 
			
		||||
        if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) {
 | 
			
		||||
            throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!");
 | 
			
		||||
        }
 | 
			
		||||
        if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) {
 | 
			
		||||
            throw new DataValidationException("JWT token expiration time should be at least 1 minute!");
 | 
			
		||||
        }
 | 
			
		||||
        if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) {
 | 
			
		||||
            throw new DataValidationException("JWT token expiration time should greater than JWT refresh token expiration time!");
 | 
			
		||||
        }
 | 
			
		||||
        if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) {
 | 
			
		||||
            throw new DataValidationException("JWT token signing key should be specified!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        byte[] decodedKey;
 | 
			
		||||
        try {
 | 
			
		||||
            decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey());
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            throw new DataValidationException("JWT token signing key should be a valid Base64 encoded string! " + e.getMessage());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (Arrays.isNullOrEmpty(decodedKey)) {
 | 
			
		||||
            throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!");
 | 
			
		||||
        }
 | 
			
		||||
        if (decodedKey.length * Byte.SIZE < 256 && !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!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2022 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.service.security.auth.jwt.settings;
 | 
			
		||||
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import org.springframework.context.annotation.Primary;
 | 
			
		||||
import org.springframework.context.annotation.Profile;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtSettings;
 | 
			
		||||
 | 
			
		||||
@Primary
 | 
			
		||||
@Profile("install")
 | 
			
		||||
@Component
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public class InstallJwtSettingsValidator implements JwtSettingsValidator {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * During Install or upgrade the validation is suppressed to keep existing data
 | 
			
		||||
     * */
 | 
			
		||||
    @Override
 | 
			
		||||
    public void validate(JwtSettings jwtSettings) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,32 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2022 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.service.security.auth.jwt.settings;
 | 
			
		||||
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtSettings;
 | 
			
		||||
 | 
			
		||||
public interface JwtSettingsService {
 | 
			
		||||
 | 
			
		||||
    JwtSettings getJwtSettings();
 | 
			
		||||
 | 
			
		||||
    void reloadJwtSettings();
 | 
			
		||||
 | 
			
		||||
    void createRandomJwtSettings();
 | 
			
		||||
 | 
			
		||||
    void saveLegacyYmlSettings();
 | 
			
		||||
 | 
			
		||||
    JwtSettings saveJwtSettings(JwtSettings jwtSettings);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2022 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.service.security.auth.jwt.settings;
 | 
			
		||||
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtSettings;
 | 
			
		||||
 | 
			
		||||
public interface JwtSettingsValidator {
 | 
			
		||||
    String ADMIN_SETTINGS_JWT_KEY = "jwt";
 | 
			
		||||
    String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey";
 | 
			
		||||
 | 
			
		||||
    void validate(JwtSettings jwtSettings);
 | 
			
		||||
}
 | 
			
		||||
@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
 | 
			
		||||
import org.thingsboard.server.dao.oauth2.OAuth2Service;
 | 
			
		||||
import org.thingsboard.server.queue.util.TbCoreComponent;
 | 
			
		||||
import org.thingsboard.server.service.security.model.JwtTokenPair;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtPair;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
 | 
			
		||||
@ -104,7 +104,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
 | 
			
		||||
            SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
 | 
			
		||||
                    registration);
 | 
			
		||||
 | 
			
		||||
            JwtTokenPair tokenPair = tokenFactory.createTokenPair(securityUser);
 | 
			
		||||
            JwtPair tokenPair = tokenFactory.createTokenPair(securityUser);
 | 
			
		||||
 | 
			
		||||
            clearAuthenticationAttributes(request, response);
 | 
			
		||||
            getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken());
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ import org.springframework.stereotype.Component;
 | 
			
		||||
import org.thingsboard.server.common.data.security.Authority;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
 | 
			
		||||
import org.thingsboard.server.service.security.model.JwtTokenPair;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtPair;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
 | 
			
		||||
 | 
			
		||||
@ -49,7 +49,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
 | 
			
		||||
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
 | 
			
		||||
                                        Authentication authentication) throws IOException, ServletException {
 | 
			
		||||
        SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
 | 
			
		||||
        JwtTokenPair tokenPair = new JwtTokenPair();
 | 
			
		||||
        JwtPair tokenPair = new JwtPair();
 | 
			
		||||
 | 
			
		||||
        if (authentication instanceof MfaAuthenticationToken) {
 | 
			
		||||
            int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true)
 | 
			
		||||
 | 
			
		||||
@ -24,8 +24,8 @@ import io.jsonwebtoken.MalformedJwtException;
 | 
			
		||||
import io.jsonwebtoken.SignatureAlgorithm;
 | 
			
		||||
import io.jsonwebtoken.SignatureException;
 | 
			
		||||
import io.jsonwebtoken.UnsupportedJwtException;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.security.authentication.BadCredentialsException;
 | 
			
		||||
import org.springframework.security.core.GrantedAuthority;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
@ -35,9 +35,9 @@ import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.UserId;
 | 
			
		||||
import org.thingsboard.server.common.data.security.Authority;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtToken;
 | 
			
		||||
import org.thingsboard.server.config.JwtSettings;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
 | 
			
		||||
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
 | 
			
		||||
import org.thingsboard.server.service.security.model.JwtTokenPair;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtPair;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
import org.thingsboard.server.service.security.model.UserPrincipal;
 | 
			
		||||
 | 
			
		||||
@ -49,6 +49,7 @@ import java.util.UUID;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
@Component
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class JwtTokenFactory {
 | 
			
		||||
 | 
			
		||||
@ -62,12 +63,7 @@ public class JwtTokenFactory {
 | 
			
		||||
    private static final String CUSTOMER_ID = "customerId";
 | 
			
		||||
    private static final String SESSION_ID = "sessionId";
 | 
			
		||||
 | 
			
		||||
    private final JwtSettings settings;
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    public JwtTokenFactory(JwtSettings settings) {
 | 
			
		||||
        this.settings = settings;
 | 
			
		||||
    }
 | 
			
		||||
    private final JwtSettingsService jwtSettingsService;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Factory method for issuing new JWT Tokens.
 | 
			
		||||
@ -80,7 +76,7 @@ public class JwtTokenFactory {
 | 
			
		||||
        UserPrincipal principal = securityUser.getUserPrincipal();
 | 
			
		||||
 | 
			
		||||
        JwtBuilder jwtBuilder = setUpToken(securityUser, securityUser.getAuthorities().stream()
 | 
			
		||||
                .map(GrantedAuthority::getAuthority).collect(Collectors.toList()), settings.getTokenExpirationTime());
 | 
			
		||||
                .map(GrantedAuthority::getAuthority).collect(Collectors.toList()), jwtSettingsService.getJwtSettings().getTokenExpirationTime());
 | 
			
		||||
        jwtBuilder.claim(FIRST_NAME, securityUser.getFirstName())
 | 
			
		||||
                .claim(LAST_NAME, securityUser.getLastName())
 | 
			
		||||
                .claim(ENABLED, securityUser.isEnabled())
 | 
			
		||||
@ -142,7 +138,7 @@ public class JwtTokenFactory {
 | 
			
		||||
    public JwtToken createRefreshToken(SecurityUser securityUser) {
 | 
			
		||||
        UserPrincipal principal = securityUser.getUserPrincipal();
 | 
			
		||||
 | 
			
		||||
        String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), settings.getRefreshTokenExpTime())
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
@ -198,16 +194,16 @@ public class JwtTokenFactory {
 | 
			
		||||
 | 
			
		||||
        return Jwts.builder()
 | 
			
		||||
                .setClaims(claims)
 | 
			
		||||
                .setIssuer(settings.getTokenIssuer())
 | 
			
		||||
                .setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer())
 | 
			
		||||
                .setIssuedAt(Date.from(currentTime.toInstant()))
 | 
			
		||||
                .setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
 | 
			
		||||
                .signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey());
 | 
			
		||||
                .signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Jws<Claims> parseTokenClaims(JwtToken token) {
 | 
			
		||||
        try {
 | 
			
		||||
            return Jwts.parser()
 | 
			
		||||
                    .setSigningKey(settings.getTokenSigningKey())
 | 
			
		||||
                    .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey())
 | 
			
		||||
                    .parseClaimsJws(token.getToken());
 | 
			
		||||
        } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) {
 | 
			
		||||
            log.debug("Invalid JWT Token", ex);
 | 
			
		||||
@ -218,10 +214,10 @@ public class JwtTokenFactory {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public JwtTokenPair createTokenPair(SecurityUser securityUser) {
 | 
			
		||||
    public JwtPair createTokenPair(SecurityUser securityUser) {
 | 
			
		||||
        JwtToken accessToken = createAccessJwtToken(securityUser);
 | 
			
		||||
        JwtToken refreshToken = createRefreshToken(securityUser);
 | 
			
		||||
        return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
 | 
			
		||||
        return new JwtPair(accessToken.getToken(), refreshToken.getToken());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -107,11 +107,11 @@ plugins:
 | 
			
		||||
# Security parameters
 | 
			
		||||
security:
 | 
			
		||||
  # JWT Token parameters
 | 
			
		||||
  jwt:
 | 
			
		||||
  jwt: # Since 3.4.2 values are persisted to the database during install or upgrade. On Install, the key will be generated randomly if no custom value set. You can change it later from Web UI under SYS_ADMIN
 | 
			
		||||
    tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours)
 | 
			
		||||
    refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week).
 | 
			
		||||
    tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
 | 
			
		||||
    tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}"
 | 
			
		||||
    tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" # Base64 encoded
 | 
			
		||||
  # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator
 | 
			
		||||
  user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}"
 | 
			
		||||
  # Enable/disable case-sensitive username login
 | 
			
		||||
 | 
			
		||||
@ -17,14 +17,21 @@ package org.thingsboard.server.controller;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.commons.lang3.RandomStringUtils;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.mockito.Mockito;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.rule.engine.api.MailService;
 | 
			
		||||
import org.thingsboard.server.common.data.AdminSettings;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtSettings;
 | 
			
		||||
import org.thingsboard.server.service.mail.DefaultMailService;
 | 
			
		||||
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.Base64;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.hamcrest.Matchers.containsString;
 | 
			
		||||
import static org.hamcrest.Matchers.is;
 | 
			
		||||
import static org.hamcrest.Matchers.notNullValue;
 | 
			
		||||
@ -32,8 +39,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 | 
			
		||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 | 
			
		||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
public abstract class BaseAdminControllerTest extends AbstractControllerTest {
 | 
			
		||||
    final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey");
 | 
			
		||||
 | 
			
		||||
    @Autowired
 | 
			
		||||
    MailService mailService;
 | 
			
		||||
@ -139,4 +147,48 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
 | 
			
		||||
        doPost("/api/admin/settings/testMail", adminSettings).andExpect(status().is5xxServerError());
 | 
			
		||||
        Mockito.doNothing().when(mailService).sendTestMail(Mockito.any(), Mockito.any());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void resetJwtSettingsToDefault() throws Exception {
 | 
			
		||||
        loginSysAdmin();
 | 
			
		||||
        doPost("/api/admin/jwtSettings", defaultJwtSettings).andExpect(status().isOk()); // jwt test scenarios are always started from
 | 
			
		||||
        loginTenantAdmin();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testGetAndSaveDefaultJwtSettings() throws Exception {
 | 
			
		||||
        JwtSettings jwtSettings;
 | 
			
		||||
        loginSysAdmin();
 | 
			
		||||
 | 
			
		||||
        jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
 | 
			
		||||
        assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
 | 
			
		||||
 | 
			
		||||
        doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk());
 | 
			
		||||
 | 
			
		||||
        jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
 | 
			
		||||
        assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
 | 
			
		||||
 | 
			
		||||
        resetJwtSettingsToDefault();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testCreateJwtSettings() throws Exception {
 | 
			
		||||
        loginSysAdmin();
 | 
			
		||||
 | 
			
		||||
        JwtSettings jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
 | 
			
		||||
        assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
 | 
			
		||||
 | 
			
		||||
        jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
 | 
			
		||||
                RandomStringUtils.randomAlphanumeric(256 / Byte.SIZE).getBytes(StandardCharsets.UTF_8)));
 | 
			
		||||
 | 
			
		||||
        doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk());
 | 
			
		||||
 | 
			
		||||
        doGet("/api/admin/jwtSettings").andExpect(status().isUnauthorized()); //the old JWT token does not work after signing key was changed!
 | 
			
		||||
 | 
			
		||||
        loginSysAdmin();
 | 
			
		||||
        JwtSettings newJwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
 | 
			
		||||
        assertThat(jwtSettings).isEqualTo(newJwtSettings);
 | 
			
		||||
 | 
			
		||||
        resetJwtSettingsToDefault();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,7 @@ import org.thingsboard.server.dao.user.UserService;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
 | 
			
		||||
import org.thingsboard.server.service.security.model.JwtTokenPair;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtPair;
 | 
			
		||||
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
@ -396,7 +396,7 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
 | 
			
		||||
    private void logInWithPreVerificationToken(String username, String password) throws Exception {
 | 
			
		||||
        LoginRequest loginRequest = new LoginRequest(username, password);
 | 
			
		||||
 | 
			
		||||
        JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class);
 | 
			
		||||
        JwtPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtPair.class);
 | 
			
		||||
        assertThat(response.getToken()).isNotNull();
 | 
			
		||||
        assertThat(response.getRefreshToken()).isNull();
 | 
			
		||||
        assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN);
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,8 @@ import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.UserId;
 | 
			
		||||
import org.thingsboard.server.common.data.security.Authority;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtToken;
 | 
			
		||||
import org.thingsboard.server.config.JwtSettings;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtSettings;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
import org.thingsboard.server.service.security.model.UserPrincipal;
 | 
			
		||||
import org.thingsboard.server.service.security.model.token.AccessJwtToken;
 | 
			
		||||
@ -36,6 +37,8 @@ import java.util.UUID;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.mockito.BDDMockito.willReturn;
 | 
			
		||||
import static org.mockito.Mockito.mock;
 | 
			
		||||
 | 
			
		||||
public class JwtTokenFactoryTest {
 | 
			
		||||
 | 
			
		||||
@ -50,7 +53,10 @@ public class JwtTokenFactoryTest {
 | 
			
		||||
        jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2));
 | 
			
		||||
        jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7));
 | 
			
		||||
 | 
			
		||||
        tokenFactory = new JwtTokenFactory(jwtSettings);
 | 
			
		||||
        JwtSettingsService jwtSettingsService = mock(JwtSettingsService.class);
 | 
			
		||||
        willReturn(jwtSettings).given(jwtSettingsService).getJwtSettings();
 | 
			
		||||
 | 
			
		||||
        tokenFactory = new JwtTokenFactory(jwtSettingsService);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
 | 
			
		||||
@ -13,19 +13,18 @@
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.service.security.model;
 | 
			
		||||
package org.thingsboard.server.common.data.security.model;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
import org.thingsboard.server.common.data.security.Authority;
 | 
			
		||||
 | 
			
		||||
@ApiModel(value = "JWT Token Pair")
 | 
			
		||||
@ApiModel(value = "JWT Pair")
 | 
			
		||||
@Data
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
public class JwtTokenPair {
 | 
			
		||||
public class JwtPair {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(position = 1, value = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..")
 | 
			
		||||
    private String token;
 | 
			
		||||
@ -34,7 +33,7 @@ public class JwtTokenPair {
 | 
			
		||||
 | 
			
		||||
    private Authority scope;
 | 
			
		||||
 | 
			
		||||
    public JwtTokenPair(String token, String refreshToken) {
 | 
			
		||||
    public JwtPair(String token, String refreshToken) {
 | 
			
		||||
        this.token = token;
 | 
			
		||||
        this.refreshToken = refreshToken;
 | 
			
		||||
    }
 | 
			
		||||
@ -13,35 +13,43 @@
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.config;
 | 
			
		||||
package org.thingsboard.server.common.data.security.model;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.springframework.boot.context.properties.ConfigurationProperties;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtToken;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
 | 
			
		||||
@Component
 | 
			
		||||
@ConfigurationProperties(prefix = "security.jwt")
 | 
			
		||||
@ApiModel(value = "JWT Settings")
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
@Data
 | 
			
		||||
public class JwtSettings {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@link JwtToken} will expire after this time.
 | 
			
		||||
     */
 | 
			
		||||
    @ApiModelProperty(position = 1, value = "The JWT will expire after seconds.", example = "9000")
 | 
			
		||||
    private Integer tokenExpirationTime;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Token issuer.
 | 
			
		||||
     */
 | 
			
		||||
    private String tokenIssuer;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Key is used to sign {@link JwtToken}.
 | 
			
		||||
     */
 | 
			
		||||
    private String tokenSigningKey;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@link JwtToken} can be refreshed during this timeframe.
 | 
			
		||||
     */
 | 
			
		||||
    @ApiModelProperty(position = 2, value = "The JWT can be refreshed during seconds.", example = "604800")
 | 
			
		||||
    private Integer refreshTokenExpTime;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Token issuer.
 | 
			
		||||
     */
 | 
			
		||||
    @ApiModelProperty(position = 3, value = "The JWT issuer.", example = "thingsboard.io")
 | 
			
		||||
    private String tokenIssuer;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Key is used to sign {@link JwtToken}.
 | 
			
		||||
     * Base64 encoded
 | 
			
		||||
     */
 | 
			
		||||
    @ApiModelProperty(position = 4, value = "The JWT key is used to sing token. Base64 encoded.", example = "cTU4WnNqemI2aU5wbWVjdm1vYXRzanhjNHRUcXliMjE=")
 | 
			
		||||
    private String tokenSigningKey;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,2 +1,3 @@
 | 
			
		||||
config.stopbubbling = true
 | 
			
		||||
lombok.anyconstructor.addconstructorproperties = true
 | 
			
		||||
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy
 | 
			
		||||
 | 
			
		||||
@ -56,6 +56,10 @@
 | 
			
		||||
        <appender-ref ref="STDOUT" />
 | 
			
		||||
    </logger>
 | 
			
		||||
 | 
			
		||||
    <logger name="org.thingsboard.server.config.jwt" level="INFO">
 | 
			
		||||
        <appender-ref ref="STDOUT" />
 | 
			
		||||
    </logger>
 | 
			
		||||
 | 
			
		||||
    <logger name="org.thingsboard.server" level="INFO" />
 | 
			
		||||
 | 
			
		||||
    <root level="INFO">
 | 
			
		||||
 | 
			
		||||
@ -136,6 +136,8 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
 | 
			
		||||
import org.thingsboard.server.common.data.rule.RuleChainType;
 | 
			
		||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
 | 
			
		||||
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtPair;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtSettings;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.SecuritySettings;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
 | 
			
		||||
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
 | 
			
		||||
@ -286,6 +288,23 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
 | 
			
		||||
        return restTemplate.postForEntity(baseURL + "/api/admin/securitySettings", securitySettings, SecuritySettings.class).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Optional<JwtSettings> getJwtSettings() {
 | 
			
		||||
        try {
 | 
			
		||||
            ResponseEntity<JwtSettings> jwtSettings = restTemplate.getForEntity(baseURL + "/api/admin/jwtSettings", JwtSettings.class);
 | 
			
		||||
            return Optional.ofNullable(jwtSettings.getBody());
 | 
			
		||||
        } catch (HttpClientErrorException exception) {
 | 
			
		||||
            if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
 | 
			
		||||
                return Optional.empty();
 | 
			
		||||
            } else {
 | 
			
		||||
                throw exception;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public JwtPair saveJwtSettings(JwtSettings jwtSettings) {
 | 
			
		||||
        return restTemplate.postForEntity(baseURL + "/api/admin/jwtSettings", jwtSettings, JwtPair.class).getBody();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Optional<RepositorySettings> getRepositorySettings() {
 | 
			
		||||
        try {
 | 
			
		||||
            ResponseEntity<RepositorySettings> repositorySettings = restTemplate.getForEntity(baseURL + "/api/admin/repositorySettings", RepositorySettings.class);
 | 
			
		||||
 | 
			
		||||
@ -20,16 +20,18 @@ import { Observable } from 'rxjs';
 | 
			
		||||
import { HttpClient } from '@angular/common/http';
 | 
			
		||||
import {
 | 
			
		||||
  AdminSettings,
 | 
			
		||||
  RepositorySettings,
 | 
			
		||||
  AutoCommitSettings,
 | 
			
		||||
  JwtSettings,
 | 
			
		||||
  MailServerSettings,
 | 
			
		||||
  RepositorySettings,
 | 
			
		||||
  RepositorySettingsInfo,
 | 
			
		||||
  SecuritySettings,
 | 
			
		||||
  TestSmsRequest,
 | 
			
		||||
  UpdateMessage,
 | 
			
		||||
  AutoCommitSettings,
 | 
			
		||||
  RepositorySettingsInfo
 | 
			
		||||
  UpdateMessage
 | 
			
		||||
} from '@shared/models/settings.models';
 | 
			
		||||
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
 | 
			
		||||
import { tap } from 'rxjs/operators';
 | 
			
		||||
import { LoginResponse } from '@shared/models/login.models';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
@ -70,6 +72,14 @@ export class AdminService {
 | 
			
		||||
      defaultHttpOptionsFromConfig(config));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getJwtSettings(config?: RequestConfig): Observable<JwtSettings> {
 | 
			
		||||
    return this.http.get<JwtSettings>(`/api/admin/jwtSettings`, defaultHttpOptionsFromConfig(config));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public saveJwtSettings(jwtSettings: JwtSettings, config?: RequestConfig): Observable<LoginResponse> {
 | 
			
		||||
    return this.http.post<LoginResponse>('/api/admin/jwtSettings', jwtSettings, defaultHttpOptionsFromConfig(config));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getRepositorySettings(config?: RequestConfig): Observable<RepositorySettings> {
 | 
			
		||||
    return this.http.get<RepositorySettings>(`/api/admin/repositorySettings`, defaultHttpOptionsFromConfig(config));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -15,8 +15,7 @@
 | 
			
		||||
    limitations under the License.
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<div>
 | 
			
		||||
  <mat-card class="settings-card">
 | 
			
		||||
<mat-card class="settings-card">
 | 
			
		||||
  <mat-card-title>
 | 
			
		||||
    <div fxLayout="row">
 | 
			
		||||
      <span class="mat-headline" translate>admin.security-settings</span>
 | 
			
		||||
@ -30,14 +29,8 @@
 | 
			
		||||
  <mat-card-content style="padding-top: 16px;">
 | 
			
		||||
    <form [formGroup]="securitySettingsFormGroup" (ngSubmit)="save()" autocomplete="off">
 | 
			
		||||
      <fieldset [disabled]="isLoading$ | async">
 | 
			
		||||
          <div class="mat-accordion-container">
 | 
			
		||||
            <mat-accordion multi="true">
 | 
			
		||||
              <mat-expansion-panel [expanded]="true">
 | 
			
		||||
                <mat-expansion-panel-header>
 | 
			
		||||
                  <mat-panel-title>
 | 
			
		||||
                    <div class="tb-panel-title" translate>admin.general-policy</div>
 | 
			
		||||
                  </mat-panel-title>
 | 
			
		||||
                </mat-expansion-panel-header>
 | 
			
		||||
        <fieldset class="fields-group">
 | 
			
		||||
          <legend class="group-title" translate>admin.general-policy</legend>
 | 
			
		||||
          <mat-form-field class="mat-block">
 | 
			
		||||
            <mat-label translate>admin.max-failed-login-attempts</mat-label>
 | 
			
		||||
            <input matInput type="number"
 | 
			
		||||
@ -53,13 +46,10 @@
 | 
			
		||||
            <input matInput type="email"
 | 
			
		||||
                   formControlName="userLockoutNotificationEmail"/>
 | 
			
		||||
          </mat-form-field>
 | 
			
		||||
              </mat-expansion-panel>
 | 
			
		||||
              <mat-expansion-panel [expanded]="true">
 | 
			
		||||
                <mat-expansion-panel-header>
 | 
			
		||||
                  <mat-panel-title>
 | 
			
		||||
                    <div class="tb-panel-title" translate>admin.password-policy</div>
 | 
			
		||||
                  </mat-panel-title>
 | 
			
		||||
                </mat-expansion-panel-header>
 | 
			
		||||
        </fieldset>
 | 
			
		||||
 | 
			
		||||
        <fieldset class="fields-group">
 | 
			
		||||
          <legend class="group-title" translate>admin.password-policy</legend>
 | 
			
		||||
          <section formGroupName="passwordPolicy">
 | 
			
		||||
            <mat-form-field class="mat-block">
 | 
			
		||||
              <mat-label translate>admin.minimum-password-length</mat-label>
 | 
			
		||||
@ -79,27 +69,32 @@
 | 
			
		||||
                {{ 'admin.minimum-password-length-range' | translate }}
 | 
			
		||||
              </mat-error>
 | 
			
		||||
            </mat-form-field>
 | 
			
		||||
                  <mat-form-field class="mat-block">
 | 
			
		||||
            <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
 | 
			
		||||
              <mat-form-field fxFlex class="mat-block">
 | 
			
		||||
                <mat-label translate>admin.minimum-uppercase-letters</mat-label>
 | 
			
		||||
                <input matInput type="number"
 | 
			
		||||
                       formControlName="minimumUppercaseLetters"
 | 
			
		||||
                       step="1"
 | 
			
		||||
                       min="0"/>
 | 
			
		||||
                    <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumUppercaseLetters').hasError('min')">
 | 
			
		||||
                <mat-error
 | 
			
		||||
                  *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumUppercaseLetters').hasError('min')">
 | 
			
		||||
                  {{ 'admin.minimum-uppercase-letters-range' | translate }}
 | 
			
		||||
                </mat-error>
 | 
			
		||||
              </mat-form-field>
 | 
			
		||||
                  <mat-form-field class="mat-block">
 | 
			
		||||
              <mat-form-field fxFlex class="mat-block">
 | 
			
		||||
                <mat-label translate>admin.minimum-lowercase-letters</mat-label>
 | 
			
		||||
                <input matInput type="number"
 | 
			
		||||
                       formControlName="minimumLowercaseLetters"
 | 
			
		||||
                       step="1"
 | 
			
		||||
                       min="0"/>
 | 
			
		||||
                    <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLowercaseLetters').hasError('min')">
 | 
			
		||||
                <mat-error
 | 
			
		||||
                  *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLowercaseLetters').hasError('min')">
 | 
			
		||||
                  {{ 'admin.minimum-lowercase-letters-range' | translate }}
 | 
			
		||||
                </mat-error>
 | 
			
		||||
              </mat-form-field>
 | 
			
		||||
                  <mat-form-field class="mat-block">
 | 
			
		||||
            </div>
 | 
			
		||||
            <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
 | 
			
		||||
              <mat-form-field fxFlex class="mat-block">
 | 
			
		||||
                <mat-label translate>admin.minimum-digits</mat-label>
 | 
			
		||||
                <input matInput type="number"
 | 
			
		||||
                       formControlName="minimumDigits"
 | 
			
		||||
@ -109,44 +104,53 @@
 | 
			
		||||
                  {{ 'admin.minimum-digits-range' | translate }}
 | 
			
		||||
                </mat-error>
 | 
			
		||||
              </mat-form-field>
 | 
			
		||||
                  <mat-form-field class="mat-block">
 | 
			
		||||
              <mat-form-field fxFlex class="mat-block">
 | 
			
		||||
                <mat-label translate>admin.minimum-special-characters</mat-label>
 | 
			
		||||
                <input matInput type="number"
 | 
			
		||||
                       formControlName="minimumSpecialCharacters"
 | 
			
		||||
                       step="1"
 | 
			
		||||
                       min="0"/>
 | 
			
		||||
                    <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumSpecialCharacters').hasError('min')">
 | 
			
		||||
                <mat-error
 | 
			
		||||
                  *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumSpecialCharacters').hasError('min')">
 | 
			
		||||
                  {{ 'admin.minimum-special-characters-range' | translate }}
 | 
			
		||||
                </mat-error>
 | 
			
		||||
              </mat-form-field>
 | 
			
		||||
                  <mat-form-field class="mat-block">
 | 
			
		||||
            </div>
 | 
			
		||||
            <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
 | 
			
		||||
              <mat-form-field fxFlex class="mat-block">
 | 
			
		||||
                <mat-label translate>admin.password-expiration-period-days</mat-label>
 | 
			
		||||
                <input matInput type="number"
 | 
			
		||||
                       formControlName="passwordExpirationPeriodDays"
 | 
			
		||||
                       step="1"
 | 
			
		||||
                       min="0"/>
 | 
			
		||||
                    <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.passwordExpirationPeriodDays').hasError('min')">
 | 
			
		||||
                <mat-error
 | 
			
		||||
                  *ngIf="securitySettingsFormGroup.get('passwordPolicy.passwordExpirationPeriodDays').hasError('min')">
 | 
			
		||||
                  {{ 'admin.password-expiration-period-days-range' | translate }}
 | 
			
		||||
                </mat-error>
 | 
			
		||||
              </mat-form-field>
 | 
			
		||||
                  <mat-form-field class="mat-block">
 | 
			
		||||
              <mat-form-field fxFlex class="mat-block">
 | 
			
		||||
                <mat-label translate>admin.password-reuse-frequency-days</mat-label>
 | 
			
		||||
                <input matInput type="number"
 | 
			
		||||
                       formControlName="passwordReuseFrequencyDays"
 | 
			
		||||
                       step="1"
 | 
			
		||||
                       min="0"/>
 | 
			
		||||
                    <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.passwordReuseFrequencyDays').hasError('min')">
 | 
			
		||||
                <mat-error
 | 
			
		||||
                  *ngIf="securitySettingsFormGroup.get('passwordPolicy.passwordReuseFrequencyDays').hasError('min')">
 | 
			
		||||
                  {{ 'admin.password-reuse-frequency-days-range' | translate }}
 | 
			
		||||
                </mat-error>
 | 
			
		||||
              </mat-form-field>
 | 
			
		||||
                  <mat-checkbox formControlName = "allowWhitespaces" >
 | 
			
		||||
            </div>
 | 
			
		||||
            <mat-checkbox formControlName="allowWhitespaces" style="margin-bottom: 16px">
 | 
			
		||||
              <mat-label translate>admin.allow-whitespace</mat-label>
 | 
			
		||||
            </mat-checkbox>
 | 
			
		||||
          </section>
 | 
			
		||||
              </mat-expansion-panel>
 | 
			
		||||
            </mat-accordion>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
 | 
			
		||||
        </fieldset>
 | 
			
		||||
        <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px" class="layout-wrap" style="margin-top: 16px">
 | 
			
		||||
          <button mat-button color="primary"
 | 
			
		||||
                  [disabled]="securitySettingsFormGroup.pristine"
 | 
			
		||||
                  (click)="discardSetting()"
 | 
			
		||||
                  type="button">{{'action.undo' | translate}}
 | 
			
		||||
          </button>
 | 
			
		||||
          <button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || securitySettingsFormGroup.invalid || !securitySettingsFormGroup.dirty"
 | 
			
		||||
                  type="submit">{{'action.save' | translate}}
 | 
			
		||||
          </button>
 | 
			
		||||
@ -154,5 +158,96 @@
 | 
			
		||||
      </fieldset>
 | 
			
		||||
    </form>
 | 
			
		||||
  </mat-card-content>
 | 
			
		||||
  </mat-card>
 | 
			
		||||
</div>
 | 
			
		||||
</mat-card>
 | 
			
		||||
<mat-card class="settings-card">
 | 
			
		||||
  <mat-card-title>
 | 
			
		||||
    <div fxLayout="row">
 | 
			
		||||
      <span class="mat-headline" translate>admin.jwt.security-settings</span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </mat-card-title>
 | 
			
		||||
  <mat-card-content style="padding-top: 16px;">
 | 
			
		||||
    <form [formGroup]="jwtSecuritySettingsFormGroup" (ngSubmit)="saveJwtSettings()" autocomplete="off">
 | 
			
		||||
      <fieldset [disabled]="isLoading$ | async" fxLayout="column" fxLayoutGap="8px">
 | 
			
		||||
        <div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
 | 
			
		||||
          <mat-form-field fxFlex class="mat-block">
 | 
			
		||||
            <mat-label translate>admin.jwt.issuer-name</mat-label>
 | 
			
		||||
            <input matInput required formControlName="tokenIssuer"/>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenIssuer').hasError('required')">
 | 
			
		||||
              {{ 'admin.jwt.issuer-name-required' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
          </mat-form-field>
 | 
			
		||||
          <mat-form-field fxFlex class="mat-block">
 | 
			
		||||
            <mat-label translate>admin.jwt.signings-key</mat-label>
 | 
			
		||||
            <input matInput (focus)="markAsTouched()" required formControlName="tokenSigningKey"/>
 | 
			
		||||
            <button type="button"
 | 
			
		||||
                    style="line-height: 32px"
 | 
			
		||||
                    matSuffix
 | 
			
		||||
                    mat-button
 | 
			
		||||
                    (click)="generateSigningKey()"
 | 
			
		||||
                    color="primary">
 | 
			
		||||
              {{ 'admin.jwt.generate-key' | translate }}
 | 
			
		||||
            </button>
 | 
			
		||||
            <mat-hint translate>admin.jwt.signings-key-hint</mat-hint>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenSigningKey').hasError('required')">
 | 
			
		||||
              {{ 'admin.jwt.signings-key-required' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenSigningKey').hasError('base64')">
 | 
			
		||||
              {{ 'admin.jwt.signings-key-base64' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenSigningKey').hasError('minLength')">
 | 
			
		||||
              {{ 'admin.jwt.signings-key-min-length' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
          </mat-form-field>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
 | 
			
		||||
          <mat-form-field fxFlex class="mat-block">
 | 
			
		||||
            <mat-label translate>admin.jwt.expiration-time</mat-label>
 | 
			
		||||
            <input matInput type="number" required
 | 
			
		||||
                   formControlName="tokenExpirationTime"
 | 
			
		||||
                   step="1"
 | 
			
		||||
                   min="0"/>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('required')">
 | 
			
		||||
              {{ 'admin.jwt.expiration-time-required' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('pattern')">
 | 
			
		||||
              {{ 'admin.jwt.expiration-time-pattern' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('tokenExpirationTime').hasError('min')">
 | 
			
		||||
              {{ 'admin.jwt.expiration-time-min' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
          </mat-form-field>
 | 
			
		||||
          <mat-form-field fxFlex class="mat-block">
 | 
			
		||||
            <mat-label translate>admin.jwt.refresh-expiration-time</mat-label>
 | 
			
		||||
            <input matInput type="number" required
 | 
			
		||||
                   formControlName="refreshTokenExpTime"
 | 
			
		||||
                   step="1"
 | 
			
		||||
                   min="0"/>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('required')">
 | 
			
		||||
              {{ 'admin.jwt.refresh-expiration-time-required' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('pattern')">
 | 
			
		||||
              {{ 'admin.jwt.refresh-expiration-time-pattern' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('min')">
 | 
			
		||||
              {{ 'admin.jwt.refresh-expiration-time-min' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
            <mat-error *ngIf="jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').hasError('lessToken')">
 | 
			
		||||
              {{ 'admin.jwt.refresh-expiration-time-less-token' | translate }}
 | 
			
		||||
            </mat-error>
 | 
			
		||||
          </mat-form-field>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px" class="layout-wrap">
 | 
			
		||||
          <button mat-button color="primary"
 | 
			
		||||
                  [disabled]="jwtSecuritySettingsFormGroup.pristine"
 | 
			
		||||
                  (click)="discardJwtSetting()"
 | 
			
		||||
                  type="button">{{'action.undo' | translate}}
 | 
			
		||||
          </button>
 | 
			
		||||
          <button mat-raised-button color="primary"
 | 
			
		||||
                  [disabled]="(isLoading$ | async) || jwtSecuritySettingsFormGroup.invalid || !jwtSecuritySettingsFormGroup.dirty"
 | 
			
		||||
                  type="submit">{{'action.save' | translate}}
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
    </form>
 | 
			
		||||
  </mat-card-content>
 | 
			
		||||
</mat-card>
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,26 @@
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
:host {
 | 
			
		||||
  .mat-accordion-container {
 | 
			
		||||
    margin-bottom: 16px;
 | 
			
		||||
  .mat-headline {
 | 
			
		||||
    margin-bottom: 8px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .mat-card-title {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .mat-card-content {
 | 
			
		||||
    padding: 0 !important;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .fields-group {
 | 
			
		||||
    padding: 8px 16px 0;
 | 
			
		||||
    margin: 10px 0;
 | 
			
		||||
    border: 1px groove rgba(0, 0, 0, .25);
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
 | 
			
		||||
    legend {
 | 
			
		||||
      color: rgba(0, 0, 0, .7);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,40 +14,50 @@
 | 
			
		||||
/// limitations under the License.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { Component } from '@angular/core';
 | 
			
		||||
import { Store } from '@ngrx/store';
 | 
			
		||||
import { AppState } from '@core/core.state';
 | 
			
		||||
import { PageComponent } from '@shared/components/page.component';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 | 
			
		||||
import { SecuritySettings } from '@shared/models/settings.models';
 | 
			
		||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
 | 
			
		||||
import { JwtSettings, SecuritySettings } from '@shared/models/settings.models';
 | 
			
		||||
import { AdminService } from '@core/http/admin.service';
 | 
			
		||||
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
 | 
			
		||||
import { mergeMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { randomAlphanumeric } from '@core/utils';
 | 
			
		||||
import { AuthService } from '@core/auth/auth.service';
 | 
			
		||||
import { DialogService } from '@core/services/dialog.service';
 | 
			
		||||
import { TranslateService } from '@ngx-translate/core';
 | 
			
		||||
import { Observable, of } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-security-settings',
 | 
			
		||||
  templateUrl: './security-settings.component.html',
 | 
			
		||||
  styleUrls: ['./security-settings.component.scss', './settings-card.scss']
 | 
			
		||||
})
 | 
			
		||||
export class SecuritySettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
 | 
			
		||||
export class SecuritySettingsComponent extends PageComponent implements HasConfirmForm {
 | 
			
		||||
 | 
			
		||||
  securitySettingsFormGroup: FormGroup;
 | 
			
		||||
  securitySettings: SecuritySettings;
 | 
			
		||||
  jwtSecuritySettingsFormGroup: FormGroup;
 | 
			
		||||
 | 
			
		||||
  private securitySettings: SecuritySettings;
 | 
			
		||||
  private jwtSettings: JwtSettings;
 | 
			
		||||
 | 
			
		||||
  constructor(protected store: Store<AppState>,
 | 
			
		||||
              private router: Router,
 | 
			
		||||
              private adminService: AdminService,
 | 
			
		||||
              public fb: FormBuilder) {
 | 
			
		||||
              private authService: AuthService,
 | 
			
		||||
              private dialogService: DialogService,
 | 
			
		||||
              private translate: TranslateService,
 | 
			
		||||
              private fb: FormBuilder) {
 | 
			
		||||
    super(store);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.buildSecuritySettingsForm();
 | 
			
		||||
    this.buildJwtSecuritySettingsForm();
 | 
			
		||||
    this.adminService.getSecuritySettings().subscribe(
 | 
			
		||||
      (securitySettings) => {
 | 
			
		||||
        this.securitySettings = securitySettings;
 | 
			
		||||
        this.securitySettingsFormGroup.reset(this.securitySettings);
 | 
			
		||||
      }
 | 
			
		||||
      securitySettings => this.processSecuritySettings(securitySettings)
 | 
			
		||||
    );
 | 
			
		||||
    this.adminService.getJwtSettings().subscribe(
 | 
			
		||||
      jwtSettings => this.processJwtSettings(jwtSettings)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -70,18 +80,114 @@ export class SecuritySettingsComponent extends PageComponent implements OnInit,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  save(): void {
 | 
			
		||||
    this.securitySettings = {...this.securitySettings, ...this.securitySettingsFormGroup.value};
 | 
			
		||||
    this.adminService.saveSecuritySettings(this.securitySettings).subscribe(
 | 
			
		||||
      (securitySettings) => {
 | 
			
		||||
        this.securitySettings = securitySettings;
 | 
			
		||||
        this.securitySettingsFormGroup.reset(this.securitySettings);
 | 
			
		||||
      }
 | 
			
		||||
  buildJwtSecuritySettingsForm() {
 | 
			
		||||
    this.jwtSecuritySettingsFormGroup = this.fb.group({
 | 
			
		||||
      tokenIssuer: ['', Validators.required],
 | 
			
		||||
      tokenSigningKey: ['', [Validators.required, this.base64Format]],
 | 
			
		||||
      tokenExpirationTime: [0, [Validators.required, Validators.pattern('[0-9]*'), Validators.min(60)]],
 | 
			
		||||
      refreshTokenExpTime: [0, [Validators.required, Validators.pattern('[0-9]*'), Validators.min(900)]]
 | 
			
		||||
    }, {validators: this.refreshTokenTimeGreatTokenTime.bind(this)});
 | 
			
		||||
    this.jwtSecuritySettingsFormGroup.get('tokenExpirationTime').valueChanges.subscribe(
 | 
			
		||||
      () => this.jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').updateValueAndValidity({onlySelf: true})
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  save(): void {
 | 
			
		||||
    this.securitySettings = {...this.securitySettings, ...this.securitySettingsFormGroup.value};
 | 
			
		||||
    this.adminService.saveSecuritySettings(this.securitySettings).subscribe(
 | 
			
		||||
      securitySettings => this.processSecuritySettings(securitySettings)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  saveJwtSettings() {
 | 
			
		||||
    const jwtFormSettings = this.jwtSecuritySettingsFormGroup.value;
 | 
			
		||||
    this.confirmChangeJWTSettings().pipe(mergeMap(value => {
 | 
			
		||||
      if (value) {
 | 
			
		||||
        return this.adminService.saveJwtSettings(jwtFormSettings).pipe(
 | 
			
		||||
          tap((data) => this.authService.setUserFromJwtToken(data.token, data.refreshToken, false)),
 | 
			
		||||
          mergeMap(() => this.adminService.getJwtSettings()),
 | 
			
		||||
          tap(jwtSettings => this.processJwtSettings(jwtSettings))
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return of(null);
 | 
			
		||||
    })).subscribe(() => {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  discardSetting() {
 | 
			
		||||
    this.securitySettingsFormGroup.reset(this.securitySettings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  discardJwtSetting() {
 | 
			
		||||
    this.jwtSecuritySettingsFormGroup.reset(this.jwtSettings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  markAsTouched() {
 | 
			
		||||
    this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsTouched();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private confirmChangeJWTSettings(): Observable<boolean> {
 | 
			
		||||
    if (this.jwtSecuritySettingsFormGroup.get('tokenIssuer').value !== (this.jwtSettings?.tokenIssuer || '') ||
 | 
			
		||||
      this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').value !== (this.jwtSettings?.tokenSigningKey || '')) {
 | 
			
		||||
      return this.dialogService.confirm(
 | 
			
		||||
        this.translate.instant('admin.jwt.info-header'),
 | 
			
		||||
        `<div style="max-width: 640px">${this.translate.instant('admin.jwt.info-message')}</div>`,
 | 
			
		||||
        this.translate.instant('action.discard-changes'),
 | 
			
		||||
        this.translate.instant('action.confirm')
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return of(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generateSigningKey() {
 | 
			
		||||
    this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').setValue(btoa(randomAlphanumeric(64)));
 | 
			
		||||
    if (this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').pristine) {
 | 
			
		||||
      this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsDirty();
 | 
			
		||||
      this.jwtSecuritySettingsFormGroup.get('tokenSigningKey').markAsTouched();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private processSecuritySettings(securitySettings: SecuritySettings) {
 | 
			
		||||
    this.securitySettings = securitySettings;
 | 
			
		||||
    this.securitySettingsFormGroup.reset(this.securitySettings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private processJwtSettings(jwtSettings: JwtSettings) {
 | 
			
		||||
    this.jwtSettings = jwtSettings;
 | 
			
		||||
    this.jwtSecuritySettingsFormGroup.reset(jwtSettings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private refreshTokenTimeGreatTokenTime(formGroup: FormGroup): { [key: string]: boolean } | null {
 | 
			
		||||
    if (formGroup) {
 | 
			
		||||
      const tokenTime = formGroup.value.tokenExpirationTime;
 | 
			
		||||
      const refreshTokenTime = formGroup.value.refreshTokenExpTime;
 | 
			
		||||
      if (tokenTime >= refreshTokenTime ) {
 | 
			
		||||
        if (formGroup.get('refreshTokenExpTime').untouched) {
 | 
			
		||||
          formGroup.get('refreshTokenExpTime').markAsTouched();
 | 
			
		||||
        }
 | 
			
		||||
        formGroup.get('refreshTokenExpTime').setErrors({lessToken: true});
 | 
			
		||||
        return {lessToken: true};
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private base64Format(control: FormControl): { [key: string]: boolean } | null {
 | 
			
		||||
    if (control.value === '' || control.value === 'thingsboardDefaultSigningKey') {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const value = atob(control.value);
 | 
			
		||||
      if (value.length < 32) {
 | 
			
		||||
        return {minLength: true};
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      return {base64: true};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  confirmForm(): FormGroup {
 | 
			
		||||
    return this.securitySettingsFormGroup;
 | 
			
		||||
    return this.securitySettingsFormGroup.dirty ? this.securitySettingsFormGroup : this.jwtSecuritySettingsFormGroup;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<h2 mat-dialog-title>{{data.title}}</h2>
 | 
			
		||||
<div mat-dialog-content [innerHTML]="data.message"></div>
 | 
			
		||||
<div mat-dialog-content [innerHTML]="data.message | safe: 'html'"></div>
 | 
			
		||||
<div mat-dialog-actions fxLayoutAlign="end center">
 | 
			
		||||
  <button mat-button color="primary" [mat-dialog-close]="false">{{data.cancel}}</button>
 | 
			
		||||
  <button mat-button color="primary" [mat-dialog-close]="true" cdkFocusInitial>{{data.ok}}</button>
 | 
			
		||||
 | 
			
		||||
@ -63,6 +63,13 @@ export interface SecuritySettings {
 | 
			
		||||
  passwordPolicy: UserPasswordPolicy;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface JwtSettings {
 | 
			
		||||
  tokenIssuer: string;
 | 
			
		||||
  tokenSigningKey: string;
 | 
			
		||||
  tokenExpirationTime: number;
 | 
			
		||||
  refreshTokenExpTime: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UpdateMessage {
 | 
			
		||||
  message: string;
 | 
			
		||||
  updateAvailable: boolean;
 | 
			
		||||
 | 
			
		||||
@ -381,6 +381,28 @@
 | 
			
		||||
            "within-time": "Within time (sec)",
 | 
			
		||||
            "within-time-pattern": "Time must be a positive integer.",
 | 
			
		||||
            "within-time-required": "Time is required."
 | 
			
		||||
        },
 | 
			
		||||
        "jwt": {
 | 
			
		||||
            "security-settings": "JWT security settings",
 | 
			
		||||
            "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-required": "Signing key is required.",
 | 
			
		||||
            "signings-key-min-length": "Signing key must be at least 256 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.",
 | 
			
		||||
            "expiration-time-pattern": "Token expiration time be a positive integer.",
 | 
			
		||||
            "expiration-time-min": "Minimum time is 60 seconds (1 minute).",
 | 
			
		||||
            "refresh-expiration-time": "Refresh token expiration time (sec)",
 | 
			
		||||
            "refresh-expiration-time-required": "Refresh token expiration time is required.",
 | 
			
		||||
            "refresh-expiration-time-pattern": "Refresh token expiration time be a positive integer.",
 | 
			
		||||
            "refresh-expiration-time-min": "Minimum time is 900 seconds (15 minute).",
 | 
			
		||||
            "refresh-expiration-time-less-token": "Refresh token time must be greater token time.",
 | 
			
		||||
            "generate-key": "Generate key",
 | 
			
		||||
            "info-header": "All users will be to re-logined",
 | 
			
		||||
            "info-message": "Change of the JWT Signing Key will cause all issued tokens to be invalid. All users will need to re-login. This will also affect scripts that use Rest API/Websockets."
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    "alarm": {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user