diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 3ed2d4a47d..6dee3b6114 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -283,11 +283,31 @@ public abstract class BaseController { @Getter protected boolean edgesEnabled; + @ExceptionHandler(Exception.class) + public void handleControllerException(Exception e, HttpServletResponse response) { + ThingsboardException thingsboardException = handleException(e); + handleThingsboardException(thingsboardException, response); + } + @ExceptionHandler(ThingsboardException.class) public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { errorResponseHandler.handle(ex, response); } + /** + * @deprecated Exceptions that are not of {@link ThingsboardException} type + * are now caught and mapped to {@link ThingsboardException} by + * {@link ExceptionHandler} {@link BaseController#handleControllerException(Exception, HttpServletResponse)} + * which basically acts like the following boilerplate: + * {@code + * try { + * someExceptionThrowingMethod(); + * } catch (Exception e) { + * throw handleException(e); + * } + * } + * */ + @Deprecated ThingsboardException handleException(Exception exception) { return handleException(exception, true); } 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 1355337127..f0d50c2600 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -30,6 +30,7 @@ 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.dao.service.ConstraintValidator; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; @@ -42,6 +43,9 @@ import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; +// FIXME: Swagger documentation +// FIXME: tests for 2FA + @RestController @RequestMapping("/api") @RequiredArgsConstructor @@ -81,7 +85,7 @@ public class TwoFactorAuthController extends BaseController { MatrixToImageWriter.writeToStream(qr, "PNG", outputStream); } } - response.setHeader("body", JacksonUtil.toString(config)); + response.setHeader("config", JacksonUtil.toString(config)); } @PostMapping("/2fa/account/config/submit") @@ -91,6 +95,7 @@ public class TwoFactorAuthController extends BaseController { twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), (provider, providerConfig) -> { + ConstraintValidator.validateFields(accountConfig); provider.prepareVerificationCode(user, providerConfig, accountConfig); }); } 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 51d4af28fd..2b44fddeb5 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 @@ -46,6 +46,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -144,7 +145,7 @@ public class TwoFactorAuthService { } - @SneakyThrows + @SneakyThrows({InterruptedException.class, ExecutionException.class}) public Optional getTwoFaSettings(TenantId tenantId) { if (tenantId.equals(TenantId.SYS_TENANT_ID)) { return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) @@ -157,7 +158,7 @@ public class TwoFactorAuthService { } } - @SneakyThrows + @SneakyThrows({InterruptedException.class, ExecutionException.class}) public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) { ConstraintValidator.validateFields(twoFactorAuthSettings); if (tenantId.equals(TenantId.SYS_TENANT_ID)) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java index 7d4a0b9ab7..2dda68899b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java @@ -15,18 +15,16 @@ */ package org.thingsboard.server.service.security.auth.mfa.provider.impl; -import lombok.RequiredArgsConstructor; +import lombok.Data; import lombok.SneakyThrows; import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.dao.service.ConstraintValidator; -import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; @@ -34,16 +32,20 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.Collections; import java.util.Map; @Service -@RequiredArgsConstructor @TbCoreComponent public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider { private final SmsService smsService; - private final TimeseriesService timeseriesService; + private final Cache verificationCodesCache; + + public SmsTwoFactorAuthProvider(SmsService smsService, CacheManager cacheManager) { + this.smsService = smsService; + this.verificationCodesCache = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE); + } + @Override public SmsTwoFactorAuthAccountConfig generateNewAccountConfig(User user, SmsTwoFactorAuthProviderConfig providerConfig) { @@ -53,10 +55,8 @@ public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider codeTs.getStrValue().get()) - .orElse(null); - } - - private void removeVerificationCode(SecurityUser user) { - timeseriesService.remove(user.getTenantId(), user.getId(), Collections.singletonList( - new BaseDeleteTsKvQuery("twoFaVerificationCode:" + user.getSessionId(), 0, System.currentTimeMillis()) - )); - } - - @Override public TwoFactorAuthProviderType getType() { return TwoFactorAuthProviderType.SMS; } + + @Data + private static class VerificationCode { + private final long timestamp; + private final String value; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java index ca2f15953a..bb5feb9af6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java @@ -24,6 +24,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; @@ -53,6 +54,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc JwtTokenPair tokenPair; + // fixme: check if this handler is not called when token is refreshed Optional twoFaAccountConfig = twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()); if (twoFaAccountConfig.isPresent()) { try { @@ -62,6 +64,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc }); tokenPair = new JwtTokenPair(); tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser).getToken()); + tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); } catch (Exception e) { log.error("Failed to process 2FA for user {}. Falling back to plain auth", securityUser.getId(), e); tokenPair = tokenFactory.createTokenPair(securityUser); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java b/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java index 57964570c1..02e28cd885 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java @@ -20,10 +20,10 @@ import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.security.Authority; @ApiModel(value = "JWT Token Pair") @Data -@AllArgsConstructor @NoArgsConstructor public class JwtTokenPair { @@ -31,4 +31,12 @@ public class JwtTokenPair { private String token; @ApiModelProperty(position = 1, value = "The JWT Refresh Token. Used to get new JWT Access Token if old one has expired.", example = "AAB254FF67D..") private String refreshToken; + + private Authority scope; + + public JwtTokenPair(String token, String refreshToken) { + this.token = token; + this.refreshToken = refreshToken; + } + } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e79d8979ce..ba6dc222ab 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -428,8 +428,8 @@ caffeine: timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}" maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}" twoFaVerificationCodes: - timeToLiveInMinutes: "1" - maxSize: "100000" + timeToLiveInMinutes: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_TTL:1}" + maxSize: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_MAX_SIZE:100000}" redis: # standalone or cluster diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 6cbf5d82d0..32d55a71dd 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -584,6 +584,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mapper.readerFor(type).readValue(content); } + protected String getErrorMessage(ResultActions result) throws Exception { + return readResponse(result, JsonNode.class).get("message").asText(); + } + public class IdComparator implements Comparator { @Override public int compare(D o1, D o2) { diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java new file mode 100644 index 0000000000..fcc2c8bcdf --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -0,0 +1,256 @@ +/** + * 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.controller; + +import org.jboss.aerogear.security.otp.Totp; +import org.jboss.aerogear.security.otp.api.Base32; +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.thingsboard.rule.engine.api.SmsService; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; +import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; +import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFactorAuthProvider; + +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +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.verify; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class TwoFactorAuthTest extends AbstractControllerTest { + + @SpyBean + private TotpTwoFactorAuthProvider totpTwoFactorAuthProvider; + @MockBean + private SmsService smsService; + + @Before + public void beforeEach() throws Exception { + loginSysAdmin(); + } + + + @Test + public void testSaveTwoFaSettings() throws Exception { + loginSysAdmin(); + testSaveTestTwoFaSettings(); + + loginTenantAdmin(); + testSaveTestTwoFaSettings(); + } + + private void testSaveTestTwoFaSettings() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); + + saveProvidersConfigs(totpTwoFaProviderConfig, smsTwoFaProviderConfig); + + TwoFactorAuthSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class); + + assertThat(savedTwoFaSettings.getProviders()).hasSize(2); + assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig); + } + + @Test + public void testSaveTotpTwoFaProviderConfig_validationError() throws Exception { + TotpTwoFactorAuthProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + invalidTotpTwoFaProviderConfig.setIssuerName(" "); + + String errorResponse = saveTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig); + assertThat(errorResponse).containsIgnoringCase("issuer name must not be blank"); + } + + @Test + public void testSaveSmsTwoFaProviderConfig_validationError() throws Exception { + SmsTwoFactorAuthProviderConfig invalidSmsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); + invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code"); + + String errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); + assertThat(errorResponse).containsIgnoringCase("must contain verification code"); + } + + private String saveTwoFaSettingsAndGetError(TwoFactorAuthProviderConfig invalidTwoFaProviderConfig) throws Exception { + TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); + twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); + + return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) + .andExpect(status().isBadRequest())); + } + + @Test + public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception { + configureSmsTwoFaProvider("${verificationCode}"); + + loginTenantAdmin(); + + TwoFactorAuthProviderType notConfiguredProviderType = TwoFactorAuthProviderType.TOTP; + String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=" + notConfiguredProviderType) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("provider is not configured"); + + TotpTwoFactorAuthAccountConfig notConfiguredProviderAccountConfig = new TotpTwoFactorAuthAccountConfig(); + notConfiguredProviderAccountConfig.setAuthUrl("aba"); + errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", notConfiguredProviderAccountConfig)); + assertThat(errorMessage).containsIgnoringCase("provider is not configured"); + } + + @Test + public void testGenerateTotpTwoFaAccountConfig() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)).isNullOrEmpty(); + generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + } + + @Test + public void testSubmitTotpTwoFaAccountConfig() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + doPost("/api/2fa/account/config/submit", generatedTotpTwoFaAccountConfig).andExpect(status().isOk()); + verify(totpTwoFactorAuthProvider).prepareVerificationCode(argThat(user -> user.getEmail().equals(TENANT_ADMIN_EMAIL)), + eq(totpTwoFaProviderConfig), eq(generatedTotpTwoFaAccountConfig)); + } + + @Test + public void testVerifyAndSaveTotpTwoFaAccountConfig() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + + String secret = UriComponentsBuilder.fromUriString(generatedTotpTwoFaAccountConfig.getAuthUrl()).build() + .getQueryParams().getFirst("secret"); + String correctVerificationCode = new Totp(secret).now(); + + doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig) + .andExpect(status().isOk()); + + TwoFactorAuthAccountConfig twoFaAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); + assertThat(twoFaAccountConfig).isEqualTo(generatedTotpTwoFaAccountConfig); + } + + @Test + public void testVerifyAndSaveTotpTwoFaAccountConfig_incorrectVerificationCode() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); + + loginTenantAdmin(); + + TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); + + String incorrectVerificationCode = "100000"; + String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + incorrectVerificationCode, generatedTotpTwoFaAccountConfig) + .andExpect(status().isBadRequest())); + + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + } + + private TotpTwoFactorAuthAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig) throws Exception { + TwoFactorAuthAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP") + .andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); + assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFactorAuthAccountConfig.class); + + assertThat(((TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> { + UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build(); + assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth"); + assertThat(otpAuthUrl.getHost()).isEqualTo("totp"); + assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName()); + assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL); + assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> { + assertDoesNotThrow(() -> Base32.decode(secretKey)); + }); + }); + return (TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig; + } + + + @Test + public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception { + testVerifyAndSaveTotpTwoFaAccountConfig(); + assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), + TotpTwoFactorAuthAccountConfig.class)).isNotNull(); + + loginSysAdmin(); + + saveProvidersConfigs(); + + assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)) + .isNullOrEmpty(); + } + +// @Test +// public void testSubmitSmsTwoFaAccountConfig() throws Exception { +// String verificationMessageTemplate = "Here is your verification code: ${verificationCode}"; +// SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = configureSmsTwoFaProvider(verificationMessageTemplate); +// +// SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); +// smsTwoFaAccountConfig.setPhoneNumber("+38054159785"); +// +// String verificationCode = ""; ? +// +// verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> { +// return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber()) +// }), eq("Here is your verification code: " + verificationCode)); +// } + + + + private TotpTwoFactorAuthProviderConfig configureTotpTwoFaProvider() throws Exception { + TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + saveProvidersConfigs(totpTwoFaProviderConfig); + return totpTwoFaProviderConfig; + } + + private SmsTwoFactorAuthProviderConfig configureSmsTwoFaProvider(String verificationMessageTemplate) throws Exception { + SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate(verificationMessageTemplate); + + saveProvidersConfigs(smsTwoFaProviderConfig); + return smsTwoFaProviderConfig; + } + + private void saveProvidersConfigs(TwoFactorAuthProviderConfig... providerConfigs) throws Exception { + TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); + twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList())); + doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthSqlTest.java new file mode 100644 index 0000000000..62024fc64e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthSqlTest.java @@ -0,0 +1,23 @@ +/** + * 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.controller.sql; + +import org.thingsboard.server.controller.TwoFactorAuthTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class TwoFactorAuthSqlTest extends TwoFactorAuthTest { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 571af34086..49db7befd3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -31,4 +31,5 @@ public class CacheConstants { public static final String TOKEN_OUTDATAGE_TIME_CACHE = "tokensOutdatageTime"; public static final String OTA_PACKAGE_CACHE = "otaPackages"; public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData"; + public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes"; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java index da7537ae75..404eeb44cc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -21,6 +21,7 @@ import org.hibernate.validator.HibernateValidatorConfiguration; import org.hibernate.validator.cfg.ConstraintMapping; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import org.thingsboard.server.dao.exception.DataValidationException; import javax.validation.ConstraintViolation; import javax.validation.Validation; @@ -46,7 +47,7 @@ public class ConstraintValidator { .distinct() .collect(Collectors.toList()); if (!validationErrors.isEmpty()) { - throw new ValidationException(String.join(", ", validationErrors)); + throw new DataValidationException("Validation error: " + String.join(", ", validationErrors)); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java index d49594a955..b750d5f1c3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java @@ -675,7 +675,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setUrl(URL); firmwareInfo.setTenantId(tenantId); - thrown.expect(ValidationException.class); + thrown.expect(DataValidationException.class); thrown.expectMessage("length of title must be equal or less than 255"); otaPackageService.saveOtaPackageInfo(firmwareInfo, true);