Browse Source

Update 2FA tests

pull/6235/head
Viacheslav Klimov 4 years ago
parent
commit
05301efe2c
  1. 55
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java
  2. 693
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  3. 2
      common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java

55
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);
}
}

693
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<String> 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<String> 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<AuditLog> 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<PlatformTwoFaSettings>... 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<SmsTwoFaProviderConfig>... 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<String> 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<String> 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<AuditLog> 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<TwoFaProviderType, TwoFactorAuthController.TwoFaProviderInfo> providersInfos = readResponse(doGet("/api/auth/2fa/providers").andExpect(status().isOk()), new TypeReference<List<TwoFactorAuthController.TwoFaProviderInfo>>() {}).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<PlatformTwoFaSettings>... 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<SmsTwoFaProviderConfig>... 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();
}
}

2
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)

Loading…
Cancel
Save