diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java index 6eb192b7e2..60541b1ce4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java @@ -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 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 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')") diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index a1fe5a7cfc..a43a73896e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -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 getAvailableTwoFaProviders() throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index fa7cb2b7ff..c6ae0c70e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -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); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java index 9f9b4e4b0a..75ca7b1e45 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java @@ -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 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 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 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); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java index b0073338b7..f1a4590572 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java @@ -26,9 +26,10 @@ import java.util.Optional; public interface TwoFaConfigManager { - Optional getAccountTwoFaSettings(TenantId tenantId, UserId userId); + AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings); + Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType); AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java index 4d4db92af1..e5c278942b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java @@ -26,9 +26,9 @@ public interface TwoFaProvider { + + 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; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java index 30f6fe4f50..f270316d2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java @@ -38,27 +38,27 @@ public abstract class OtpBasedTwoFaProvider 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; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java index 9b251e5b4a..289ddb7c9d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java @@ -45,7 +45,7 @@ public class TotpTwoFaProvider implements TwoFaProvider 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; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java new file mode 100644 index 0000000000..a2170d7a54 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java @@ -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; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java index 67d9d499ae..84a7b6924e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java @@ -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; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java index ecafde86ce..5cdec02e90 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java @@ -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; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java index 5d366205cd..6bf2ee8fd3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java @@ -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 { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java new file mode 100644 index 0000000000..787f5b561d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java @@ -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; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java index 23d64c79aa..f1595e733f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java @@ -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; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java index 81efb7058e..e0ed0587d9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java @@ -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; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java index b631d367e5..32f443f785 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java @@ -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; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java index 69d9989af4..65d9242900 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java @@ -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 { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java index 7e6f25195e..dd36f24394 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java @@ -17,5 +17,6 @@ package org.thingsboard.server.common.data.security.model.mfa.provider; public enum TwoFaProviderType { TOTP, - SMS + SMS, + EMAIL } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java index 86da82187d..10c37da564 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java @@ -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;