Email 2FA provider; 2FA API improvements
This commit is contained in:
		
							parent
							
								
									bcc736991e
								
							
						
					
					
						commit
						4480badd78
					
				@ -33,7 +33,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
 | 
			
		||||
import org.springframework.web.bind.annotation.RequestParam;
 | 
			
		||||
import org.springframework.web.bind.annotation.RestController;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
 | 
			
		||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
 | 
			
		||||
@ -51,6 +50,7 @@ import javax.servlet.http.HttpServletResponse;
 | 
			
		||||
import javax.validation.Valid;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
 | 
			
		||||
@ -65,20 +65,15 @@ public class TwoFaConfigController extends BaseController {
 | 
			
		||||
    private final TwoFactorAuthService twoFactorAuthService;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get 2FA account config (getTwoFaAccountConfig)",
 | 
			
		||||
            notes = "Get user's account 2FA configuration. Returns empty result if user did not configured 2FA, " +
 | 
			
		||||
                    "or if a provider for previously set up account config is not now configured." + NEW_LINE +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE +
 | 
			
		||||
                    "Response example for TOTP 2FA: " + NEW_LINE +
 | 
			
		||||
                    "```\n{\n" +
 | 
			
		||||
                    "  \"providerType\": \"TOTP\",\n" +
 | 
			
		||||
                    "  \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" +
 | 
			
		||||
                    "}\n```" + NEW_LINE +
 | 
			
		||||
                    "Response example for SMS 2FA: " + NEW_LINE +
 | 
			
		||||
                    "```\n{\n" +
 | 
			
		||||
                    "  \"providerType\": \"SMS\",\n" +
 | 
			
		||||
                    "  \"phoneNumber\": \"+380505005050\"\n" +
 | 
			
		||||
                    "}\n```")
 | 
			
		||||
    @ApiOperation(value = "Get account 2FA settings (getAccountTwoFaSettings)",
 | 
			
		||||
            notes = "Get user's account 2FA configuration. Configuration contains configs for different 2FA providers." + NEW_LINE +
 | 
			
		||||
                    "Example:\n" +
 | 
			
		||||
                    "```\n{\n  \"configs\": {\n" +
 | 
			
		||||
                    "    \"EMAIL\": {\n      \"providerType\": \"EMAIL\",\n      \"useByDefault\": true,\n      \"email\": \"tenant@thingsboard.org\"\n    },\n" +
 | 
			
		||||
                    "    \"TOTP\": {\n      \"providerType\": \"TOTP\",\n      \"useByDefault\": false,\n      \"authUrl\": \"otpauth://totp/TB%202FA:tenant@thingsboard.org?issuer=TB+2FA&secret=P6Z2TLYTASOGP6LCJZAD24ETT5DACNNX\"\n    },\n" +
 | 
			
		||||
                    "    \"SMS\": {\n      \"providerType\": \"SMS\",\n      \"useByDefault\": false,\n      \"phoneNumber\": \"+380501253652\"\n    }\n" +
 | 
			
		||||
                    "  }\n}\n```" +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
 | 
			
		||||
    @GetMapping("/account/settings")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException {
 | 
			
		||||
@ -88,24 +83,29 @@ public class TwoFaConfigController extends BaseController {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)",
 | 
			
		||||
            notes = "Generate new 2FA account config for specified provider type. " +
 | 
			
		||||
                    "This method is only useful for TOTP 2FA, as there is nothing to generate for other provider types. " +
 | 
			
		||||
            notes = "Generate new 2FA account config template for specified provider type. " + NEW_LINE +
 | 
			
		||||
                    "For TOTP, this will return a corresponding account config template " +
 | 
			
		||||
                    "with a generated OTP auth URL (with new random secret key for each API call) that can be then " +
 | 
			
		||||
                    "converted to a QR code to scan with an authenticator app. " +
 | 
			
		||||
                    "For other provider types, this method will return an empty config. " + NEW_LINE +
 | 
			
		||||
                    "Will throw an error (Bad Request) if the provider is not configured for usage. " +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE +
 | 
			
		||||
                    "Example of a generated account config for TOTP 2FA: " + NEW_LINE +
 | 
			
		||||
                    "converted to a QR code to scan with an authenticator app. Example:\n" +
 | 
			
		||||
                    "```\n{\n" +
 | 
			
		||||
                    "  \"providerType\": \"TOTP\",\n" +
 | 
			
		||||
                    "  \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" +
 | 
			
		||||
                    "  \"useByDefault\": false,\n" +
 | 
			
		||||
                    "  \"authUrl\": \"otpauth://totp/TB%202FA:tenant@thingsboard.org?issuer=TB+2FA&secret=PNJDNWJVAK4ZTUYT7RFGPQLXA7XGU7PX\"\n" +
 | 
			
		||||
                    "}\n```" + NEW_LINE +
 | 
			
		||||
                    "For SMS provider type it will return something like: " + NEW_LINE +
 | 
			
		||||
                    "For EMAIL, the generated config will contain email from user's account:\n" +
 | 
			
		||||
                    "```\n{\n" +
 | 
			
		||||
                    "  \"providerType\": \"EMAIL\",\n" +
 | 
			
		||||
                    "  \"useByDefault\": false,\n" +
 | 
			
		||||
                    "  \"email\": \"tenant@thingsboard.org\"\n" +
 | 
			
		||||
                    "}\n```" + NEW_LINE +
 | 
			
		||||
                    "For SMS 2FA this method will just return a config with empty/default values as there is nothing to generate/preset:\n" +
 | 
			
		||||
                    "```\n{\n" +
 | 
			
		||||
                    "  \"providerType\": \"SMS\",\n" +
 | 
			
		||||
                    "  \"useByDefault\": false,\n" +
 | 
			
		||||
                    "  \"phoneNumber\": null\n" +
 | 
			
		||||
                    "}\n```")
 | 
			
		||||
                    "}\n```" + NEW_LINE +
 | 
			
		||||
                    "Will throw an error (Bad Request) if the provider is not configured for usage. " +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
 | 
			
		||||
    @PostMapping("/account/config/generate")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    public TwoFaAccountConfig generateTwoFaAccountConfig(@ApiParam(value = "2FA provider type to generate new account config for", defaultValue = "TOTP", required = true)
 | 
			
		||||
@ -133,51 +133,84 @@ public class TwoFaConfigController extends BaseController {
 | 
			
		||||
            notes = "Submit 2FA account config to prepare for a future verification. " +
 | 
			
		||||
                    "Basically, this method will send a verification code for a given account config, if this has " +
 | 
			
		||||
                    "sense for a chosen 2FA provider. This code is needed to then verify and save the account config." + NEW_LINE +
 | 
			
		||||
                    "Example of EMAIL 2FA account config:\n" +
 | 
			
		||||
                    "```\n{\n" +
 | 
			
		||||
                    "  \"providerType\": \"EMAIL\",\n" +
 | 
			
		||||
                    "  \"useByDefault\": true,\n" +
 | 
			
		||||
                    "  \"email\": \"separate-email-for-2fa@thingsboard.org\"\n" +
 | 
			
		||||
                    "}\n```" + NEW_LINE +
 | 
			
		||||
                    "Example of SMS 2FA account config:\n" +
 | 
			
		||||
                    "```\n{\n" +
 | 
			
		||||
                    "  \"providerType\": \"SMS\",\n" +
 | 
			
		||||
                    "  \"useByDefault\": false,\n" +
 | 
			
		||||
                    "  \"phoneNumber\": \"+38012312321\"\n" +
 | 
			
		||||
                    "}\n```" + NEW_LINE +
 | 
			
		||||
                    "For TOTP this method does nothing." + NEW_LINE +
 | 
			
		||||
                    "Will throw an error (Bad Request) if submitted account config is not valid, " +
 | 
			
		||||
                    "or if the provider is not configured for usage. " +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
 | 
			
		||||
    @PostMapping("/account/config/submit")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    public void submitTwoFaAccountConfig(@ApiParam(value = "2FA account config value. For TOTP 2FA config, authUrl value must not be blank and must match specific pattern. " +
 | 
			
		||||
            "For SMS 2FA, phoneNumber property must not be blank and must be of E.164 phone number format.", required = true)
 | 
			
		||||
                                         @Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception {
 | 
			
		||||
    public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
        twoFactorAuthService.prepareVerificationCode(user, accountConfig, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Verify and save 2FA account config (verifyAndSaveTwoFaAccountConfig)",
 | 
			
		||||
            notes = "Checks the verification code for submitted config, and if it is correct, saves the provided account config. " +
 | 
			
		||||
                    "The config is stored in the user's additionalInfo. " + NEW_LINE +
 | 
			
		||||
            notes = "Checks the verification code for submitted config, and if it is correct, saves the provided account config. " + NEW_LINE +
 | 
			
		||||
                    "Returns whole account's 2FA settings object.\n" +
 | 
			
		||||
                    "Will throw an error (Bad Request) if the provider is not configured for usage. " +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
 | 
			
		||||
    @PostMapping("/account/config")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@ApiParam(value = "2FA account config to save. Validation rules are the same as in submitTwoFaAccountConfig API method", required = true)
 | 
			
		||||
                                                                @Valid @RequestBody TwoFaAccountConfig accountConfig,
 | 
			
		||||
                                                                @ApiParam(value = "6-digit code from an authenticator app in case of TOTP 2FA, or the one sent via an SMS message in case of SMS 2FA", required = true)
 | 
			
		||||
    public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig,
 | 
			
		||||
                                                                @ApiParam(value = "6-digit code from an authenticator app in case of TOTP 2FA, or the one sent via an SMS or email message in case of SMS or EMAIL 2FA", required = true)
 | 
			
		||||
                                                                @RequestParam String verificationCode) throws Exception {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
        if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig.getProviderType()).isPresent()) {
 | 
			
		||||
            throw new IllegalArgumentException("2FA provider is already configured");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false);
 | 
			
		||||
        if (verificationSuccess) {
 | 
			
		||||
            return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.INVALID_ARGUMENTS);
 | 
			
		||||
            throw new IllegalArgumentException("Verification code is incorrect");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Update 2FA account config (updateTwoFaAccountConfig)", notes =
 | 
			
		||||
            "Update config for a given provider type. \n" +
 | 
			
		||||
                    "Update request example:\n" +
 | 
			
		||||
                    "```\n{\n  \"useByDefault\": true\n}\n```\n" +
 | 
			
		||||
                    "Returns whole account's 2FA settings object.\n" +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
 | 
			
		||||
    @PutMapping("/account/config")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType,
 | 
			
		||||
                                                         @RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
        TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType)
 | 
			
		||||
                .orElseThrow(() -> new IllegalArgumentException("No 2FA config for provider " + providerType));
 | 
			
		||||
 | 
			
		||||
        AccountTwoFaSettings settings = twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId())
 | 
			
		||||
                .orElseThrow(() -> new IllegalArgumentException("No 2FA config found"));
 | 
			
		||||
        Map<TwoFaProviderType, TwoFaAccountConfig> configs = settings.getConfigs();
 | 
			
		||||
 | 
			
		||||
        TwoFaAccountConfig accountConfig;
 | 
			
		||||
        if ((accountConfig = configs.get(providerType)) == null) {
 | 
			
		||||
            throw new IllegalArgumentException("Config for " + providerType + " 2FA provider not found");
 | 
			
		||||
        }
 | 
			
		||||
        if (updateRequest.isUseByDefault()) {
 | 
			
		||||
            configs.values().forEach(config -> config.setUseByDefault(false));
 | 
			
		||||
        }
 | 
			
		||||
        accountConfig.setUseByDefault(updateRequest.isUseByDefault());
 | 
			
		||||
        return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
 | 
			
		||||
 | 
			
		||||
        return twoFaConfigManager.saveAccountTwoFaSettings(user.getTenantId(), user.getId(), settings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)",
 | 
			
		||||
            notes = "Delete user's 2FA config. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
 | 
			
		||||
    @ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", notes =
 | 
			
		||||
            "Delete 2FA config for a given 2FA provider type. \n" +
 | 
			
		||||
                    "Returns whole account's 2FA settings object.\n" +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
 | 
			
		||||
    @DeleteMapping("/account/config")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException {
 | 
			
		||||
@ -186,6 +219,12 @@ public class TwoFaConfigController extends BaseController {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
 | 
			
		||||
            "Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" +
 | 
			
		||||
                    "Example of response:\n" +
 | 
			
		||||
                    "```\n[\n  \"TOTP\",\n  \"EMAIL\",\n  \"SMS\"\n]\n```" +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER
 | 
			
		||||
    )
 | 
			
		||||
    @GetMapping("/providers")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException {
 | 
			
		||||
@ -196,8 +235,9 @@ public class TwoFaConfigController extends BaseController {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get 2FA settings (getTwoFaSettings)", // FIXME [viacheslav]
 | 
			
		||||
            notes = "Get settings for 2FA. If 2FA is not configured, then an empty response will be returned." +
 | 
			
		||||
    @ApiOperation(value = "Get platform 2FA settings (getPlatformTwoFaSettings)",
 | 
			
		||||
            notes = "Get platform settings for 2FA. The settings are described for savePlatformTwoFaSettings API method. " +
 | 
			
		||||
                    "If 2FA is not configured, then an empty response will be returned." +
 | 
			
		||||
                    ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
 | 
			
		||||
    @GetMapping("/settings")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
 | 
			
		||||
@ -205,9 +245,49 @@ public class TwoFaConfigController extends BaseController {
 | 
			
		||||
        return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), false).orElse(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Save 2FA settings (saveTwoFaSettings)",
 | 
			
		||||
            notes = "Save settings for 2FA. If a user is sysadmin - the settings are saved as AdminSettings; " +
 | 
			
		||||
                    "if it is a tenant admin - as a tenant attribute." +
 | 
			
		||||
    @ApiOperation(value = "Save platform 2FA settings (savePlatformTwoFaSettings)",
 | 
			
		||||
            notes = "Save 2FA settings for platform. The settings have following properties:\n" +
 | 
			
		||||
                    "- `useSystemTwoFactorAuthSettings` - option for tenant admins to use 2FA settings configured by sysadmin. " +
 | 
			
		||||
                    "If this param is set to true, then the settings will not be validated for constraints (if it is a tenant admin; for sysadmin this param is ignored).\n" +
 | 
			
		||||
                    "- `providers` - the list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list. \n\n" +
 | 
			
		||||
                    "- `verificationCodeSendRateLimit` - rate limit configuration for verification code sending. " +
 | 
			
		||||
                    "The format is standard: 'amountOfRequests:periodInSeconds'. The value of '1:60' would limit verification " +
 | 
			
		||||
                    "code sending requests to one per minute.\n" +
 | 
			
		||||
                    "- `verificationCodeCheckRateLimit` - rate limit configuration for verification code checking.\n" +
 | 
			
		||||
                    "- `maxVerificationFailuresBeforeUserLockout` - maximum number of verification failures before a user gets disabled.\n" +
 | 
			
		||||
                    "- `totalAllowedTimeForVerification` - total amount of time in seconds allotted for verification. " +
 | 
			
		||||
                    "Basically, this property sets a lifetime for pre-verification token. If not set, default value of 30 minutes is used.\n" + NEW_LINE +
 | 
			
		||||
                    "TOTP 2FA provider config has following settings:\n" +
 | 
			
		||||
                    "- `issuerName` - issuer name that will be displayed in an authenticator app near a username. Must not be blank.\n\n" +
 | 
			
		||||
                    "For SMS 2FA provider:\n" +
 | 
			
		||||
                    "- `smsVerificationMessageTemplate` - verification message template.  Available template variables " +
 | 
			
		||||
                    "are ${verificationCode} and ${userEmail}. It must not be blank and must contain verification code variable.\n" +
 | 
			
		||||
                    "- `verificationCodeLifetime` - verification code lifetime in seconds. Required to be positive.\n\n" +
 | 
			
		||||
                    "For EMAIL provider type:\n" +
 | 
			
		||||
                    "- `verificationCodeLifetime` - the same as for SMS." + NEW_LINE +
 | 
			
		||||
                    "Example of the settings:\n" +
 | 
			
		||||
                    "```\n{\n" +
 | 
			
		||||
                    "  \"useSystemTwoFactorAuthSettings\": false,\n" +
 | 
			
		||||
                    "  \"providers\": [\n" +
 | 
			
		||||
                    "    {\n" +
 | 
			
		||||
                    "      \"providerType\": \"TOTP\",\n" +
 | 
			
		||||
                    "      \"issuerName\": \"TB\"\n" +
 | 
			
		||||
                    "    },\n" +
 | 
			
		||||
                    "    {\n" +
 | 
			
		||||
                    "      \"providerType\": \"EMAIL\",\n" +
 | 
			
		||||
                    "      \"verificationCodeLifetime\": 60\n" +
 | 
			
		||||
                    "    },\n" +
 | 
			
		||||
                    "    {\n" +
 | 
			
		||||
                    "      \"providerType\": \"SMS\",\n" +
 | 
			
		||||
                    "      \"verificationCodeLifetime\": 60,\n" +
 | 
			
		||||
                    "      \"smsVerificationMessageTemplate\": \"Here is your verification code: ${verificationCode}\"\n" +
 | 
			
		||||
                    "    }\n" +
 | 
			
		||||
                    "  ],\n" +
 | 
			
		||||
                    "  \"verificationCodeSendRateLimit\": \"1:60\",\n" +
 | 
			
		||||
                    "  \"verificationCodeCheckRateLimit\": \"3:900\",\n" +
 | 
			
		||||
                    "  \"maxVerificationFailuresBeforeUserLockout\": 10,\n" +
 | 
			
		||||
                    "  \"totalAllowedTimeForVerification\": 600\n" +
 | 
			
		||||
                    "}\n```" +
 | 
			
		||||
                    ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
 | 
			
		||||
    @PostMapping("/settings")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
 | 
			
		||||
 | 
			
		||||
@ -84,8 +84,8 @@ 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(@ApiParam(value = "6-digit verification code", required = true)
 | 
			
		||||
                                                   @RequestParam TwoFaProviderType providerType,
 | 
			
		||||
    public JwtTokenPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType,
 | 
			
		||||
                                                   @ApiParam(value = "6-digit verification code", required = true)
 | 
			
		||||
                                                   @RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
        boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
 | 
			
		||||
@ -101,6 +101,13 @@ public class TwoFactorAuthController extends BaseController {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
 | 
			
		||||
            "Get the list of 2FA provider infos available for user to use. Example:\n" +
 | 
			
		||||
                    "```\n[\n" +
 | 
			
		||||
                    "  {\n    \"type\": \"EMAIL\",\n    \"default\": true\n  },\n" +
 | 
			
		||||
                    "  {\n    \"type\": \"TOTP\",\n    \"default\": false\n  },\n" +
 | 
			
		||||
                    "  {\n    \"type\": \"SMS\",\n    \"default\": false\n  }\n" +
 | 
			
		||||
                    "]\n```")
 | 
			
		||||
    @GetMapping("/providers")
 | 
			
		||||
    @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
 | 
			
		||||
    public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException {
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,6 @@ import org.thingsboard.server.common.data.ApiUsageStateValue;
 | 
			
		||||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
 | 
			
		||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
 | 
			
		||||
import org.thingsboard.server.common.data.id.CustomerId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.EntityId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.dao.exception.IncorrectParameterException;
 | 
			
		||||
import org.thingsboard.server.dao.settings.AdminSettingsService;
 | 
			
		||||
@ -311,6 +310,14 @@ public class DefaultMailService implements MailService {
 | 
			
		||||
        sendMail(mailSender, mailFrom, email, subject, message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendTwoFaVerificationEmail(String email, String verificationCode) throws ThingsboardException { // TODO [viacheslav]: mail template
 | 
			
		||||
        String subject = "ThingsBoard two-factor authentication";
 | 
			
		||||
        String message = "Your 2FA verification code: " + verificationCode;
 | 
			
		||||
 | 
			
		||||
        sendMail(mailSender, mailFrom, email, subject, message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException {
 | 
			
		||||
        String subject = messages.getMessage("api.usage.state", null, Locale.US);
 | 
			
		||||
 | 
			
		||||
@ -68,6 +68,20 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings) {
 | 
			
		||||
        UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
 | 
			
		||||
                .orElseGet(() -> {
 | 
			
		||||
                    UserAuthSettings newUserAuthSettings = new UserAuthSettings();
 | 
			
		||||
                    newUserAuthSettings.setUserId(userId);
 | 
			
		||||
                    return newUserAuthSettings;
 | 
			
		||||
                });
 | 
			
		||||
        userAuthSettings.setTwoFaSettings(settings);
 | 
			
		||||
        userAuthSettingsDao.save(tenantId, userAuthSettings);
 | 
			
		||||
        return settings;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
 | 
			
		||||
        return getAccountTwoFaSettings(tenantId, userId)
 | 
			
		||||
@ -80,33 +94,21 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
 | 
			
		||||
        getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
 | 
			
		||||
                .orElseThrow(() -> new IllegalArgumentException("2FA provider is not configured"));
 | 
			
		||||
 | 
			
		||||
        return createOrUpdateAccountTwoFaSettings(tenantId, userId, accountTwoFaSettings -> {
 | 
			
		||||
            Map<TwoFaProviderType, TwoFaAccountConfig> configs = accountTwoFaSettings.getConfigs();
 | 
			
		||||
            configs.put(accountConfig.getProviderType(), accountConfig);
 | 
			
		||||
        AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId).orElseGet(() -> {
 | 
			
		||||
            AccountTwoFaSettings newSettings = new AccountTwoFaSettings();
 | 
			
		||||
            newSettings.setConfigs(new LinkedHashMap<>());
 | 
			
		||||
            return newSettings;
 | 
			
		||||
        });
 | 
			
		||||
        settings.getConfigs().put(accountConfig.getProviderType(), accountConfig);
 | 
			
		||||
        return saveAccountTwoFaSettings(tenantId, userId, settings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
 | 
			
		||||
        return createOrUpdateAccountTwoFaSettings(tenantId, userId, accountTwoFaSettings -> {
 | 
			
		||||
            accountTwoFaSettings.getConfigs().keySet().removeIf(providerType::equals);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private AccountTwoFaSettings createOrUpdateAccountTwoFaSettings(TenantId tenantId, UserId userId, Consumer<AccountTwoFaSettings> updater) {
 | 
			
		||||
        UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
 | 
			
		||||
                .orElseGet(() -> {
 | 
			
		||||
                    UserAuthSettings newUserAuthSettings = new UserAuthSettings();
 | 
			
		||||
                    newUserAuthSettings.setUserId(userId);
 | 
			
		||||
 | 
			
		||||
                    AccountTwoFaSettings newAccountTwoFaSettings = new AccountTwoFaSettings();
 | 
			
		||||
                    newAccountTwoFaSettings.setConfigs(new LinkedHashMap<>());
 | 
			
		||||
                    newUserAuthSettings.setTwoFaSettings(newAccountTwoFaSettings);
 | 
			
		||||
                    return newUserAuthSettings;
 | 
			
		||||
                });
 | 
			
		||||
        updater.accept(userAuthSettings.getTwoFaSettings());
 | 
			
		||||
        userAuthSettingsDao.save(tenantId, userAuthSettings);
 | 
			
		||||
        return userAuthSettings.getTwoFaSettings();
 | 
			
		||||
        AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId)
 | 
			
		||||
                .orElseThrow(() -> new IllegalArgumentException("2FA not configured"));
 | 
			
		||||
        settings.getConfigs().remove(providerType);
 | 
			
		||||
        return saveAccountTwoFaSettings(tenantId, userId, settings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,9 +26,10 @@ import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
public interface TwoFaConfigManager {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId);
 | 
			
		||||
 | 
			
		||||
    AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings);
 | 
			
		||||
 | 
			
		||||
    Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType);
 | 
			
		||||
 | 
			
		||||
    AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig);
 | 
			
		||||
 | 
			
		||||
@ -26,9 +26,9 @@ public interface TwoFaProvider<C extends TwoFaProviderConfig, A extends TwoFaAcc
 | 
			
		||||
 | 
			
		||||
    A generateNewAccountConfig(User user, C providerConfig);
 | 
			
		||||
 | 
			
		||||
    default void prepareVerificationCode(SecurityUser securityUser, C providerConfig, A accountConfig) throws ThingsboardException {}
 | 
			
		||||
    default void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException {}
 | 
			
		||||
 | 
			
		||||
    boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, C providerConfig, A accountConfig);
 | 
			
		||||
    boolean checkVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    TwoFaProviderType getType();
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,57 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.mfa.provider.impl;
 | 
			
		||||
 | 
			
		||||
import org.springframework.cache.CacheManager;
 | 
			
		||||
import org.springframework.stereotype.Service;
 | 
			
		||||
import org.thingsboard.rule.engine.api.MailService;
 | 
			
		||||
import org.thingsboard.server.common.data.User;
 | 
			
		||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.account.EmailTwoFaAccountConfig;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.provider.EmailTwoFaProviderConfig;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
 | 
			
		||||
import org.thingsboard.server.queue.util.TbCoreComponent;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
@TbCoreComponent
 | 
			
		||||
public class EmailTwoFaProvider extends OtpBasedTwoFaProvider<EmailTwoFaProviderConfig, EmailTwoFaAccountConfig> {
 | 
			
		||||
 | 
			
		||||
    private final MailService mailService;
 | 
			
		||||
 | 
			
		||||
    protected EmailTwoFaProvider(CacheManager cacheManager, MailService mailService) {
 | 
			
		||||
        super(cacheManager);
 | 
			
		||||
        this.mailService = mailService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public EmailTwoFaAccountConfig generateNewAccountConfig(User user, EmailTwoFaProviderConfig providerConfig) {
 | 
			
		||||
        EmailTwoFaAccountConfig config = new EmailTwoFaAccountConfig();
 | 
			
		||||
        config.setEmail(user.getEmail());
 | 
			
		||||
        return config;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void sendVerificationCode(SecurityUser user, String verificationCode, EmailTwoFaProviderConfig providerConfig, EmailTwoFaAccountConfig accountConfig) throws ThingsboardException {
 | 
			
		||||
        mailService.sendTwoFaVerificationEmail(accountConfig.getEmail(), verificationCode);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TwoFaProviderType getType() {
 | 
			
		||||
        return TwoFaProviderType.EMAIL;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -38,27 +38,27 @@ public abstract class OtpBasedTwoFaProvider<C extends OtpBasedTwoFaProviderConfi
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void prepareVerificationCode(SecurityUser securityUser, C providerConfig, A accountConfig) throws ThingsboardException {
 | 
			
		||||
    public final void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException {
 | 
			
		||||
        String verificationCode = RandomStringUtils.randomNumeric(6);
 | 
			
		||||
        verificationCodesCache.put(securityUser.getId(), new Otp(System.currentTimeMillis(), verificationCode, accountConfig));
 | 
			
		||||
        sendVerificationCode(securityUser, verificationCode, providerConfig, accountConfig);
 | 
			
		||||
        sendVerificationCode(user, verificationCode, providerConfig, accountConfig);
 | 
			
		||||
        verificationCodesCache.put(user.getId(), new Otp(System.currentTimeMillis(), verificationCode, accountConfig));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract void sendVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) throws ThingsboardException;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, C providerConfig, A accountConfig) {
 | 
			
		||||
        Otp correctVerificationCode = verificationCodesCache.get(securityUser.getId(), Otp.class);
 | 
			
		||||
    public final boolean checkVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) {
 | 
			
		||||
        Otp correctVerificationCode = verificationCodesCache.get(user.getId(), Otp.class);
 | 
			
		||||
        if (correctVerificationCode != null) {
 | 
			
		||||
            if (System.currentTimeMillis() - correctVerificationCode.getTimestamp()
 | 
			
		||||
                    > TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) {
 | 
			
		||||
                verificationCodesCache.evict(securityUser.getId());
 | 
			
		||||
                verificationCodesCache.evict(user.getId());
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            if (verificationCode.equals(correctVerificationCode.getValue())
 | 
			
		||||
                    && accountConfig.equals(correctVerificationCode.getAccountConfig())) {
 | 
			
		||||
                verificationCodesCache.evict(securityUser.getId());
 | 
			
		||||
                verificationCodesCache.evict(user.getId());
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@ public class TotpTwoFaProvider implements TwoFaProvider<TotpTwoFaProviderConfig,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TotpTwoFaProviderConfig providerConfig, TotpTwoFaAccountConfig accountConfig) {
 | 
			
		||||
    public final boolean checkVerificationCode(SecurityUser user, String verificationCode, TotpTwoFaProviderConfig providerConfig, TotpTwoFaAccountConfig accountConfig) {
 | 
			
		||||
        String secretKey = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build().getQueryParams().getFirst("secret");
 | 
			
		||||
        return new Totp(secretKey).verify(verificationCode);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -15,11 +15,9 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.common.data.security.model.mfa;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
 | 
			
		||||
 | 
			
		||||
import javax.validation.Valid;
 | 
			
		||||
import javax.validation.constraints.Min;
 | 
			
		||||
@ -28,29 +26,18 @@ import java.util.List;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
@ApiModel
 | 
			
		||||
public class PlatformTwoFaSettings {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "Option for tenant admins to use 2FA settings configured by sysadmin. " +
 | 
			
		||||
            "If this param is set to true, then the settings will not be validated for constraints " +
 | 
			
		||||
            "(if it is a tenant admin; for sysadmin this param is ignored)")
 | 
			
		||||
    private boolean useSystemTwoFactorAuthSettings;
 | 
			
		||||
    @ApiModelProperty(value = "The list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list.")
 | 
			
		||||
    @Valid
 | 
			
		||||
    private List<TwoFaProviderConfig> providers;
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "Rate limit configuration for verification code sending. The format is standard: 'amountOfRequests:periodInSeconds'. " +
 | 
			
		||||
            "The value of '1:60' would limit verification code sending requests to one per minute.", example = "1:60", required = false)
 | 
			
		||||
    @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code send rate limit configuration is invalid")
 | 
			
		||||
    private String verificationCodeSendRateLimit;
 | 
			
		||||
    @ApiModelProperty(value = "Rate limit configuration for verification code checking.", example = "3:900", required = false)
 | 
			
		||||
    @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid")
 | 
			
		||||
    private String verificationCodeCheckRateLimit;
 | 
			
		||||
    @ApiModelProperty(value = "Maximum number of verification failures before a user gets disabled.", example = "10", required = false)
 | 
			
		||||
    @Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
 | 
			
		||||
    private int maxVerificationFailuresBeforeUserLockout;
 | 
			
		||||
    @ApiModelProperty(value = "Total amount of time in seconds allotted for verification. " +
 | 
			
		||||
            "Basically, this property sets a lifetime for pre-verification token. If not set, default value of 30 minutes is used.", example = "3600", required = false)
 | 
			
		||||
    @Min(value = 1, message = "total amount of time allotted for verification must be greater than 0")
 | 
			
		||||
    private Integer totalAllowedTimeForVerification;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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.common.data.security.model.mfa.account;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.Email;
 | 
			
		||||
import javax.validation.constraints.NotBlank;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
public class EmailTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig {
 | 
			
		||||
 | 
			
		||||
    @NotBlank
 | 
			
		||||
    @Email
 | 
			
		||||
    private String email;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TwoFaProviderType getProviderType() {
 | 
			
		||||
        return TwoFaProviderType.EMAIL;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -15,8 +15,6 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.common.data.security.model.mfa.account;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
 | 
			
		||||
@ -24,12 +22,10 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi
 | 
			
		||||
import javax.validation.constraints.NotBlank;
 | 
			
		||||
import javax.validation.constraints.Pattern;
 | 
			
		||||
 | 
			
		||||
@ApiModel
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
@Data
 | 
			
		||||
public class SmsTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "Phone number to use for 2FA. Must no be blank and must be of E.164 number format.", required = true)
 | 
			
		||||
    @NotBlank(message = "phone number cannot be blank")
 | 
			
		||||
    @Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format")
 | 
			
		||||
    private String phoneNumber;
 | 
			
		||||
 | 
			
		||||
@ -15,8 +15,6 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.common.data.security.model.mfa.account;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
 | 
			
		||||
@ -24,13 +22,10 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi
 | 
			
		||||
import javax.validation.constraints.NotBlank;
 | 
			
		||||
import javax.validation.constraints.Pattern;
 | 
			
		||||
 | 
			
		||||
@ApiModel // FIXME [viacheslav]
 | 
			
		||||
@Data
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
public class TotpTwoFaAccountConfig extends TwoFaAccountConfig {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "OTP auth URL used to generate a QR code to scan with an authenticator app. Must not be blank and must follow specific pattern.",
 | 
			
		||||
            example = "otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII", required = true)
 | 
			
		||||
    @NotBlank(message = "OTP auth URL cannot be blank")
 | 
			
		||||
    @Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid")
 | 
			
		||||
    private String authUrl;
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,8 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi
 | 
			
		||||
        property = "providerType")
 | 
			
		||||
@JsonSubTypes({
 | 
			
		||||
        @Type(name = "TOTP", value = TotpTwoFaAccountConfig.class),
 | 
			
		||||
        @Type(name = "SMS", value = SmsTwoFaAccountConfig.class)
 | 
			
		||||
        @Type(name = "SMS", value = SmsTwoFaAccountConfig.class),
 | 
			
		||||
        @Type(name = "EMAIL", value = EmailTwoFaAccountConfig.class)
 | 
			
		||||
})
 | 
			
		||||
@Data
 | 
			
		||||
public abstract class TwoFaAccountConfig {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,30 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.common.data.security.model.mfa.provider;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
public class EmailTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TwoFaProviderType getProviderType() {
 | 
			
		||||
        return TwoFaProviderType.EMAIL;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -15,15 +15,14 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.common.data.security.model.mfa.provider;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.Min;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
public abstract class OtpBasedTwoFaProviderConfig implements TwoFaProviderConfig {
 | 
			
		||||
    @ApiModelProperty(value = "Verification code lifetime in seconds. Verification codes with a lifetime bigger than this param " +
 | 
			
		||||
            "will be considered incorrect", example = "60", required = true)
 | 
			
		||||
 | 
			
		||||
    @Min(value = 1, message = "verification code lifetime is required")
 | 
			
		||||
    private int verificationCodeLifetime;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,22 +15,16 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.common.data.security.model.mfa.provider;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotBlank;
 | 
			
		||||
import javax.validation.constraints.Pattern;
 | 
			
		||||
 | 
			
		||||
@ApiModel(parent = OtpBasedTwoFaProviderConfig.class)
 | 
			
		||||
@EqualsAndHashCode(callSuper = true)
 | 
			
		||||
@Data
 | 
			
		||||
public class SmsTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "SMS verification message template. Available template variables are ${verificationCode} and ${userEmail}. " +
 | 
			
		||||
            "It must not be blank and must contain verification code variable.",
 | 
			
		||||
            example = "Here is your verification code: ${verificationCode}", required = true)
 | 
			
		||||
    @NotBlank(message = "verification message template is required")
 | 
			
		||||
    @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code")
 | 
			
		||||
    private String smsVerificationMessageTemplate;
 | 
			
		||||
 | 
			
		||||
@ -15,18 +15,13 @@
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.common.data.security.model.mfa.provider;
 | 
			
		||||
 | 
			
		||||
import io.swagger.annotations.ApiModel;
 | 
			
		||||
import io.swagger.annotations.ApiModelProperty;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotBlank;
 | 
			
		||||
 | 
			
		||||
@ApiModel
 | 
			
		||||
@Data
 | 
			
		||||
public class TotpTwoFaProviderConfig implements TwoFaProviderConfig {
 | 
			
		||||
 | 
			
		||||
    @ApiModelProperty(value = "Issuer name that will be displayed in an authenticator app near a username. " +
 | 
			
		||||
            "Must not be blank.", example = "ThingsBoard", required = true)
 | 
			
		||||
    @NotBlank(message = "issuer name must not be blank")
 | 
			
		||||
    private String issuerName;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
 | 
			
		||||
        property = "providerType")
 | 
			
		||||
@JsonSubTypes({
 | 
			
		||||
        @Type(name = "TOTP", value = TotpTwoFaProviderConfig.class),
 | 
			
		||||
        @Type(name = "SMS", value = SmsTwoFaProviderConfig.class)
 | 
			
		||||
        @Type(name = "SMS", value = SmsTwoFaProviderConfig.class),
 | 
			
		||||
        @Type(name = "EMAIL", value = EmailTwoFaProviderConfig.class)
 | 
			
		||||
})
 | 
			
		||||
public interface TwoFaProviderConfig {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,5 +17,6 @@ package org.thingsboard.server.common.data.security.model.mfa.provider;
 | 
			
		||||
 | 
			
		||||
public enum TwoFaProviderType {
 | 
			
		||||
    TOTP,
 | 
			
		||||
    SMS
 | 
			
		||||
    SMS,
 | 
			
		||||
    EMAIL
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -24,8 +24,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
 | 
			
		||||
import org.thingsboard.server.common.data.id.CustomerId;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
public interface MailService {
 | 
			
		||||
 | 
			
		||||
    void updateMailConfiguration();
 | 
			
		||||
@ -46,6 +44,8 @@ public interface MailService {
 | 
			
		||||
 | 
			
		||||
    void sendAccountLockoutEmail(String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException;
 | 
			
		||||
 | 
			
		||||
    void sendTwoFaVerificationEmail(String email, String verificationCode) throws ThingsboardException;
 | 
			
		||||
 | 
			
		||||
    void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail) throws ThingsboardException;
 | 
			
		||||
 | 
			
		||||
    void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender) throws ThingsboardException;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user