diff --git a/application/pom.xml b/application/pom.xml index 1163629012..8093bf6eb2 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -345,18 +345,6 @@ org.jboss.aerogear aerogear-otp-java - - - com.google.zxing - core - 3.3.0 - - - com.google.zxing - javase - 3.3.0 - - 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 d974189bab..907618c26f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java @@ -15,10 +15,6 @@ */ package org.thingsboard.server.controller; -import com.google.zxing.BarcodeFormat; -import com.google.zxing.client.j2se.MatrixToImageWriter; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.qrcode.QRCodeWriter; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.Data; @@ -32,11 +28,9 @@ import org.springframework.web.bind.annotation.RequestBody; 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.ThingsboardException; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; -import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; @@ -45,8 +39,6 @@ import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.model.SecurityUser; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.util.Collections; import java.util.List; @@ -113,21 +105,6 @@ public class TwoFaConfigController extends BaseController { return twoFactorAuthService.generateNewAccountConfig(user, providerType); } - /* TMP */ - @PostMapping("/account/config/tmp/generate/qr") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public void generateTwoFaAccountConfigWithQr(@RequestParam TwoFaProviderType providerType, HttpServletResponse response) throws Exception { - TwoFaAccountConfig config = generateTwoFaAccountConfig(providerType); - if (providerType == TwoFaProviderType.TOTP) { - BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFaAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200); - try (ServletOutputStream outputStream = response.getOutputStream()) { - MatrixToImageWriter.writeToStream(qr, "PNG", outputStream); - } - } - response.setHeader("config", JacksonUtil.toString(config)); - } - /* TMP */ - @ApiOperation(value = "Submit 2FA account config (submitTwoFaAccountConfig)", 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 " + 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 a43a73896e..ea120ce432 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -27,9 +27,12 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; 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.account.EmailTwoFaAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; @@ -104,9 +107,9 @@ 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 \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" + + " {\n \"type\": \"TOTP\",\n \"default\": false,\n \"contact\": null\n },\n" + + " {\n \"type\": \"SMS\",\n \"default\": false,\n \"contact\": \"+38********12\"\n }\n" + "]\n```") @GetMapping("/providers") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") @@ -114,10 +117,24 @@ public class TwoFactorAuthController extends BaseController { SecurityUser user = getCurrentUser(); return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()) .map(settings -> settings.getConfigs().values()).orElse(Collections.emptyList()) - .stream().map(config -> TwoFaProviderInfo.builder() - .type(config.getProviderType()) - .isDefault(config.isUseByDefault()) - .build()) + .stream().map(config -> { + String contact = null; + switch (config.getProviderType()) { + case SMS: + String phoneNumber = ((SmsTwoFaAccountConfig) config).getPhoneNumber(); + contact = StringUtils.obfuscate(phoneNumber, 2, '*', phoneNumber.indexOf('+') + 1, phoneNumber.length()); + break; + case EMAIL: + String email = ((EmailTwoFaAccountConfig) config).getEmail(); + contact = StringUtils.obfuscate(email, 2, '*', 0, email.indexOf('@')); + break; + } + return TwoFaProviderInfo.builder() + .type(config.getProviderType()) + .isDefault(config.isUseByDefault()) + .contact(contact) + .build(); + }) .collect(Collectors.toList()); } @@ -127,6 +144,7 @@ public class TwoFactorAuthController extends BaseController { public static class TwoFaProviderInfo { private TwoFaProviderType type; private boolean isDefault; + private String contact; } } 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 c6ae0c70e4..fdb1b579e9 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 @@ -47,6 +47,7 @@ import org.thingsboard.server.queue.usagestats.TbApiUsageClient; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import javax.annotation.PostConstruct; +import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.ByteArrayInputStream; import java.util.HashMap; @@ -100,7 +101,17 @@ public class DefaultMailService implements MailService { mailSender = createMailSender(jsonConfig); mailFrom = jsonConfig.get("mailFrom").asText(); } else { - throw new IncorrectParameterException("Failed to date mail configuration. Settings not found!"); + throw new IncorrectParameterException("Failed to update mail configuration. Settings not found!"); + } + } + + @Override + public boolean isConfigured(TenantId tenantId) { + try { + mailSender.testConnection(); + return true; + } catch (MessagingException e) { + return false; } } @@ -311,9 +322,12 @@ public class DefaultMailService implements MailService { } @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; + public void sendTwoFaVerificationEmail(String email, String verificationCode) throws ThingsboardException { + String subject = messages.getMessage("2fa.verification.code.subject", null, Locale.US); + String message = mergeTemplateIntoString("2fa.verification.code.ftl", Map.of( + TARGET_EMAIL, email, + "verificationCode", verificationCode + )); sendMail(mailSender, mailFrom, email, subject, message); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index fd4f16ccdb..c3ab2c6650 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -68,6 +68,11 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { .orElse(false); } + @Override + public void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException { + getTwoFaProvider(providerType).check(tenantId); + } + @Override public void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index b959f94acb..aabda27ec7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -27,6 +27,8 @@ public interface TwoFactorAuthService { boolean isTwoFaEnabled(TenantId tenantId, UserId userId); + void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException; + void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception; 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 a1c56e4cd7..8227a3596a 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 @@ -17,10 +17,13 @@ package org.thingsboard.server.service.security.auth.mfa.config; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; @@ -36,6 +39,7 @@ import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.settings.AdminSettingsDao; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserAuthSettingsDao; +import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import java.util.Collections; import java.util.Comparator; @@ -51,6 +55,8 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { private final AdminSettingsService adminSettingsService; private final AdminSettingsDao adminSettingsDao; private final AttributesService attributesService; + @Autowired @Lazy + private TwoFactorAuthService twoFactorAuthService; protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings"; @@ -147,10 +153,13 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { @SneakyThrows({InterruptedException.class, ExecutionException.class}) @Override - public void savePlatformTwoFaSettings(TenantId tenantId, PlatformTwoFaSettings twoFactorAuthSettings) { + public void savePlatformTwoFaSettings(TenantId tenantId, PlatformTwoFaSettings twoFactorAuthSettings) throws ThingsboardException { if (tenantId.equals(TenantId.SYS_TENANT_ID) || !twoFactorAuthSettings.isUseSystemTwoFactorAuthSettings()) { ConstraintValidator.validateFields(twoFactorAuthSettings); } + for (TwoFaProviderConfig providerConfig : twoFactorAuthSettings.getProviders()) { + twoFactorAuthService.checkProvider(tenantId, providerConfig.getProviderType()); + } if (tenantId.equals(TenantId.SYS_TENANT_ID)) { AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) .orElseGet(() -> { 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 212c4697bf..0fe33b7757 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.security.auth.mfa.config; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; @@ -38,7 +39,7 @@ public interface TwoFaConfigManager { Optional getPlatformTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault); - void savePlatformTwoFaSettings(TenantId tenantId, PlatformTwoFaSettings twoFactorAuthSettings); + void savePlatformTwoFaSettings(TenantId tenantId, PlatformTwoFaSettings twoFactorAuthSettings) throws ThingsboardException; void deletePlatformTwoFaSettings(TenantId tenantId); 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 e5c278942b..19cc106657 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 @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.mfa.provider; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; @@ -30,6 +31,8 @@ public interface TwoFaProvider + + + + + + Thingsboard - Api Usage State + + + + + + + + + + + + + + + + + + Your 2FA verification code: ${verificationCode} + + + — The ThingsBoard + + + + + + + + + + + This email was sent to ${targetEmail} by ThingsBoard. + + + + + diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java index f9dc02e8d3..2c4bd9fdb9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -54,6 +54,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -67,11 +68,12 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { private CacheManager cacheManager; @Autowired private TwoFaConfigManager twoFaConfigManager; - @Autowired + @SpyBean private TwoFactorAuthService twoFactorAuthService; @Before public void beforeEach() throws Exception { + doNothing().when(twoFactorAuthService).checkProvider(any(), any()); loginSysAdmin(); } diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 1ad340d54b..74b95284aa 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -26,6 +26,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionStatus; @@ -68,6 +69,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -75,7 +77,7 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest { @Autowired private TwoFaConfigManager twoFaConfigManager; - @Autowired + @SpyBean private TwoFactorAuthService twoFactorAuthService; @MockBean private SmsService smsService; @@ -100,6 +102,7 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest { loginSysAdmin(); user = createUser(user, password); + doNothing().when(twoFactorAuthService).checkProvider(any(), any()); } @After diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index f1391555b0..17f3713b74 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data; +import static org.apache.commons.lang3.StringUtils.repeat; + public class StringUtils { public static boolean isEmpty(String source) { @@ -32,4 +34,20 @@ public class StringUtils { public static boolean isNotBlank(String source) { return source != null && !source.isEmpty() && !source.trim().isEmpty(); } + + public static String obfuscate(String input, int seenMargin, char obfuscationChar, + int startIndexInclusive, int endIndexExclusive) { + + String part = input.substring(startIndexInclusive, endIndexExclusive); + String obfuscatedPart; + if (part.length() <= seenMargin * 2) { + obfuscatedPart = repeat(obfuscationChar, part.length()); + } else { + obfuscatedPart = part.substring(0, seenMargin) + + repeat(obfuscationChar, part.length() - seenMargin * 2) + + part.substring(part.length() - seenMargin); + } + return input.substring(0, startIndexInclusive) + obfuscatedPart + input.substring(endIndexExclusive); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java index 1728fca936..bebddf5aa0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java @@ -42,7 +42,7 @@ import java.util.UUID; @NoArgsConstructor @TypeDef(name = "json", typeClass = JsonStringType.class) @Entity -@Table(name = ModelConstants.USER_AUTH_SETTINGS_COLUMN_FAMILY_NAME) // FIXME [viacheslav]: add to upgrade script +@Table(name = ModelConstants.USER_AUTH_SETTINGS_COLUMN_FAMILY_NAME) public class UserAuthSettingsEntity extends BaseSqlEntity implements BaseEntity { @Column(name = ModelConstants.USER_AUTH_SETTINGS_USER_ID_PROPERTY, nullable = false, unique = true) 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 10c37da564..236391ba16 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 @@ -28,6 +28,8 @@ public interface MailService { void updateMailConfiguration(); + boolean isConfigured(TenantId tenantId); + void sendEmail(TenantId tenantId, String email, String subject, String message) throws ThingsboardException; void sendTestMail(JsonNode config, String email) throws ThingsboardException; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/SmsService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/SmsService.java index d9578783b0..c38f99ecb2 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/SmsService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/SmsService.java @@ -24,6 +24,8 @@ public interface SmsService { void updateSmsConfiguration(); + boolean isConfigured(TenantId tenantId); + void sendSms(TenantId tenantId, CustomerId customerId, String[] numbersTo, String message) throws ThingsboardException;; void sendTestSms(TestSmsRequest testSmsRequest) throws ThingsboardException;