Browse Source

Tests for 2FA config management; refactoring

pull/6235/head
Viacheslav Klimov 5 years ago
parent
commit
1250fa2ba0
  1. 2
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java
  2. 7
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  3. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  4. 37
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java
  5. 7
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java
  6. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java
  8. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java
  9. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java
  12. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  14. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  15. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  16. 525
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java
  17. 245
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  18. 23
      application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthConfigSqlTest.java

2
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java

@ -114,7 +114,7 @@ public class TwoFactorAuthConfigController extends BaseController {
@GetMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException {
return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId()).orElse(null);
return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId(), false).orElse(null);
}
@PostMapping("/settings")

7
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java

@ -37,14 +37,9 @@ import javax.servlet.http.HttpServletRequest;
/*
* TODO [viacheslav]:
* - Tests for 2FA
* - Swagger documentation
* */
/*
*
*
* TODO (later):
* TODO [viacheslav] (later):
* - 2FA entries should be secured against code injection by code validation
* - ability to force users to use 2FA (maybe on log in, do not give them token pair but to give temporary
* token to configure 2FA account config); also will need to make users configure 2FA during activation and password setup...

6
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java

@ -71,7 +71,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
@Override
public void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException {
TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId())
TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId(), true)
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
if (checkLimits) {
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) {
@ -102,7 +102,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
throw new ThingsboardException("User is disabled", ThingsboardErrorCode.AUTHENTICATION);
}
TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId())
TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId(), true)
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
if (checkLimits) {
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) {
@ -136,7 +136,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
private TwoFactorAuthProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) throws ThingsboardException {
return configManager.getTwoFaSettings(tenantId)
return configManager.getTwoFaSettings(tenantId, true)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType))
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
}

37
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.settings.AdminSettingsDao;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
@ -47,6 +48,7 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan
private final UserService userService;
private final AdminSettingsService adminSettingsService;
private final AdminSettingsDao adminSettingsDao;
private final AttributesService attributesService;
protected static final String TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY = "twoFaConfig";
@ -54,8 +56,8 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan
@Override
public boolean isTwoFaEnabled(User user) {
return getTwoFaAccountConfig(user.getTenantId(), user.getId()).isPresent();
public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) {
return getTwoFaAccountConfig(tenantId, userId).isPresent();
}
@Override
@ -96,21 +98,26 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan
private Optional<TwoFactorAuthProviderConfig> getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) {
return getTwoFaSettings(tenantId)
return getTwoFaSettings(tenantId, true)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType));
}
@SneakyThrows({InterruptedException.class, ExecutionException.class})
@Override
public Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId) {
public Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault) {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, TWO_FACTOR_AUTH_SETTINGS_KEY))
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TwoFactorAuthSettings.class));
} else {
return attributesService.find(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get()
.map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class))
.filter(tenantTwoFactorAuthSettings -> !tenantTwoFactorAuthSettings.isUseSystemTwoFactorAuthSettings())
.or(() -> getTwoFaSettings(TenantId.SYS_TENANT_ID));
Optional<TwoFactorAuthSettings> tenantTwoFaSettings = attributesService.find(TenantId.SYS_TENANT_ID, tenantId,
DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get()
.map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class));
if (sysadminSettingsAsDefault) {
if (tenantTwoFaSettings.isEmpty() || tenantTwoFaSettings.get().isUseSystemTwoFactorAuthSettings()) {
return getTwoFaSettings(TenantId.SYS_TENANT_ID, false);
}
}
return tenantTwoFaSettings;
}
}
@ -136,4 +143,16 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan
}
}
@SneakyThrows({InterruptedException.class, ExecutionException.class})
@Override
public void deleteTwoFaSettings(TenantId tenantId) {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.ifPresent(adminSettings -> adminSettingsDao.removeById(tenantId, adminSettings.getId().getId()));
} else {
attributesService.removeAll(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE,
Collections.singletonList(TWO_FACTOR_AUTH_SETTINGS_KEY)).get();
}
}
}

7
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.security.auth.mfa.config;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@ -25,7 +24,7 @@ import java.util.Optional;
public interface TwoFactorAuthConfigManager {
boolean isTwoFaEnabled(User user);
boolean isTwoFaEnabled(TenantId tenantId, UserId userId);
Optional<TwoFactorAuthAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId);
@ -34,8 +33,10 @@ public interface TwoFactorAuthConfigManager {
void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId);
Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId);
Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault);
void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings);
void deleteTwoFaSettings(TenantId tenantId);
}

10
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java

@ -34,17 +34,17 @@ public class TwoFactorAuthSettings {
private List<TwoFactorAuthProviderConfig> providers;
@ApiModelProperty(example = "1:60 (1 request per minute)")
@Pattern(regexp = "[^0]\\d+:[^0]\\d+", message = "Rate limit configuration is invalid")
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code send rate limit configuration is invalid")
private String verificationCodeSendRateLimit;
@ApiModelProperty(example = "3:900 (3 requests per 15 minutes)")
@Pattern(regexp = "[^0]\\d+:[^0]\\d+", message = "Rate limit configuration is invalid")
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid")
private String verificationCodeCheckRateLimit;
@ApiModelProperty(example = "10")
@Min(0)
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
private int maxVerificationFailuresBeforeUserLockout;
@ApiModelProperty(value = "in minutes", example = "60")
@Min(1)
private int totalAllowedTimeForVerification;
@Min(value = 1, message = "total amount of time allotted for verification must be greater than 0")
private Integer totalAllowedTimeForVerification;
public Optional<TwoFactorAuthProviderConfig> getProviderConfig(TwoFactorAuthProviderType providerType) {

3
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/OtpBasedTwoFactorAuthAccountConfig.java

@ -15,5 +15,8 @@
*/
package org.thingsboard.server.service.security.auth.mfa.config.account;
import lombok.Data;
@Data
public abstract class OtpBasedTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig {
}

4
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java

@ -26,8 +26,8 @@ import javax.validation.constraints.Pattern;
@Data
public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig {
@NotBlank
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "Phone number is not of E.164 format")
@NotBlank(message = "phone number cannot be blank")
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format")
private String phoneNumber;
@Override

6
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.security.auth.mfa.config.account;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
@ -24,8 +25,9 @@ import javax.validation.constraints.Pattern;
@Data
public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig {
@NotBlank
// @Pattern(regexp = "otpauth://totp/") // FIXME [viacheslav]: validate otp auth url by pattern
@ApiModelProperty(example = "otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII")
@NotBlank(message = "OTP auth URL cannot be blank")
@Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid")
private String authUrl;
@Override

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java

@ -23,6 +23,6 @@ import javax.validation.constraints.Min;
@Data
public abstract class OtpBasedTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig {
@ApiModelProperty(value = "in seconds", example = "60")
@Min(1) // TODO [viacheslav]: test
@Min(value = 1, message = "verification code lifetime is required")
private int verificationCodeLifetime;
}

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java

@ -26,7 +26,7 @@ import javax.validation.constraints.Pattern;
@Data
public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig {
@NotBlank
@NotBlank(message = "verification message template is required")
@Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code")
private String smsVerificationMessageTemplate;

6
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java

@ -57,7 +57,7 @@ public abstract class OtpBasedTwoFactorAuthProvider<C extends OtpBasedTwoFactorA
return false;
}
if (verificationCode.equals(correctVerificationCode.getValue())
&& correctVerificationCode.getAccountConfig().equals(accountConfig)) {
&& accountConfig.equals(correctVerificationCode.getAccountConfig())) {
verificationCodesCache.evict(securityUser.getId());
return true;
}
@ -65,10 +65,10 @@ public abstract class OtpBasedTwoFactorAuthProvider<C extends OtpBasedTwoFactorA
return false;
}
// TODO [viacheslav]: periodically clean up codes cache
// FIXME [viacheslav]: periodically clean up codes cache
@Data
private static class Otp {
public static class Otp {
private final long timestamp;
private final String value;
private final OtpBasedTwoFactorAuthAccountConfig accountConfig;

2
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java

@ -82,7 +82,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
String username = userPrincipal.getValue();
String password = (String) authentication.getCredentials();
securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
if (twoFactorAuthConfigManager.isTwoFaEnabled(securityUser)) {
if (twoFactorAuthConfigManager.isTwoFaEnabled(securityUser.getTenantId(), securityUser.getId())) {
return new MfaAuthenticationToken(securityUser);
} else {
systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null);

6
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java

@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -37,6 +36,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Component(value = "defaultAuthenticationSuccessHandler")
@ -54,8 +54,8 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
JwtTokenPair tokenPair = new JwtTokenPair();
if (authentication instanceof MfaAuthenticationToken) {
int preVerificationTokenLifetime = (int) TimeUnit.MINUTES.toSeconds(twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId())
.map(TwoFactorAuthSettings::getTotalAllowedTimeForVerification).orElse(30));
int preVerificationTokenLifetime = (int) TimeUnit.MINUTES.toSeconds(twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId(), true)
.flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())).orElse(30));
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
tokenPair.setRefreshToken(null);
tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN);

5
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -66,6 +66,7 @@ import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeCon
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
@ -124,6 +125,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected String username;
protected TenantId tenantId;
protected UserId tenantAdminUserId;
@SuppressWarnings("rawtypes")
private HttpMessageConverter mappingJackson2HttpMessageConverter;
@ -186,7 +188,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
tenantAdmin.setTenantId(tenantId);
tenantAdmin.setEmail(TENANT_ADMIN_EMAIL);
createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD);
tenantAdmin = createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD);
tenantAdminUserId = tenantAdmin.getId();
Customer customer = new Customer();
customer.setTitle("Customer");

525
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java

@ -0,0 +1,525 @@
/**
* 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.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cache.CacheManager;
import org.springframework.web.util.UriComponents;
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.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
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.OtpBasedTwoFactorAuthProvider;
import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFactorAuthProvider;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
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 TwoFactorAuthConfigTest extends AbstractControllerTest {
@SpyBean
private TotpTwoFactorAuthProvider totpTwoFactorAuthProvider;
@MockBean
private SmsService smsService;
@Autowired
private CacheManager cacheManager;
@Autowired
private TwoFactorAuthConfigManager twoFactorAuthConfigManager;
@Before
public void beforeEach() throws Exception {
loginSysAdmin();
}
@After
public void afterEach() {
twoFactorAuthConfigManager.deleteTwoFaSettings(TenantId.SYS_TENANT_ID);
twoFactorAuthConfigManager.deleteTwoFaSettings(tenantId);
}
@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}");
smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings();
twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig));
twoFaSettings.setVerificationCodeSendRateLimit("1:60");
twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
twoFaSettings.setTotalAllowedTimeForVerification(60);
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
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 testSaveTwoFaSettings_validationError() throws Exception {
loginTenantAdmin();
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings();
twoFaSettings.setProviders(Collections.emptyList());
twoFaSettings.setVerificationCodeSendRateLimit("ab:aba");
twoFaSettings.setVerificationCodeCheckRateLimit("0:12");
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(-1);
twoFaSettings.setTotalAllowedTimeForVerification(0);
String errorMessage = getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).contains(
"verification code send rate limit configuration is invalid",
"verification code check rate limit configuration is invalid",
"maximum number of verification failure before user lockout must be positive",
"total amount of time allotted for verification must be greater than 0"
);
twoFaSettings.setUseSystemTwoFactorAuthSettings(true);
doPost("/api/2fa/settings", twoFaSettings)
.andExpect(status().isOk());
twoFaSettings.setVerificationCodeSendRateLimit(null);
twoFaSettings.setVerificationCodeCheckRateLimit(null);
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(0);
twoFaSettings.setTotalAllowedTimeForVerification(null);
doPost("/api/2fa/settings", twoFaSettings)
.andExpect(status().isOk());
}
@Test
public void testGetTwoFaSettings_useSysadminSettingsAsDefault() throws Exception {
loginSysAdmin();
TwoFactorAuthSettings sysadminTwoFaSettings = new TwoFactorAuthSettings();
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
sysadminTwoFaSettings.setProviders(Collections.singletonList(totpTwoFaProviderConfig));
sysadminTwoFaSettings.setMaxVerificationFailuresBeforeUserLockout(25);
doPost("/api/2fa/settings", sysadminTwoFaSettings).andExpect(status().isOk());
loginTenantAdmin();
TwoFactorAuthSettings tenantTwoFaSettings = new TwoFactorAuthSettings();
tenantTwoFaSettings.setUseSystemTwoFactorAuthSettings(true);
tenantTwoFaSettings.setProviders(Collections.emptyList());
doPost("/api/2fa/settings", tenantTwoFaSettings).andExpect(status().isOk());
TwoFactorAuthSettings twoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class);
assertThat(twoFaSettings).isEqualTo(tenantTwoFaSettings);
doPost("/api/2fa/account/config/generate?providerType=TOTP")
.andExpect(status().isOk());
tenantTwoFaSettings.setUseSystemTwoFactorAuthSettings(false);
tenantTwoFaSettings.setProviders(Collections.emptyList());
tenantTwoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
doPost("/api/2fa/settings", tenantTwoFaSettings).andExpect(status().isOk());
twoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class);
assertThat(twoFaSettings).isEqualTo(tenantTwoFaSettings);
assertThat(getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=TOTP")
.andExpect(status().isBadRequest()))).containsIgnoringCase("provider is not configured");
loginSysAdmin();
sysadminTwoFaSettings.setProviders(Collections.emptyList());
doPost("/api/2fa/settings", sysadminTwoFaSettings).andExpect(status().isOk());
loginTenantAdmin();
tenantTwoFaSettings.setUseSystemTwoFactorAuthSettings(true);
tenantTwoFaSettings.setProviders(Collections.singletonList(totpTwoFaProviderConfig));
doPost("/api/2fa/settings", tenantTwoFaSettings).andExpect(status().isOk());
assertThat(getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=TOTP")
.andExpect(status().isBadRequest()))).containsIgnoringCase("provider is not configured");
tenantTwoFaSettings.setUseSystemTwoFactorAuthSettings(false);
doPost("/api/2fa/settings", tenantTwoFaSettings).andExpect(status().isOk());
doPost("/api/2fa/account/config/generate?providerType=TOTP")
.andExpect(status().isOk());
loginSysAdmin();
twoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class);
assertThat(twoFaSettings).isEqualTo(sysadminTwoFaSettings);
}
@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");
invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(60);
String errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig);
assertThat(errorResponse).containsIgnoringCase("must contain verification code");
invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate(null);
invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(0);
errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig);
assertThat(errorResponse).containsIgnoringCase("verification message template is required");
assertThat(errorResponse).containsIgnoringCase("verification code lifetime is required");
}
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("otpauth://totp/aba:aba?issuer=aba&secret=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 testSubmitTotpTwoFaAccountConfig_validationError() throws Exception {
configureTotpTwoFaProvider();
loginTenantAdmin();
TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = new TotpTwoFactorAuthAccountConfig();
totpTwoFaAccountConfig.setAuthUrl(null);
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("otp auth url cannot be blank");
totpTwoFaAccountConfig.setAuthUrl("otpauth://totp/T B: aba");
errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("otp auth url is invalid");
totpTwoFaAccountConfig.setAuthUrl("otpauth://totp/ThingsBoard%20(Tenant):tenant@thingsboard.org?issuer=ThingsBoard+%28Tenant%29&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII");
doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig)
.andExpect(status().isOk());
}
@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 testGenerateSmsTwoFaAccountConfig() throws Exception {
configureSmsTwoFaProvider("${verificationCode}");
doPost("/api/2fa/account/config/generate?providerType=SMS")
.andExpect(status().isOk());
}
@Test
public void testSubmitSmsTwoFaAccountConfig() throws Exception {
String verificationMessageTemplate = "Here is your verification code: ${verificationCode}";
configureSmsTwoFaProvider(verificationMessageTemplate);
loginTenantAdmin();
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38054159785");
doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig).andExpect(status().isOk());
String verificationCode = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE)
.get(tenantAdminUserId, OtpBasedTwoFactorAuthProvider.Otp.class).getValue();
verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> {
return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber());
}), eq("Here is your verification code: " + verificationCode));
}
@Test
public void testSubmitSmsTwoFaAccountConfig_validationError() throws Exception {
configureSmsTwoFaProvider("${verificationCode}");
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
String blankPhoneNumber = "";
smsTwoFaAccountConfig.setPhoneNumber(blankPhoneNumber);
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("phone number cannot be blank");
String nonE164PhoneNumber = "8754868";
smsTwoFaAccountConfig.setPhoneNumber(nonE164PhoneNumber);
errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("phone number is not of E.164 format");
}
@Test
public void testVerifyAndSaveSmsTwoFaAccountConfig() throws Exception {
configureSmsTwoFaProvider("${verificationCode}");
loginTenantAdmin();
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38051889445");
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig).andExpect(status().isOk());
verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> {
return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber());
}), verificationCodeCaptor.capture());
String correctVerificationCode = verificationCodeCaptor.getValue();
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, smsTwoFaAccountConfig)
.andExpect(status().isOk());
TwoFactorAuthAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class);
assertThat(accountConfig).isEqualTo(smsTwoFaAccountConfig);
}
@Test
public void testVerifyAndSaveSmsTwoFaAccountConfig_incorrectVerificationCode() throws Exception {
configureSmsTwoFaProvider("${verificationCode}");
loginTenantAdmin();
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38051889445");
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=100000", smsTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
}
@Test
public void testVerifyAndSaveSmsTwoFaAccountConfig_differentAccountConfigs() throws Exception {
configureSmsTwoFaProvider("${verificationCode}");
loginTenantAdmin();
SmsTwoFactorAuthAccountConfig initialSmsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
initialSmsTwoFaAccountConfig.setPhoneNumber("+38051889445");
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
doPost("/api/2fa/account/config/submit", initialSmsTwoFaAccountConfig).andExpect(status().isOk());
verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> {
return phoneNumbers[0].equals(initialSmsTwoFaAccountConfig.getPhoneNumber());
}), verificationCodeCaptor.capture());
String correctVerificationCode = verificationCodeCaptor.getValue();
SmsTwoFactorAuthAccountConfig anotherSmsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
anotherSmsTwoFaAccountConfig.setPhoneNumber("+38111111111");
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, anotherSmsTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, initialSmsTwoFaAccountConfig)
.andExpect(status().isOk());
TwoFactorAuthAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class);
assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig);
}
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);
smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
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());
}
@Test
public void testIsTwoFaEnabled() throws ThingsboardException {
SmsTwoFactorAuthAccountConfig accountConfig = new SmsTwoFactorAuthAccountConfig();
accountConfig.setPhoneNumber("+380505050");
twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig);
assertThat(twoFactorAuthConfigManager.isTwoFaEnabled(tenantId, tenantAdminUserId)).isTrue();
}
@Test
public void testDeleteTwoFaAccountConfig() throws Exception {
SmsTwoFactorAuthAccountConfig accountConfig = new SmsTwoFactorAuthAccountConfig();
accountConfig.setPhoneNumber("+380505050");
twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig);
loginTenantAdmin();
TwoFactorAuthAccountConfig savedAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class);
assertThat(savedAccountConfig).isEqualTo(accountConfig);
doDelete("/api/2fa/account/config").andExpect(status().isOk());
assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class))
.isNullOrEmpty();
}
}

245
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java

@ -15,242 +15,15 @@
*/
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.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.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
// TODO [viacheslav]: test validation for all account configs, provider configs and two factor auth settings
// TODO [viacheslav]: test authentication details, log login action, last login ts, rate limiting, user blocking, etc
/*
* TODO [viacheslav]
* check validation of the verification code
* test rate limits
* test code expiration
* test pre-verification token lifetime
* test user blocking
* test log login action, lastLoginTs, and authentication details
* */
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());
}
}

23
application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthConfigSqlTest.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.dao.service.DaoSqlTest;
import org.thingsboard.server.controller.TwoFactorAuthConfigTest;
@DaoSqlTest
public class TwoFactorAuthConfigSqlTest extends TwoFactorAuthConfigTest {
}
Loading…
Cancel
Save