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 954d313480..3438ed19f8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -30,6 +30,7 @@ import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; @@ -82,15 +83,15 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { @Test - public void testSaveTwoFaSettings() throws Exception { + public void testSavePlatformTwoFaSettingsForDifferentAuthorities() throws Exception { loginSysAdmin(); - testSaveTestTwoFaSettings(); + testSavePlatformTwoFaSettings(); loginTenantAdmin(); - testSaveTestTwoFaSettings(); + testSavePlatformTwoFaSettings(); } - private void testSaveTestTwoFaSettings() throws Exception { + private void testSavePlatformTwoFaSettings() throws Exception { TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); totpTwoFaProviderConfig.setIssuerName("tb"); SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig(); @@ -113,7 +114,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { } @Test - public void testSaveTwoFaSettings_validationError() throws Exception { + public void testSavePlatformTwoFaSettings_validationError() throws Exception { loginTenantAdmin(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); @@ -147,7 +148,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { } @Test - public void testGetTwoFaSettings_useSysadminSettingsAsDefault() throws Exception { + public void testGetPlatformTwoFaSettings_useSysadminSettingsAsDefault() throws Exception { loginSysAdmin(); PlatformTwoFaSettings sysadminTwoFaSettings = new PlatformTwoFaSettings(); TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); @@ -204,7 +205,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { TotpTwoFaProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); invalidTotpTwoFaProviderConfig.setIssuerName(" "); - String errorResponse = saveTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig); + String errorResponse = savePlatformTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig); assertThat(errorResponse).containsIgnoringCase("issuer name must not be blank"); } @@ -214,17 +215,17 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code"); invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(60); - String errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); + String errorResponse = savePlatformTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); assertThat(errorResponse).containsIgnoringCase("must contain verification code"); invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate(null); invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(0); - errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); + errorResponse = savePlatformTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); assertThat(errorResponse).containsIgnoringCase("verification message template is required"); assertThat(errorResponse).containsIgnoringCase("verification code lifetime is required"); } - private String saveTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { + private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); @@ -232,6 +233,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { .andExpect(status().isBadRequest())); } + @Test public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception { configureSmsTwoFaProvider("${verificationCode}"); @@ -255,7 +257,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { loginTenantAdmin(); - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)).isNullOrEmpty(); + assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), String.class)).isNullOrEmpty(); generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); } @@ -309,7 +311,10 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig) .andExpect(status().isOk()); - TwoFaAccountConfig twoFaAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class); + AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); + assertThat(accountTwoFaSettings.getConfigs()).size().isOne(); + + TwoFaAccountConfig twoFaAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.TOTP); assertThat(twoFaAccountConfig).isEqualTo(generatedTotpTwoFaAccountConfig); } @@ -349,15 +354,16 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { @Test public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception { testVerifyAndSaveTotpTwoFaAccountConfig(); - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), - TotpTwoFaAccountConfig.class)).isNotNull(); + assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), + AccountTwoFaSettings.class).getConfigs()).isNotEmpty(); loginSysAdmin(); - saveProvidersConfigs(); - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)) - .isNullOrEmpty(); + loginTenantAdmin(); + + assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs()) + .isEmpty(); } @Test @@ -427,7 +433,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, smsTwoFaAccountConfig) .andExpect(status().isOk()); - TwoFaAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class); + AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); + TwoFaAccountConfig accountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS); assertThat(accountConfig).isEqualTo(smsTwoFaAccountConfig); } @@ -470,7 +477,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, initialSmsTwoFaAccountConfig) .andExpect(status().isOk()); - TwoFaAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class); + AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); + TwoFaAccountConfig accountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS); assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig); } @@ -518,13 +526,14 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); - TwoFaAccountConfig savedAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class); + AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); + TwoFaAccountConfig savedAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS); assertThat(savedAccountConfig).isEqualTo(accountConfig); - doDelete("/api/2fa/account/config").andExpect(status().isOk()); + doDelete("/api/2fa/account/config?providerType=SMS").andExpect(status().isOk()); - assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)) - .isNullOrEmpty(); + assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs()) + .doesNotContainKey(TwoFaProviderType.SMS); } } 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 17910c4f16..1ad340d54b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; @@ -36,23 +37,26 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.audit.AuditLogService; -import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; -import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; +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.common.data.security.model.mfa.account.TotpTwoFaAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.EmailTwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; +import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.model.JwtTokenPair; import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -68,319 +72,372 @@ import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public abstract class TwoFactorAuthTest extends AbstractControllerTest { -// -// @Autowired -// private TwoFaConfigManager twoFaConfigManager; -// @Autowired -// private TwoFactorAuthService twoFactorAuthService; -// @MockBean -// private SmsService smsService; -// @Autowired -// private AuditLogService auditLogService; -// @Autowired -// private UserService userService; -// -// private User user; -// private String username; -// private String password; -// -// @Before -// public void beforeEach() throws Exception { -// username = "mfa@tb.io"; -// password = "psswrd"; -// -// user = new User(); -// user.setAuthority(Authority.TENANT_ADMIN); -// user.setEmail(username); -// user.setTenantId(tenantId); -// -// loginSysAdmin(); -// user = createUser(user, password); -// } -// -// @After -// public void afterEach() { -// twoFaConfigManager.deletePlatformTwoFaSettings(tenantId); -// twoFaConfigManager.deletePlatformTwoFaSettings(TenantId.SYS_TENANT_ID); -// } -// -// @Test -// public void testTwoFa_totp() throws Exception { -// TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); -// -// logInWithPreVerificationToken(); -// -// doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isOk()); -// -// String correctVerificationCode = getCorrectTotp(totpTwoFaAccountConfig); -// -// JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) -// .andExpect(status().isOk()), JsonNode.class); -// validateAndSetJwtToken(tokenPair, username); -// -// User currentUser = readResponse(doGet("/api/auth/user") -// .andExpect(status().isOk()), User.class); -// assertThat(currentUser.getId()).isEqualTo(user.getId()); -// } -// -// @Test -// public void testTwoFa_sms() throws Exception { -// configureSmsTwoFa(); -// -// logInWithPreVerificationToken(); -// -// doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isOk()); -// -// ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); -// verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); -// String correctVerificationCode = verificationCodeCaptor.getValue(); -// -// JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) -// .andExpect(status().isOk()), JsonNode.class); -// validateAndSetJwtToken(tokenPair, username); -// -// User currentUser = readResponse(doGet("/api/auth/user") -// .andExpect(status().isOk()), User.class); -// assertThat(currentUser.getId()).isEqualTo(user.getId()); -// } -// -// @Test -// public void testTwoFaPreVerificationTokenLifetime() throws Exception { -// configureTotpTwoFa(twoFaSettings -> { -// twoFaSettings.setTotalAllowedTimeForVerification(5); -// }); -// -// logInWithPreVerificationToken(); -// -// await("expiration of the pre-verification token") -// .atLeast(Duration.ofSeconds(3).plusMillis(500)) -// .atMost(Duration.ofSeconds(6)) -// .untilAsserted(() -> { -// doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isUnauthorized()); -// }); -// } -// -// @Test -// public void testCheckVerificationCode_userBlocked() throws Exception { -// configureTotpTwoFa(twoFaSettings -> { -// twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); -// }); -// -// logInWithPreVerificationToken(); -// -// Stream.generate(() -> RandomStringUtils.randomNumeric(6)) -// .limit(9) -// .forEach(incorrectVerificationCode -> { -// try { -// String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + incorrectVerificationCode) -// .andExpect(status().isBadRequest())); -// assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); -// } catch (Exception e) { -// fail(); -// } -// }); -// -// String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + RandomStringUtils.randomNumeric(6)) -// .andExpect(status().isUnauthorized())); -// assertThat(errorMessage).containsIgnoringCase("account was locked due to exceeded 2fa verification attempts"); -// -// errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + RandomStringUtils.randomNumeric(6)) -// .andExpect(status().isUnauthorized())); -// assertThat(errorMessage).containsIgnoringCase("user is disabled"); -// } -// -// @Test -// public void testSendVerificationCode_rateLimit() throws Exception { -// configureTotpTwoFa(twoFaSettings -> { -// twoFaSettings.setVerificationCodeSendRateLimit("3:10"); -// }); -// -// logInWithPreVerificationToken(); -// -// for (int i = 0; i < 3; i++) { -// doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isOk()); -// } -// -// String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isTooManyRequests())); -// assertThat(rateLimitExceededError).containsIgnoringCase("too many verification code sending requests"); -// -// await("verification code sending rate limit resetting") -// .atLeast(Duration.ofSeconds(8)) -// .atMost(Duration.ofSeconds(12)) -// .untilAsserted(() -> { -// doPost("/api/auth/2fa/verification/send") -// .andExpect(status().isOk()); -// }); -// } -// -// @Test -// public void testCheckVerificationCode_rateLimit() throws Exception { -// TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(twoFaSettings -> { -// twoFaSettings.setVerificationCodeCheckRateLimit("3:10"); -// }); -// -// logInWithPreVerificationToken(); -// -// for (int i = 0; i < 3; i++) { -// String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") -// .andExpect(status().isBadRequest())); -// assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); -// } -// -// String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") -// .andExpect(status().isTooManyRequests())); -// assertThat(rateLimitExceededError).containsIgnoringCase("too many verification code checking requests"); -// -// await("verification code checking rate limit resetting") -// .atLeast(Duration.ofSeconds(8)) -// .atMost(Duration.ofSeconds(12)) -// .untilAsserted(() -> { -// String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") -// .andExpect(status().isBadRequest())); -// assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); -// }); -// -// doPost("/api/auth/2fa/verification/check?verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) -// .andExpect(status().isOk()); -// } -// -// @Test -// public void testCheckVerificationCode_invalidVerificationCode() throws Exception { -// configureTotpTwoFa(); -// logInWithPreVerificationToken(); -// -// for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) { -// String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + invalidVerificationCode) -// .andExpect(status().isBadRequest())); -// assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); -// } -// } -// -// @Test -// public void testCheckVerificationCode_codeExpiration() throws Exception { -// configureSmsTwoFa(smsTwoFaProviderConfig -> { -// smsTwoFaProviderConfig.setVerificationCodeLifetime(10); -// }); -// -// logInWithPreVerificationToken(); -// -// ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); -// doPost("/api/auth/2fa/verification/send").andExpect(status().isOk()); -// verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); -// -// String correctVerificationCode = verificationCodeCaptor.getValue(); -// -// await("verification code expiration") -// .pollDelay(10, TimeUnit.SECONDS) -// .atLeast(10, TimeUnit.SECONDS) -// .atMost(12, TimeUnit.SECONDS) -// .untilAsserted(() -> { -// String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) -// .andExpect(status().isBadRequest())); -// assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); -// }); -// } -// -// @Test -// public void testTwoFa_logLoginAction() throws Exception { -// TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); -// -// logInWithPreVerificationToken(); -// await("async audit log saving").during(1, TimeUnit.SECONDS); -// assertThat(getLogInAuditLogs()).isEmpty(); -// assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() -// .get("lastLoginTs")).isNull(); -// -// doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") -// .andExpect(status().isBadRequest()); -// -// await("async audit log saving").atMost(1, TimeUnit.SECONDS) -// .until(() -> getLogInAuditLogs().size() == 1); -// assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> { -// assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE); -// assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect"); -// assertThat(failedLogInAuditLog.getUserName()).isEqualTo(username); -// }); -// -// doPost("/api/auth/2fa/verification/check?verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) -// .andExpect(status().isOk()); -// await("async audit log saving").atMost(1, TimeUnit.SECONDS) -// .until(() -> getLogInAuditLogs().size() == 2); -// assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> { -// assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS); -// assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username); -// }); -// assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() -// .get("lastLoginTs").asLong()) -// .isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3)); -// } -// -// private List getLogInAuditLogs() { -// return auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, user.getId(), List.of(ActionType.LOGIN), -// new TimePageLink(new PageLink(10, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC)), 0L, System.currentTimeMillis())).getData(); -// } -// -// @Test -// public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException { -// configureTotpTwoFa(); -// twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId(), ); -// -// assertDoesNotThrow(() -> { -// login(username, password); -// }); -// } -// -// private void logInWithPreVerificationToken() throws Exception { -// LoginRequest loginRequest = new LoginRequest(username, password); -// -// JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class); -// assertThat(response.getToken()).isNotNull(); -// assertThat(response.getRefreshToken()).isNull(); -// assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); -// -// this.token = response.getToken(); -// } -// -// private TotpTwoFaAccountConfig configureTotpTwoFa(Consumer... customizer) throws ThingsboardException { -// TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); -// totpTwoFaProviderConfig.setIssuerName("tb"); -// -// PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); -// twoFaSettings.setUseSystemTwoFactorAuthSettings(false); -// twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); -// Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings)); -// twoFaConfigManager.savePlatformTwoFaSettings(tenantId, twoFaSettings); -// -// TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFaProviderType.TOTP); -// twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig); -// return totpTwoFaAccountConfig; -// } -// -// private SmsTwoFaAccountConfig configureSmsTwoFa(Consumer... customizer) throws ThingsboardException { -// SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig(); -// smsTwoFaProviderConfig.setVerificationCodeLifetime(60); -// smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); -// Arrays.stream(customizer).forEach(c -> c.accept(smsTwoFaProviderConfig)); -// -// PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); -// twoFaSettings.setUseSystemTwoFactorAuthSettings(false); -// twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList())); -// twoFaConfigManager.savePlatformTwoFaSettings(tenantId, twoFaSettings); -// -// SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); -// smsTwoFaAccountConfig.setPhoneNumber("+38050505050"); -// twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig); -// return smsTwoFaAccountConfig; -// } -// -// private String getCorrectTotp(TotpTwoFaAccountConfig totpTwoFaAccountConfig) { -// String secret = StringUtils.substringAfterLast(totpTwoFaAccountConfig.getAuthUrl(), "secret="); -// return new Totp(secret).now(); -// } + + @Autowired + private TwoFaConfigManager twoFaConfigManager; + @Autowired + private TwoFactorAuthService twoFactorAuthService; + @MockBean + private SmsService smsService; + @Autowired + private AuditLogService auditLogService; + @Autowired + private UserService userService; + + private User user; + private String username; + private String password; + + @Before + public void beforeEach() throws Exception { + username = "mfa@tb.io"; + password = "psswrd"; + + user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setEmail(username); + user.setTenantId(tenantId); + + loginSysAdmin(); + user = createUser(user, password); + } + + @After + public void afterEach() { + twoFaConfigManager.deletePlatformTwoFaSettings(tenantId); + twoFaConfigManager.deletePlatformTwoFaSettings(TenantId.SYS_TENANT_ID); + } + + @Test + public void testTwoFa_totp() throws Exception { + TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); + + logInWithPreVerificationToken(username, password); + + doPost("/api/auth/2fa/verification/send?providerType=TOTP") + .andExpect(status().isOk()); + + String correctVerificationCode = getCorrectTotp(totpTwoFaAccountConfig); + + JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + correctVerificationCode) + .andExpect(status().isOk()), JsonNode.class); + validateAndSetJwtToken(tokenPair, username); + + User currentUser = readResponse(doGet("/api/auth/user") + .andExpect(status().isOk()), User.class); + assertThat(currentUser.getId()).isEqualTo(user.getId()); + } + + @Test + public void testTwoFa_sms() throws Exception { + configureSmsTwoFa(); + + logInWithPreVerificationToken(username, password); + + doPost("/api/auth/2fa/verification/send?providerType=SMS") + .andExpect(status().isOk()); + + ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); + verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); + String correctVerificationCode = verificationCodeCaptor.getValue(); + + JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?providerType=SMS&verificationCode=" + correctVerificationCode) + .andExpect(status().isOk()), JsonNode.class); + validateAndSetJwtToken(tokenPair, username); + + User currentUser = readResponse(doGet("/api/auth/user") + .andExpect(status().isOk()), User.class); + assertThat(currentUser.getId()).isEqualTo(user.getId()); + } + + @Test + public void testTwoFaPreVerificationTokenLifetime() throws Exception { + configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setTotalAllowedTimeForVerification(5); + }); + + logInWithPreVerificationToken(username, password); + + await("expiration of the pre-verification token") + .atLeast(Duration.ofSeconds(3).plusMillis(500)) + .atMost(Duration.ofSeconds(6)) + .untilAsserted(() -> { + doPost("/api/auth/2fa/verification/send?providerType=TOTP") + .andExpect(status().isUnauthorized()); + }); + } + + @Test + public void testCheckVerificationCode_userBlocked() throws Exception { + configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); + }); + + logInWithPreVerificationToken(username, password); + + Stream.generate(() -> RandomStringUtils.randomNumeric(6)) + .limit(9) + .forEach(incorrectVerificationCode -> { + try { + String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + incorrectVerificationCode) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + } catch (Exception e) { + fail(); + } + }); + + String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + RandomStringUtils.randomNumeric(6)) + .andExpect(status().isUnauthorized())); + assertThat(errorMessage).containsIgnoringCase("account was locked due to exceeded 2fa verification attempts"); + + errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + RandomStringUtils.randomNumeric(6)) + .andExpect(status().isUnauthorized())); + assertThat(errorMessage).containsIgnoringCase("user is disabled"); + } + + @Test + public void testSendVerificationCode_rateLimit() throws Exception { + configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setVerificationCodeSendRateLimit("3:10"); + }); + + logInWithPreVerificationToken(username, password); + + for (int i = 0; i < 3; i++) { + doPost("/api/auth/2fa/verification/send?providerType=TOTP") + .andExpect(status().isOk()); + } + + String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/send?providerType=TOTP") + .andExpect(status().isTooManyRequests())); + assertThat(rateLimitExceededError).containsIgnoringCase("too many requests"); + + await("verification code sending rate limit resetting") + .atLeast(Duration.ofSeconds(8)) + .atMost(Duration.ofSeconds(12)) + .untilAsserted(() -> { + doPost("/api/auth/2fa/verification/send?providerType=TOTP") + .andExpect(status().isOk()); + }); + } + + @Test + public void testCheckVerificationCode_rateLimit() throws Exception { + TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(twoFaSettings -> { + twoFaSettings.setVerificationCodeCheckRateLimit("3:10"); + }); + + logInWithPreVerificationToken(username, password); + + for (int i = 0; i < 3; i++) { + String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") + .andExpect(status().isBadRequest())); + assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); + } + + String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") + .andExpect(status().isTooManyRequests())); + assertThat(rateLimitExceededError).containsIgnoringCase("too many requests"); + + await("verification code checking rate limit resetting") + .atLeast(Duration.ofSeconds(8)) + .atMost(Duration.ofSeconds(12)) + .untilAsserted(() -> { + String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") + .andExpect(status().isBadRequest())); + assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); + }); + + doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) + .andExpect(status().isOk()); + } + + @Test + public void testCheckVerificationCode_invalidVerificationCode() throws Exception { + configureTotpTwoFa(); + logInWithPreVerificationToken(username, password); + + for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) { + String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + invalidVerificationCode) + .andExpect(status().isBadRequest())); + assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); + } + } + + @Test + public void testCheckVerificationCode_codeExpiration() throws Exception { + configureSmsTwoFa(smsTwoFaProviderConfig -> { + smsTwoFaProviderConfig.setVerificationCodeLifetime(10); + }); + + logInWithPreVerificationToken(username, password); + + ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); + doPost("/api/auth/2fa/verification/send?providerType=SMS").andExpect(status().isOk()); + verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); + + String correctVerificationCode = verificationCodeCaptor.getValue(); + + await("verification code expiration") + .pollDelay(10, TimeUnit.SECONDS) + .atLeast(10, TimeUnit.SECONDS) + .atMost(12, TimeUnit.SECONDS) + .untilAsserted(() -> { + String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=SMS&verificationCode=" + correctVerificationCode) + .andExpect(status().isBadRequest())); + assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); + }); + } + + @Test + public void testTwoFa_logLoginAction() throws Exception { + TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); + + logInWithPreVerificationToken(username, password); + await("async audit log saving").during(1, TimeUnit.SECONDS); + assertThat(getLogInAuditLogs()).isEmpty(); + assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() + .get("lastLoginTs")).isNull(); + + doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") + .andExpect(status().isBadRequest()); + + await("async audit log saving").atMost(1, TimeUnit.SECONDS) + .until(() -> getLogInAuditLogs().size() == 1); + assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> { + assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE); + assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect"); + assertThat(failedLogInAuditLog.getUserName()).isEqualTo(username); + }); + + doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) + .andExpect(status().isOk()); + await("async audit log saving").atMost(1, TimeUnit.SECONDS) + .until(() -> getLogInAuditLogs().size() == 2); + assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> { + assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS); + assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username); + }); + assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() + .get("lastLoginTs").asLong()) + .isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3)); + } + + private List getLogInAuditLogs() { + return auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, user.getId(), List.of(ActionType.LOGIN), + new TimePageLink(new PageLink(10, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC)), 0L, System.currentTimeMillis())).getData(); + } + + @Test + public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException { + configureTotpTwoFa(); + twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId(), TwoFaProviderType.TOTP); + + assertDoesNotThrow(() -> { + login(username, password); + }); + } + + @Test + public void testTwoFa_multipleProviders() throws Exception { + PlatformTwoFaSettings platformTwoFaSettings = new PlatformTwoFaSettings(); + platformTwoFaSettings.setUseSystemTwoFactorAuthSettings(true); + + TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("TB"); + + SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig(); + smsTwoFaProviderConfig.setVerificationCodeLifetime(60); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); + + EmailTwoFaProviderConfig emailTwoFaProviderConfig = new EmailTwoFaProviderConfig(); + emailTwoFaProviderConfig.setVerificationCodeLifetime(60); + + platformTwoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig, emailTwoFaProviderConfig)); + twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, platformTwoFaSettings); + + User twoFaUser = new User(); + twoFaUser.setAuthority(Authority.TENANT_ADMIN); + twoFaUser.setTenantId(tenantId); + twoFaUser.setEmail("2fa@thingsboard.org"); + twoFaUser = createUserAndLogin(twoFaUser, "12345678"); + + TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(twoFaUser, TwoFaProviderType.TOTP); + totpTwoFaAccountConfig.setUseByDefault(true); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), totpTwoFaAccountConfig); + + SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); + smsTwoFaAccountConfig.setPhoneNumber("+38012312322"); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), smsTwoFaAccountConfig); + + EmailTwoFaAccountConfig emailTwoFaAccountConfig = new EmailTwoFaAccountConfig(); + emailTwoFaAccountConfig.setEmail(twoFaUser.getEmail()); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), emailTwoFaAccountConfig); + + logInWithPreVerificationToken(twoFaUser.getEmail(), "12345678"); + + Map providersInfos = readResponse(doGet("/api/auth/2fa/providers").andExpect(status().isOk()), new TypeReference>() {}).stream() + .collect(Collectors.toMap(TwoFactorAuthController.TwoFaProviderInfo::getType, v -> v)); + + assertThat(providersInfos).size().isEqualTo(3); + + assertThat(providersInfos).containsKey(TwoFaProviderType.TOTP); + assertThat(providersInfos.get(TwoFaProviderType.TOTP).isDefault()).isTrue(); + + assertThat(providersInfos).containsKey(TwoFaProviderType.SMS); + assertThat(providersInfos.get(TwoFaProviderType.SMS).isDefault()).isFalse(); + + assertThat(providersInfos).containsKey(TwoFaProviderType.EMAIL); + assertThat(providersInfos.get(TwoFaProviderType.EMAIL).isDefault()).isFalse(); + } + + private void logInWithPreVerificationToken(String username, String password) throws Exception { + LoginRequest loginRequest = new LoginRequest(username, password); + + JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class); + assertThat(response.getToken()).isNotNull(); + assertThat(response.getRefreshToken()).isNull(); + assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); + + this.token = response.getToken(); + } + + private TotpTwoFaAccountConfig configureTotpTwoFa(Consumer... customizer) throws ThingsboardException { + TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setUseSystemTwoFactorAuthSettings(false); + twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); + Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings)); + twoFaConfigManager.savePlatformTwoFaSettings(tenantId, twoFaSettings); + + TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFaProviderType.TOTP); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig); + return totpTwoFaAccountConfig; + } + + private SmsTwoFaAccountConfig configureSmsTwoFa(Consumer... customizer) throws ThingsboardException { + SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig(); + smsTwoFaProviderConfig.setVerificationCodeLifetime(60); + smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); + Arrays.stream(customizer).forEach(c -> c.accept(smsTwoFaProviderConfig)); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setUseSystemTwoFactorAuthSettings(false); + twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList())); + twoFaConfigManager.savePlatformTwoFaSettings(tenantId, twoFaSettings); + + SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); + smsTwoFaAccountConfig.setPhoneNumber("+38050505050"); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig); + return smsTwoFaAccountConfig; + } + + private String getCorrectTotp(TotpTwoFaAccountConfig totpTwoFaAccountConfig) { + String secret = StringUtils.substringAfterLast(totpTwoFaAccountConfig.getAuthUrl(), "secret="); + return new Totp(secret).now(); + } } diff --git a/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java b/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java index 3878d64ec9..b0bbfa3dc6 100644 --- a/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java +++ b/common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java @@ -67,7 +67,7 @@ public class RateLimitsTest { assertThat(rateLimits.tryConsume()).as("new token is available").isFalse(); int expectedRefillTime = period * 1000; - int gap = 100; + int gap = 300; await("tokens refill for rate limit " + rateLimitConfig) .atLeast(expectedRefillTime - gap, TimeUnit.MILLISECONDS)