Browse Source

Multiple used 2FA providers for account

pull/6235/head
Viacheslav Klimov 4 years ago
parent
commit
bcc736991e
  1. 2
      application/src/main/data/upgrade/3.3.4/schema_update.sql
  2. 107
      application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java
  3. 44
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  4. 120
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  5. 21
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java
  6. 89
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
  7. 45
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java
  8. 43
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java
  9. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java
  10. 12
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java
  11. 20
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java
  12. 22
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java
  13. 10
      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. 4
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  16. 4
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  17. 141
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java
  18. 644
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  19. 4
      common/data/src/main/java/org/thingsboard/server/common/data/security/UserAuthSettings.java
  20. 10
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java
  21. 26
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java
  22. 4
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java
  23. 8
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java
  24. 13
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java
  25. 14
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java
  26. 2
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java
  27. 8
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java
  28. 6
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java
  29. 8
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java
  30. 2
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java
  31. 6
      common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java
  32. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  33. 15
      dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java
  34. 2
      dao/src/main/resources/sql/schema-entities.sql

2
application/src/main/data/upgrade/3.3.4/schema_update.sql

@ -18,5 +18,5 @@ CREATE TABLE IF NOT EXISTS user_auth_settings (
id uuid NOT NULL CONSTRAINT user_auth_settings_pkey PRIMARY KEY,
created_time bigint NOT NULL,
user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id),
mfa_account_config varchar
two_fa_settings varchar
);

107
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java → application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java

@ -21,11 +21,13 @@ import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@ -33,20 +35,20 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ -57,9 +59,9 @@ import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
@RequestMapping("/api/2fa")
@TbCoreComponent
@RequiredArgsConstructor
public class TwoFactorAuthConfigController extends BaseController {
public class TwoFaConfigController extends BaseController {
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager;
private final TwoFaConfigManager twoFaConfigManager;
private final TwoFactorAuthService twoFactorAuthService;
@ -71,27 +73,17 @@ public class TwoFactorAuthConfigController extends BaseController {
"```\n{\n" +
" \"providerType\": \"TOTP\",\n" +
" \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" +
"}\n```" + NEW_LINE +
"}\n```" + NEW_LINE +
"Response example for SMS 2FA: " + NEW_LINE +
"```\n{\n" +
" \"providerType\": \"SMS\",\n" +
" \"phoneNumber\": \"+380505005050\"\n" +
"}\n```")
@GetMapping("/account/config")
@GetMapping("/account/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public TwoFactorAuthAccountConfig getTwoFaAccountConfig() throws ThingsboardException {
public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException {
SecurityUser user = getCurrentUser();
return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null);
}
@GetMapping("/providers")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public List<TwoFactorAuthProviderType> getAvailableTwoFaProviders() throws ThingsboardException {
return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId(), true)
.map(TwoFactorAuthSettings::getProviders).orElse(Collections.emptyList()).stream()
.map(TwoFactorAuthProviderConfig::getProviderType)
.collect(Collectors.toList());
return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()).orElse(null);
}
@ -116,19 +108,19 @@ public class TwoFactorAuthConfigController extends BaseController {
"}\n```")
@PostMapping("/account/config/generate")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public TwoFactorAuthAccountConfig generateTwoFaAccountConfig(@ApiParam(value = "2FA provider type to generate new account config for", defaultValue = "TOTP", required = true)
@RequestParam TwoFactorAuthProviderType providerType) throws Exception {
public TwoFaAccountConfig generateTwoFaAccountConfig(@ApiParam(value = "2FA provider type to generate new account config for", defaultValue = "TOTP", required = true)
@RequestParam TwoFaProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser();
return twoFactorAuthService.generateNewAccountConfig(user, providerType);
}
/* TMP */
@PostMapping("/account/config/generate/qr")
@PostMapping("/account/config/tmp/generate/qr")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void generateTwoFaAccountConfigWithQr(@RequestParam TwoFactorAuthProviderType providerType, HttpServletResponse response) throws Exception {
TwoFactorAuthAccountConfig config = generateTwoFaAccountConfig(providerType);
if (providerType == TwoFactorAuthProviderType.TOTP) {
BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFactorAuthAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200);
public void generateTwoFaAccountConfigWithQr(@RequestParam TwoFaProviderType providerType, HttpServletResponse response) throws Exception {
TwoFaAccountConfig config = generateTwoFaAccountConfig(providerType);
if (providerType == TwoFaProviderType.TOTP) {
BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFaAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200);
try (ServletOutputStream outputStream = response.getOutputStream()) {
MatrixToImageWriter.writeToStream(qr, "PNG", outputStream);
}
@ -148,7 +140,7 @@ public class TwoFactorAuthConfigController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void submitTwoFaAccountConfig(@ApiParam(value = "2FA account config value. For TOTP 2FA config, authUrl value must not be blank and must match specific pattern. " +
"For SMS 2FA, phoneNumber property must not be blank and must be of E.164 phone number format.", required = true)
@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception {
@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception {
SecurityUser user = getCurrentUser();
twoFactorAuthService.prepareVerificationCode(user, accountConfig, false);
}
@ -160,36 +152,57 @@ public class TwoFactorAuthConfigController extends BaseController {
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PostMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void verifyAndSaveTwoFaAccountConfig(@ApiParam(value = "2FA account config to save. Validation rules are the same as in submitTwoFaAccountConfig API method", required = true)
@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig,
@ApiParam(value = "6-digit code from an authenticator app in case of TOTP 2FA, or the one sent via an SMS message in case of SMS 2FA", required = true)
@RequestParam String verificationCode) throws Exception {
public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@ApiParam(value = "2FA account config to save. Validation rules are the same as in submitTwoFaAccountConfig API method", required = true)
@Valid @RequestBody TwoFaAccountConfig accountConfig,
@ApiParam(value = "6-digit code from an authenticator app in case of TOTP 2FA, or the one sent via an SMS message in case of SMS 2FA", required = true)
@RequestParam String verificationCode) throws Exception {
SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false);
if (verificationSuccess) {
twoFactorAuthConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
} else {
throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.INVALID_ARGUMENTS);
}
}
@PutMapping("/account/config")
public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType,
@RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException {
SecurityUser user = getCurrentUser();
TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType)
.orElseThrow(() -> new IllegalArgumentException("No 2FA config for provider " + providerType));
accountConfig.setUseByDefault(updateRequest.isUseByDefault());
return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
}
@ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)",
notes = "Delete user's 2FA config. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@DeleteMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void deleteTwoFaAccountConfig() throws ThingsboardException {
public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException {
SecurityUser user = getCurrentUser();
twoFactorAuthConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId());
return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType);
}
@GetMapping("/providers")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException {
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true)
.map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream()
.map(TwoFaProviderConfig::getProviderType)
.collect(Collectors.toList());
}
@ApiOperation(value = "Get 2FA settings (getTwoFaSettings)",
@ApiOperation(value = "Get 2FA settings (getTwoFaSettings)", // FIXME [viacheslav]
notes = "Get settings for 2FA. If 2FA is not configured, then an empty response will be returned." +
ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public TwoFactorAuthSettings getTwoFaSettings() throws ThingsboardException {
return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId(), false).orElse(null);
public PlatformTwoFaSettings getPlatformTwoFaSettings() throws ThingsboardException {
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), false).orElse(null);
}
@ApiOperation(value = "Save 2FA settings (saveTwoFaSettings)",
@ -198,9 +211,15 @@ public class TwoFactorAuthConfigController extends BaseController {
ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void saveTwoFaSettings(@ApiParam(value = "Settings value", required = true)
@RequestBody TwoFactorAuthSettings twoFaSettings) throws ThingsboardException {
twoFactorAuthConfigManager.saveTwoFaSettings(getTenantId(), twoFaSettings);
public void savePlatformTwoFaSettings(@ApiParam(value = "Settings value", required = true)
@RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException {
twoFaConfigManager.savePlatformTwoFaSettings(getTenantId(), twoFaSettings);
}
@Data
public static class TwoFaAccountConfigUpdateRequest {
private boolean useByDefault;
}
}

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

@ -17,6 +17,9 @@ package org.thingsboard.server.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
@ -30,9 +33,8 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -41,6 +43,10 @@ import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
@RestController
@ -50,7 +56,7 @@ import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
public class TwoFactorAuthController extends BaseController {
private final TwoFactorAuthService twoFactorAuthService;
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager;
private final TwoFaConfigManager twoFaConfigManager;
private final JwtTokenFactory tokenFactory;
private final SystemSecurityService systemSecurityService;
private final UserService userService;
@ -65,9 +71,9 @@ public class TwoFactorAuthController extends BaseController {
"and Too Many Requests error if rate limits are exceeded.")
@PostMapping("/verification/send")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public void requestTwoFaVerificationCode() throws Exception {
public void requestTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser();
twoFactorAuthService.prepareVerificationCode(user, true);
twoFactorAuthService.prepareVerificationCode(user, providerType, true);
}
@ApiOperation(value = "Check 2FA verification code (checkTwoFaVerificationCode)",
@ -79,9 +85,10 @@ public class TwoFactorAuthController extends BaseController {
@PostMapping("/verification/check")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public JwtTokenPair checkTwoFaVerificationCode(@ApiParam(value = "6-digit verification code", required = true)
@RequestParam TwoFaProviderType providerType,
@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true);
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
if (verificationSuccess) {
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null);
user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal());
@ -93,13 +100,26 @@ public class TwoFactorAuthController extends BaseController {
}
}
@ApiOperation(value = "Get currently used 2FA provider type (getCurrentlyUsedTwoFaProviderType)")
@GetMapping("/provider/type")
@GetMapping("/providers")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public TwoFactorAuthProviderType getCurrentlyUsedTwoFaProviderType() throws ThingsboardException {
public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException {
SecurityUser user = getCurrentUser();
return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId())
.map(TwoFactorAuthAccountConfig::getProviderType).orElse(null);
return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId())
.map(settings -> settings.getConfigs().values()).orElse(Collections.emptyList())
.stream().map(config -> TwoFaProviderInfo.builder()
.type(config.getProviderType())
.isDefault(config.isUseByDefault())
.build())
.collect(Collectors.toList());
}
@Data
@AllArgsConstructor
@Builder
public static class TwoFaProviderInfo {
private TwoFaProviderType type;
private boolean isDefault;
}
}

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

@ -25,15 +25,15 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider;
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.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -49,117 +49,129 @@ import java.util.concurrent.ConcurrentMap;
@TbCoreComponent
public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
private final TwoFactorAuthConfigManager configManager;
private final TwoFaConfigManager configManager;
private final SystemSecurityService systemSecurityService;
private final UserService userService;
private final Map<TwoFactorAuthProviderType, TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>> providers = new EnumMap<>(TwoFactorAuthProviderType.class);
// TODO [viacheslav]: these rate limits are local, and will work bad in the cluster
private final ConcurrentMap<UserId, TbRateLimits> verificationCodeSendingRateLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, TbRateLimits> verificationCodeCheckingRateLimits = new ConcurrentHashMap<>();
private final Map<TwoFaProviderType, TwoFaProvider<TwoFaProviderConfig, TwoFaAccountConfig>> providers = new EnumMap<>(TwoFaProviderType.class);
private static final ThingsboardException ACCOUNT_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
private static final ThingsboardException PROVIDER_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
private static final ThingsboardException PROVIDER_NOT_AVAILABLE_ERROR = new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.GENERAL);
private final ConcurrentMap<UserId, ConcurrentMap<TwoFaProviderType, TbRateLimits>> verificationCodeSendingRateLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, ConcurrentMap<TwoFaProviderType, TbRateLimits>> verificationCodeCheckingRateLimits = new ConcurrentHashMap<>();
@Override
public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) {
return configManager.getAccountTwoFaSettings(tenantId, userId)
.map(settings -> !settings.getConfigs().isEmpty())
.orElse(false);
}
@Override
public void prepareVerificationCode(SecurityUser securityUser, boolean checkLimits) throws Exception {
TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId())
public void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception {
TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType)
.orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR);
prepareVerificationCode(securityUser, accountConfig, checkLimits);
prepareVerificationCode(user, accountConfig, checkLimits);
}
@Override
public void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException {
TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId(), true)
public void prepareVerificationCode(SecurityUser user, TwoFaAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException {
PlatformTwoFaSettings twoFaSettings = configManager.getPlatformTwoFaSettings(user.getTenantId(), true)
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
if (checkLimits) {
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) {
TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> {
return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit(), true);
});
if (!rateLimits.tryConsume()) {
throw new ThingsboardException("Too many verification code sending requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
}
checkRateLimits(user.getId(), accountConfig.getProviderType(), twoFaSettings.getVerificationCodeSendRateLimit(), verificationCodeSendingRateLimits);
}
TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
getTwoFaProvider(accountConfig.getProviderType()).prepareVerificationCode(securityUser, providerConfig, accountConfig);
getTwoFaProvider(accountConfig.getProviderType()).prepareVerificationCode(user, providerConfig, accountConfig);
}
@Override
public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean checkLimits) throws ThingsboardException {
TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId())
public boolean checkVerificationCode(SecurityUser user, TwoFaProviderType providerType, String verificationCode, boolean checkLimits) throws ThingsboardException {
TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType)
.orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR);
return checkVerificationCode(securityUser, verificationCode, accountConfig, checkLimits);
return checkVerificationCode(user, verificationCode, accountConfig, checkLimits);
}
@Override
public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException {
if (!userService.findUserCredentialsByUserId(securityUser.getTenantId(), securityUser.getId()).isEnabled()) {
public boolean checkVerificationCode(SecurityUser user, String verificationCode, TwoFaAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException {
if (!userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()).isEnabled()) {
throw new ThingsboardException("User is disabled", ThingsboardErrorCode.AUTHENTICATION);
}
TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId(), true)
PlatformTwoFaSettings twoFaSettings = configManager.getPlatformTwoFaSettings(user.getTenantId(), true)
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
if (checkLimits) {
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) {
TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> {
return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit(), true);
});
if (!rateLimits.tryConsume()) {
throw new ThingsboardException("Too many verification code checking requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
}
checkRateLimits(user.getId(), accountConfig.getProviderType(), twoFaSettings.getVerificationCodeCheckRateLimit(), verificationCodeCheckingRateLimits);
}
TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
boolean verificationSuccess;
if (StringUtils.isNumeric(verificationCode) && verificationCode.length() == 6) {
verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(securityUser, verificationCode, providerConfig, accountConfig);
verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(user, verificationCode, providerConfig, accountConfig);
} else {
verificationSuccess = false;
}
if (checkLimits) {
try {
systemSecurityService.validateTwoFaVerification(securityUser, verificationSuccess, twoFaSettings);
systemSecurityService.validateTwoFaVerification(user, verificationSuccess, twoFaSettings);
} catch (LockedException e) {
verificationCodeCheckingRateLimits.remove(securityUser.getId());
verificationCodeSendingRateLimits.remove(securityUser.getId());
verificationCodeCheckingRateLimits.remove(user.getId());
verificationCodeSendingRateLimits.remove(user.getId());
throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.AUTHENTICATION);
}
if (verificationSuccess) {
verificationCodeCheckingRateLimits.remove(securityUser.getId());
verificationCodeSendingRateLimits.remove(securityUser.getId());
verificationCodeCheckingRateLimits.remove(user.getId());
verificationCodeSendingRateLimits.remove(user.getId());
}
}
return verificationSuccess;
}
private void checkRateLimits(UserId userId, TwoFaProviderType providerType, String rateLimitConfig,
ConcurrentMap<UserId, ConcurrentMap<TwoFaProviderType, TbRateLimits>> rateLimits) throws ThingsboardException {
if (StringUtils.isNotEmpty(rateLimitConfig)) {
ConcurrentMap<TwoFaProviderType, TbRateLimits> providersRateLimits = rateLimits.computeIfAbsent(userId, i -> new ConcurrentHashMap<>());
TbRateLimits rateLimit = providersRateLimits.get(providerType);
if (rateLimit == null || !rateLimit.getConfig().equals(rateLimitConfig)) {
rateLimit = new TbRateLimits(rateLimitConfig, true);
providersRateLimits.put(providerType, rateLimit);
}
if (!rateLimit.tryConsume()) {
throw new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
} else {
rateLimits.remove(userId);
}
}
@Override
public TwoFactorAuthAccountConfig generateNewAccountConfig(User user, TwoFactorAuthProviderType providerType) throws ThingsboardException {
TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(user.getTenantId(), providerType);
public TwoFaAccountConfig generateNewAccountConfig(User user, TwoFaProviderType providerType) throws ThingsboardException {
TwoFaProviderConfig providerConfig = getTwoFaProviderConfig(user.getTenantId(), providerType);
return getTwoFaProvider(providerType).generateNewAccountConfig(user, providerConfig);
}
private TwoFactorAuthProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) throws ThingsboardException {
return configManager.getTwoFaSettings(tenantId, true)
private TwoFaProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException {
return configManager.getPlatformTwoFaSettings(tenantId, true)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType))
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
}
private TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> getTwoFaProvider(TwoFactorAuthProviderType providerType) throws ThingsboardException {
private TwoFaProvider<TwoFaProviderConfig, TwoFaAccountConfig> getTwoFaProvider(TwoFaProviderType providerType) throws ThingsboardException {
return Optional.ofNullable(providers.get(providerType))
.orElseThrow(() -> PROVIDER_NOT_AVAILABLE_ERROR);
}
@Autowired
private void setProviders(Collection<TwoFactorAuthProvider> providers) {
private void setProviders(Collection<TwoFaProvider> providers) {
providers.forEach(provider -> {
this.providers.put(provider.getType(), provider);
});

21
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java

@ -17,20 +17,27 @@ package org.thingsboard.server.service.security.auth.mfa;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TwoFactorAuthService {
void prepareVerificationCode(SecurityUser securityUser, boolean checkLimits) throws Exception;
boolean isTwoFaEnabled(TenantId tenantId, UserId userId);
void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException;
boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean checkLimits) throws ThingsboardException;
void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception;
boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException;
void prepareVerificationCode(SecurityUser user, TwoFaAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException;
TwoFactorAuthAccountConfig generateNewAccountConfig(User user, TwoFactorAuthProviderType providerType) throws ThingsboardException;
boolean checkVerificationCode(SecurityUser user, TwoFaProviderType providerType, String verificationCode, boolean checkLimits) throws ThingsboardException;
boolean checkVerificationCode(SecurityUser user, String verificationCode, TwoFaAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException;
TwoFaAccountConfig generateNewAccountConfig(User user, TwoFaProviderType providerType) throws ThingsboardException;
}

89
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java → application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java

@ -21,17 +21,16 @@ import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.security.UserAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
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.attributes.AttributesService;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.settings.AdminSettingsDao;
@ -39,12 +38,15 @@ import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserAuthSettingsDao;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
@Service
@RequiredArgsConstructor
public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigManager {
public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
private final UserAuthSettingsDao userAuthSettingsDao;
private final AdminSettingsService adminSettingsService;
@ -55,60 +57,77 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan
@Override
public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) {
return getTwoFaAccountConfig(tenantId, userId).isPresent();
public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId) {
return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
.flatMap(userAuthSettings -> Optional.ofNullable(userAuthSettings.getTwoFaSettings()))
.map(twoFaSettings -> {
twoFaSettings.getConfigs().keySet().removeIf(providerType -> {
return getTwoFaProviderConfig(tenantId, providerType).isEmpty();
});
return twoFaSettings;
});
}
@Override
public Optional<TwoFactorAuthAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId) {
return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
.flatMap(userAuthSettings -> Optional.ofNullable(userAuthSettings.getTwoFaAccountConfig()))
.filter(twoFaAccountConfig -> getTwoFaProviderConfig(tenantId, twoFaAccountConfig.getProviderType()).isPresent());
public Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
return getAccountTwoFaSettings(tenantId, userId)
.map(AccountTwoFaSettings::getConfigs)
.flatMap(configs -> Optional.ofNullable(configs.get(providerType)));
}
@Override
public void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException {
public AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig) {
getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
.orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS));
.orElseThrow(() -> new IllegalArgumentException("2FA provider is not configured"));
return createOrUpdateAccountTwoFaSettings(tenantId, userId, accountTwoFaSettings -> {
Map<TwoFaProviderType, TwoFaAccountConfig> configs = accountTwoFaSettings.getConfigs();
configs.put(accountConfig.getProviderType(), accountConfig);
});
}
@Override
public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
return createOrUpdateAccountTwoFaSettings(tenantId, userId, accountTwoFaSettings -> {
accountTwoFaSettings.getConfigs().keySet().removeIf(providerType::equals);
});
}
private AccountTwoFaSettings createOrUpdateAccountTwoFaSettings(TenantId tenantId, UserId userId, Consumer<AccountTwoFaSettings> updater) {
UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
.orElseGet(() -> {
UserAuthSettings newUserAuthSettings = new UserAuthSettings();
newUserAuthSettings.setUserId(userId);
AccountTwoFaSettings newAccountTwoFaSettings = new AccountTwoFaSettings();
newAccountTwoFaSettings.setConfigs(new LinkedHashMap<>());
newUserAuthSettings.setTwoFaSettings(newAccountTwoFaSettings);
return newUserAuthSettings;
});
userAuthSettings.setTwoFaAccountConfig(accountConfig);
updater.accept(userAuthSettings.getTwoFaSettings());
userAuthSettingsDao.save(tenantId, userAuthSettings);
}
@Override
public void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId) {
Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
.ifPresent(userAuthSettings -> {
userAuthSettings.setTwoFaAccountConfig(null);
userAuthSettingsDao.save(tenantId, userAuthSettings);
});
return userAuthSettings.getTwoFaSettings();
}
private Optional<TwoFactorAuthProviderConfig> getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) {
return getTwoFaSettings(tenantId, true)
private Optional<TwoFaProviderConfig> getTwoFaProviderConfig(TenantId tenantId, TwoFaProviderType providerType) {
return getPlatformTwoFaSettings(tenantId, true)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType));
}
@SneakyThrows({InterruptedException.class, ExecutionException.class})
@Override
public Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault) {
public Optional<PlatformTwoFaSettings> getPlatformTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault) {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, TWO_FACTOR_AUTH_SETTINGS_KEY))
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TwoFactorAuthSettings.class));
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), PlatformTwoFaSettings.class));
} else {
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));
Optional<PlatformTwoFaSettings> tenantTwoFaSettings = attributesService.find(TenantId.SYS_TENANT_ID, tenantId,
DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get()
.map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), PlatformTwoFaSettings.class));
if (sysadminSettingsAsDefault) {
if (tenantTwoFaSettings.isEmpty() || tenantTwoFaSettings.get().isUseSystemTwoFactorAuthSettings()) {
return getTwoFaSettings(TenantId.SYS_TENANT_ID, false);
return getPlatformTwoFaSettings(TenantId.SYS_TENANT_ID, false);
}
}
return tenantTwoFaSettings;
@ -117,7 +136,7 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan
@SneakyThrows({InterruptedException.class, ExecutionException.class})
@Override
public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) {
public void savePlatformTwoFaSettings(TenantId tenantId, PlatformTwoFaSettings twoFactorAuthSettings) {
if (tenantId.equals(TenantId.SYS_TENANT_ID) || !twoFactorAuthSettings.isUseSystemTwoFactorAuthSettings()) {
ConstraintValidator.validateFields(twoFactorAuthSettings);
}
@ -139,7 +158,7 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan
@SneakyThrows({InterruptedException.class, ExecutionException.class})
@Override
public void deleteTwoFaSettings(TenantId tenantId) {
public void deletePlatformTwoFaSettings(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()));

45
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java

@ -0,0 +1,45 @@
/**
* 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.service.security.auth.mfa.config;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import java.util.Optional;
public interface TwoFaConfigManager {
Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId);
Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType);
AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig);
AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType);
Optional<PlatformTwoFaSettings> getPlatformTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault);
void savePlatformTwoFaSettings(TenantId tenantId, PlatformTwoFaSettings twoFactorAuthSettings);
void deletePlatformTwoFaSettings(TenantId tenantId);
}

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

@ -1,43 +0,0 @@
/**
* 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.service.security.auth.mfa.config;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig;
import java.util.Optional;
public interface TwoFactorAuthConfigManager {
boolean isTwoFaEnabled(TenantId tenantId, UserId userId);
Optional<TwoFactorAuthAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId);
void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException;
void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId);
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/provider/TwoFactorAuthProvider.java → application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java

@ -17,12 +17,12 @@ package org.thingsboard.server.service.security.auth.mfa.provider;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TwoFactorAuthProvider<C extends TwoFactorAuthProviderConfig, A extends TwoFactorAuthAccountConfig> {
public interface TwoFaProvider<C extends TwoFaProviderConfig, A extends TwoFaAccountConfig> {
A generateNewAccountConfig(User user, C providerConfig);
@ -31,6 +31,6 @@ public interface TwoFactorAuthProvider<C extends TwoFactorAuthProviderConfig, A
boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, C providerConfig, A accountConfig);
TwoFactorAuthProviderType getType();
TwoFaProviderType getType();
}

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

@ -21,18 +21,18 @@ import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.mfa.account.OtpBasedTwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.OtpBasedTwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider;
import org.thingsboard.server.common.data.security.model.mfa.account.OtpBasedTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.OtpBasedTwoFaProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.concurrent.TimeUnit;
public abstract class OtpBasedTwoFactorAuthProvider<C extends OtpBasedTwoFactorAuthProviderConfig, A extends OtpBasedTwoFactorAuthAccountConfig> implements TwoFactorAuthProvider<C, A> {
public abstract class OtpBasedTwoFaProvider<C extends OtpBasedTwoFaProviderConfig, A extends OtpBasedTwoFaAccountConfig> implements TwoFaProvider<C, A> {
private final Cache verificationCodesCache;
protected OtpBasedTwoFactorAuthProvider(CacheManager cacheManager) {
protected OtpBasedTwoFaProvider(CacheManager cacheManager) {
this.verificationCodesCache = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE);
}
@ -70,7 +70,7 @@ public abstract class OtpBasedTwoFactorAuthProvider<C extends OtpBasedTwoFactorA
public static class Otp {
private final long timestamp;
private final String value;
private final OtpBasedTwoFactorAuthAccountConfig accountConfig;
private final OtpBasedTwoFaAccountConfig accountConfig;
}
}

20
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java → application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java

@ -21,9 +21,9 @@ import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -31,23 +31,23 @@ import java.util.Map;
@Service
@TbCoreComponent
public class SmsTwoFactorAuthProvider extends OtpBasedTwoFactorAuthProvider<SmsTwoFactorAuthProviderConfig, SmsTwoFactorAuthAccountConfig> {
public class SmsTwoFaProvider extends OtpBasedTwoFaProvider<SmsTwoFaProviderConfig, SmsTwoFaAccountConfig> {
private final SmsService smsService;
public SmsTwoFactorAuthProvider(CacheManager cacheManager, SmsService smsService) {
public SmsTwoFaProvider(CacheManager cacheManager, SmsService smsService) {
super(cacheManager);
this.smsService = smsService;
}
@Override
public SmsTwoFactorAuthAccountConfig generateNewAccountConfig(User user, SmsTwoFactorAuthProviderConfig providerConfig) {
return new SmsTwoFactorAuthAccountConfig();
public SmsTwoFaAccountConfig generateNewAccountConfig(User user, SmsTwoFaProviderConfig providerConfig) {
return new SmsTwoFaAccountConfig();
}
@Override
protected void sendVerificationCode(SecurityUser user, String verificationCode, SmsTwoFactorAuthProviderConfig providerConfig, SmsTwoFactorAuthAccountConfig accountConfig) throws ThingsboardException {
protected void sendVerificationCode(SecurityUser user, String verificationCode, SmsTwoFaProviderConfig providerConfig, SmsTwoFaAccountConfig accountConfig) throws ThingsboardException {
Map<String, String> messageData = Map.of(
"verificationCode", verificationCode,
"userEmail", user.getEmail()
@ -60,8 +60,8 @@ public class SmsTwoFactorAuthProvider extends OtpBasedTwoFactorAuthProvider<SmsT
@Override
public TwoFactorAuthProviderType getType() {
return TwoFactorAuthProviderType.SMS;
public TwoFaProviderType getType() {
return TwoFaProviderType.SMS;
}
}

22
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java → application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java

@ -24,34 +24,34 @@ import org.jboss.aerogear.security.otp.api.Base32;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFaProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
@Service
@RequiredArgsConstructor
@TbCoreComponent
public class TotpTwoFactorAuthProvider implements TwoFactorAuthProvider<TotpTwoFactorAuthProviderConfig, TotpTwoFactorAuthAccountConfig> {
public class TotpTwoFaProvider implements TwoFaProvider<TotpTwoFaProviderConfig, TotpTwoFaAccountConfig> {
@Override
public final TotpTwoFactorAuthAccountConfig generateNewAccountConfig(User user, TotpTwoFactorAuthProviderConfig providerConfig) {
TotpTwoFactorAuthAccountConfig config = new TotpTwoFactorAuthAccountConfig();
public final TotpTwoFaAccountConfig generateNewAccountConfig(User user, TotpTwoFaProviderConfig providerConfig) {
TotpTwoFaAccountConfig config = new TotpTwoFaAccountConfig();
String secretKey = generateSecretKey();
config.setAuthUrl(getTotpAuthUrl(user, secretKey, providerConfig));
return config;
}
@Override
public final boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TotpTwoFactorAuthProviderConfig providerConfig, TotpTwoFactorAuthAccountConfig accountConfig) {
public final boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TotpTwoFaProviderConfig providerConfig, TotpTwoFaAccountConfig accountConfig) {
String secretKey = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build().getQueryParams().getFirst("secret");
return new Totp(secretKey).verify(verificationCode);
}
@SneakyThrows
private String getTotpAuthUrl(User user, String secretKey, TotpTwoFactorAuthProviderConfig providerConfig) {
private String getTotpAuthUrl(User user, String secretKey, TotpTwoFaProviderConfig providerConfig) {
URIBuilder uri = new URIBuilder()
.setScheme("otpauth")
.setHost("totp")
@ -67,8 +67,8 @@ public class TotpTwoFactorAuthProvider implements TwoFactorAuthProvider<TotpTwoF
@Override
public TwoFactorAuthProviderType getType() {
return TwoFactorAuthProviderType.TOTP;
public TwoFaProviderType getType() {
return TwoFaProviderType.TOTP;
}
}

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

@ -39,7 +39,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -54,17 +54,17 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
private final SystemSecurityService systemSecurityService;
private final UserService userService;
private final CustomerService customerService;
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager;
private final TwoFactorAuthService twoFactorAuthService;
@Autowired
public RestAuthenticationProvider(final UserService userService,
final CustomerService customerService,
final SystemSecurityService systemSecurityService,
TwoFactorAuthConfigManager twoFactorAuthConfigManager) {
TwoFactorAuthService twoFactorAuthService) {
this.userService = userService;
this.customerService = customerService;
this.systemSecurityService = systemSecurityService;
this.twoFactorAuthConfigManager = twoFactorAuthConfigManager;
this.twoFactorAuthService = twoFactorAuthService;
}
@Override
@ -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.getTenantId(), securityUser.getId())) {
if (twoFactorAuthService.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

@ -26,7 +26,7 @@ import org.springframework.stereotype.Component;
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.TwoFaConfigManager;
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;
@ -44,7 +44,7 @@ import java.util.concurrent.TimeUnit;
public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper mapper;
private final JwtTokenFactory tokenFactory;
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager;
private final TwoFaConfigManager twoFaConfigManager;
private final RefreshTokenRepository refreshTokenRepository;
@Override
@ -54,7 +54,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
JwtTokenPair tokenPair = new JwtTokenPair();
if (authentication instanceof MfaAuthenticationToken) {
int preVerificationTokenLifetime = twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId(), true)
int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true)
.flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())).orElse((int) TimeUnit.MINUTES.toSeconds(30));
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
tokenPair.setRefreshToken(null);

4
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -54,7 +54,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -160,7 +160,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
}
@Override
public void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings) {
public void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, PlatformTwoFaSettings twoFaSettings) {
TenantId tenantId = securityUser.getTenantId();
UserId userId = securityUser.getId();

4
application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java

@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
@ -36,7 +36,7 @@ public interface SystemSecurityService {
void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException;
void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings);
void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, PlatformTwoFaSettings twoFaSettings);
void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException;

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

@ -30,17 +30,18 @@ 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.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.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 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.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.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.service.security.auth.mfa.provider.impl.OtpBasedTwoFaProvider;
import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFaProvider;
import java.util.Arrays;
import java.util.Collections;
@ -58,13 +59,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@SpyBean
private TotpTwoFactorAuthProvider totpTwoFactorAuthProvider;
private TotpTwoFaProvider totpTwoFactorAuthProvider;
@MockBean
private SmsService smsService;
@Autowired
private CacheManager cacheManager;
@Autowired
private TwoFactorAuthConfigManager twoFactorAuthConfigManager;
private TwoFaConfigManager twoFaConfigManager;
@Autowired
private TwoFactorAuthService twoFactorAuthService;
@Before
public void beforeEach() throws Exception {
@ -73,8 +76,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@After
public void afterEach() {
twoFactorAuthConfigManager.deleteTwoFaSettings(TenantId.SYS_TENANT_ID);
twoFactorAuthConfigManager.deleteTwoFaSettings(tenantId);
twoFaConfigManager.deletePlatformTwoFaSettings(TenantId.SYS_TENANT_ID);
twoFaConfigManager.deletePlatformTwoFaSettings(tenantId);
}
@ -88,13 +91,13 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
}
private void testSaveTestTwoFaSettings() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig();
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig();
SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}");
smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings();
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig));
twoFaSettings.setVerificationCodeSendRateLimit("1:60");
twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
@ -103,7 +106,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
TwoFactorAuthSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class);
PlatformTwoFaSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), PlatformTwoFaSettings.class);
assertThat(savedTwoFaSettings.getProviders()).hasSize(2);
assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig);
@ -113,7 +116,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
public void testSaveTwoFaSettings_validationError() throws Exception {
loginTenantAdmin();
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings();
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Collections.emptyList());
twoFaSettings.setVerificationCodeSendRateLimit("ab:aba");
twoFaSettings.setVerificationCodeCheckRateLimit("0:12");
@ -146,19 +149,19 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test
public void testGetTwoFaSettings_useSysadminSettingsAsDefault() throws Exception {
loginSysAdmin();
TwoFactorAuthSettings sysadminTwoFaSettings = new TwoFactorAuthSettings();
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig();
PlatformTwoFaSettings sysadminTwoFaSettings = new PlatformTwoFaSettings();
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
sysadminTwoFaSettings.setProviders(Collections.singletonList(totpTwoFaProviderConfig));
sysadminTwoFaSettings.setMaxVerificationFailuresBeforeUserLockout(25);
doPost("/api/2fa/settings", sysadminTwoFaSettings).andExpect(status().isOk());
loginTenantAdmin();
TwoFactorAuthSettings tenantTwoFaSettings = new TwoFactorAuthSettings();
PlatformTwoFaSettings tenantTwoFaSettings = new PlatformTwoFaSettings();
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);
PlatformTwoFaSettings twoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), PlatformTwoFaSettings.class);
assertThat(twoFaSettings).isEqualTo(tenantTwoFaSettings);
doPost("/api/2fa/account/config/generate?providerType=TOTP")
@ -168,7 +171,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
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);
twoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), PlatformTwoFaSettings.class);
assertThat(twoFaSettings).isEqualTo(tenantTwoFaSettings);
assertThat(getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=TOTP")
@ -192,13 +195,13 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
.andExpect(status().isOk());
loginSysAdmin();
twoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class);
twoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), PlatformTwoFaSettings.class);
assertThat(twoFaSettings).isEqualTo(sysadminTwoFaSettings);
}
@Test
public void testSaveTotpTwoFaProviderConfig_validationError() throws Exception {
TotpTwoFactorAuthProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig();
TotpTwoFaProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
invalidTotpTwoFaProviderConfig.setIssuerName(" ");
String errorResponse = saveTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig);
@ -207,7 +210,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test
public void testSaveSmsTwoFaProviderConfig_validationError() throws Exception {
SmsTwoFactorAuthProviderConfig invalidSmsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig();
SmsTwoFaProviderConfig invalidSmsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code");
invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(60);
@ -221,8 +224,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
assertThat(errorResponse).containsIgnoringCase("verification code lifetime is required");
}
private String saveTwoFaSettingsAndGetError(TwoFactorAuthProviderConfig invalidTwoFaProviderConfig) throws Exception {
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings();
private String saveTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception {
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig));
return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
@ -235,12 +238,12 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
loginTenantAdmin();
TwoFactorAuthProviderType notConfiguredProviderType = TwoFactorAuthProviderType.TOTP;
TwoFaProviderType notConfiguredProviderType = TwoFaProviderType.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();
TotpTwoFaAccountConfig notConfiguredProviderAccountConfig = new TotpTwoFaAccountConfig();
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");
@ -248,7 +251,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test
public void testGenerateTotpTwoFaAccountConfig() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin();
@ -258,11 +261,11 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test
public void testSubmitTotpTwoFaAccountConfig() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin();
TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
TotpTwoFaAccountConfig 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));
@ -274,7 +277,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
loginTenantAdmin();
TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = new TotpTwoFactorAuthAccountConfig();
TotpTwoFaAccountConfig totpTwoFaAccountConfig = new TotpTwoFaAccountConfig();
totpTwoFaAccountConfig.setAuthUrl(null);
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig)
@ -293,11 +296,11 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test
public void testVerifyAndSaveTotpTwoFaAccountConfig() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin();
TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
TotpTwoFaAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
String secret = UriComponentsBuilder.fromUriString(generatedTotpTwoFaAccountConfig.getAuthUrl()).build()
.getQueryParams().getFirst("secret");
@ -306,17 +309,17 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig)
.andExpect(status().isOk());
TwoFactorAuthAccountConfig twoFaAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class);
TwoFaAccountConfig twoFaAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class);
assertThat(twoFaAccountConfig).isEqualTo(generatedTotpTwoFaAccountConfig);
}
@Test
public void testVerifyAndSaveTotpTwoFaAccountConfig_incorrectVerificationCode() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin();
TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
TotpTwoFaAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
String incorrectVerificationCode = "100000";
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + incorrectVerificationCode, generatedTotpTwoFaAccountConfig)
@ -325,12 +328,12 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
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);
private TotpTwoFaAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFaProviderConfig totpTwoFaProviderConfig) throws Exception {
TwoFaAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP")
.andExpect(status().isOk()), TwoFaAccountConfig.class);
assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFaAccountConfig.class);
assertThat(((TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> {
assertThat(((TotpTwoFaAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> {
UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build();
assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth");
assertThat(otpAuthUrl.getHost()).isEqualTo("totp");
@ -340,14 +343,14 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
assertDoesNotThrow(() -> Base32.decode(secretKey));
});
});
return (TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig;
return (TotpTwoFaAccountConfig) generatedTwoFaAccountConfig;
}
@Test
public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception {
testVerifyAndSaveTotpTwoFaAccountConfig();
assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()),
TotpTwoFactorAuthAccountConfig.class)).isNotNull();
TotpTwoFaAccountConfig.class)).isNotNull();
loginSysAdmin();
@ -371,13 +374,13 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
loginTenantAdmin();
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
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();
.get(tenantAdminUserId, OtpBasedTwoFaProvider.Otp.class).getValue();
verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> {
return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber());
@ -388,7 +391,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
public void testSubmitSmsTwoFaAccountConfig_validationError() throws Exception {
configureSmsTwoFaProvider("${verificationCode}");
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
String blankPhoneNumber = "";
smsTwoFaAccountConfig.setPhoneNumber(blankPhoneNumber);
@ -410,7 +413,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
loginTenantAdmin();
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38051889445");
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
@ -424,7 +427,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, smsTwoFaAccountConfig)
.andExpect(status().isOk());
TwoFactorAuthAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class);
TwoFaAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class);
assertThat(accountConfig).isEqualTo(smsTwoFaAccountConfig);
}
@ -434,7 +437,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
loginTenantAdmin();
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38051889445");
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=100000", smsTwoFaAccountConfig)
@ -447,7 +450,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
configureSmsTwoFaProvider("${verificationCode}");
loginTenantAdmin();
SmsTwoFactorAuthAccountConfig initialSmsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
SmsTwoFaAccountConfig initialSmsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
initialSmsTwoFaAccountConfig.setPhoneNumber("+38051889445");
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
@ -459,7 +462,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
String correctVerificationCode = verificationCodeCaptor.getValue();
SmsTwoFactorAuthAccountConfig anotherSmsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
SmsTwoFaAccountConfig anotherSmsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
anotherSmsTwoFaAccountConfig.setPhoneNumber("+38111111111");
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, anotherSmsTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
@ -467,20 +470,20 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, initialSmsTwoFaAccountConfig)
.andExpect(status().isOk());
TwoFactorAuthAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class);
TwoFaAccountConfig accountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class);
assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig);
}
private TotpTwoFactorAuthProviderConfig configureTotpTwoFaProvider() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig();
private TotpTwoFaProviderConfig configureTotpTwoFaProvider() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
saveProvidersConfigs(totpTwoFaProviderConfig);
return totpTwoFaProviderConfig;
}
private SmsTwoFactorAuthProviderConfig configureSmsTwoFaProvider(String verificationMessageTemplate) throws Exception {
SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig();
private SmsTwoFaProviderConfig configureSmsTwoFaProvider(String verificationMessageTemplate) throws Exception {
SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate(verificationMessageTemplate);
smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
@ -488,8 +491,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
return smsTwoFaProviderConfig;
}
private void saveProvidersConfigs(TwoFactorAuthProviderConfig... providerConfigs) throws Exception {
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings();
private void saveProvidersConfigs(TwoFaProviderConfig... providerConfigs) throws Exception {
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList()));
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
@ -498,24 +501,24 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test
public void testIsTwoFaEnabled() throws Exception {
configureSmsTwoFaProvider("${verificationCode}");
SmsTwoFactorAuthAccountConfig accountConfig = new SmsTwoFactorAuthAccountConfig();
SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig();
accountConfig.setPhoneNumber("+38050505050");
twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig);
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig);
assertThat(twoFactorAuthConfigManager.isTwoFaEnabled(tenantId, tenantAdminUserId)).isTrue();
assertThat(twoFactorAuthService.isTwoFaEnabled(tenantId, tenantAdminUserId)).isTrue();
}
@Test
public void testDeleteTwoFaAccountConfig() throws Exception {
configureSmsTwoFaProvider("${verificationCode}");
SmsTwoFactorAuthAccountConfig accountConfig = new SmsTwoFactorAuthAccountConfig();
SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig();
accountConfig.setPhoneNumber("+38050505050");
loginTenantAdmin();
twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig);
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig);
TwoFactorAuthAccountConfig savedAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class);
TwoFaAccountConfig savedAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFaAccountConfig.class);
assertThat(savedAccountConfig).isEqualTo(accountConfig);
doDelete("/api/2fa/account/config").andExpect(status().isOk());

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

@ -39,14 +39,14 @@ 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.TwoFactorAuthConfigManager;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
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.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig;
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.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.JwtTokenPair;
@ -68,319 +68,319 @@ import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class TwoFactorAuthTest extends AbstractControllerTest {
@Autowired
private TwoFactorAuthConfigManager twoFactorAuthConfigManager;
@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() {
twoFactorAuthConfigManager.deleteTwoFaSettings(tenantId);
twoFactorAuthConfigManager.deleteTwoFaSettings(TenantId.SYS_TENANT_ID);
}
@Test
public void testTwoFa_totp() throws Exception {
TotpTwoFactorAuthAccountConfig 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 {
TotpTwoFactorAuthAccountConfig 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 {
TotpTwoFactorAuthAccountConfig 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();
twoFactorAuthConfigManager.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 TotpTwoFactorAuthAccountConfig configureTotpTwoFa(Consumer<TwoFactorAuthSettings>... customizer) throws ThingsboardException {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings();
twoFaSettings.setUseSystemTwoFactorAuthSettings(false);
twoFaSettings.setProviders(Arrays.stream(new TwoFactorAuthProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings));
twoFactorAuthConfigManager.saveTwoFaSettings(tenantId, twoFaSettings);
TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = (TotpTwoFactorAuthAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFactorAuthProviderType.TOTP);
twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig);
return totpTwoFaAccountConfig;
}
private SmsTwoFactorAuthAccountConfig configureSmsTwoFa(Consumer<SmsTwoFactorAuthProviderConfig>... customizer) throws ThingsboardException {
SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig();
smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}");
Arrays.stream(customizer).forEach(c -> c.accept(smsTwoFaProviderConfig));
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings();
twoFaSettings.setUseSystemTwoFactorAuthSettings(false);
twoFaSettings.setProviders(Arrays.stream(new TwoFactorAuthProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList()));
twoFactorAuthConfigManager.saveTwoFaSettings(tenantId, twoFaSettings);
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38050505050");
twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig);
return smsTwoFaAccountConfig;
}
private String getCorrectTotp(TotpTwoFactorAuthAccountConfig 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();
//
// 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();
// }
}

4
common/data/src/main/java/org/thingsboard/server/common/data/security/UserAuthSettings.java

@ -20,7 +20,7 @@ import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.id.UserAuthSettingsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
@Data
@EqualsAndHashCode(callSuper = true)
@ -29,6 +29,6 @@ public class UserAuthSettings extends BaseData<UserAuthSettingsId> {
private static final long serialVersionUID = 2628320657987010348L;
private UserId userId;
private TwoFactorAuthAccountConfig twoFaAccountConfig;
private AccountTwoFaSettings twoFaSettings;
}

10
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/TwoFactorAuthSettings.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java

@ -18,8 +18,8 @@ package org.thingsboard.server.common.data.security.model.mfa;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import javax.validation.Valid;
import javax.validation.constraints.Min;
@ -29,7 +29,7 @@ import java.util.Optional;
@Data
@ApiModel
public class TwoFactorAuthSettings {
public class PlatformTwoFaSettings {
@ApiModelProperty(value = "Option for tenant admins to use 2FA settings configured by sysadmin. " +
"If this param is set to true, then the settings will not be validated for constraints " +
@ -37,7 +37,7 @@ public class TwoFactorAuthSettings {
private boolean useSystemTwoFactorAuthSettings;
@ApiModelProperty(value = "The list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list.")
@Valid
private List<TwoFactorAuthProviderConfig> providers;
private List<TwoFaProviderConfig> providers;
@ApiModelProperty(value = "Rate limit configuration for verification code sending. The format is standard: 'amountOfRequests:periodInSeconds'. " +
"The value of '1:60' would limit verification code sending requests to one per minute.", example = "1:60", required = false)
@ -55,7 +55,7 @@ public class TwoFactorAuthSettings {
private Integer totalAllowedTimeForVerification;
public Optional<TwoFactorAuthProviderConfig> getProviderConfig(TwoFactorAuthProviderType providerType) {
public Optional<TwoFaProviderConfig> getProviderConfig(TwoFaProviderType providerType) {
return Optional.ofNullable(providers)
.flatMap(providersConfigs -> providersConfigs.stream()
.filter(providerConfig -> providerConfig.getProviderType() == providerType)

26
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java

@ -0,0 +1,26 @@
/**
* 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.common.data.security.model.mfa.account;
import lombok.Data;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import java.util.LinkedHashMap;
@Data
public class AccountTwoFaSettings {
private LinkedHashMap<TwoFaProviderType, TwoFaAccountConfig> configs;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFactorAuthAccountConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java

@ -16,7 +16,9 @@
package org.thingsboard.server.common.data.security.model.mfa.account;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
public abstract class OtpBasedTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig {
@EqualsAndHashCode(callSuper = true)
public abstract class OtpBasedTwoFaAccountConfig extends TwoFaAccountConfig {
}

8
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFactorAuthAccountConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java

@ -19,7 +19,7 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@ -27,7 +27,7 @@ import javax.validation.constraints.Pattern;
@ApiModel
@EqualsAndHashCode(callSuper = true)
@Data
public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig {
public class SmsTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig {
@ApiModelProperty(value = "Phone number to use for 2FA. Must no be blank and must be of E.164 number format.", required = true)
@NotBlank(message = "phone number cannot be blank")
@ -35,8 +35,8 @@ public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountC
private String phoneNumber;
@Override
public TwoFactorAuthProviderType getProviderType() {
return TwoFactorAuthProviderType.SMS;
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.SMS;
}
}

13
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFactorAuthAccountConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java

@ -18,14 +18,16 @@ package org.thingsboard.server.common.data.security.model.mfa.account;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@ApiModel
@ApiModel // FIXME [viacheslav]
@Data
public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig {
@EqualsAndHashCode(callSuper = true)
public class TotpTwoFaAccountConfig extends TwoFaAccountConfig {
@ApiModelProperty(value = "OTP auth URL used to generate a QR code to scan with an authenticator app. Must not be blank and must follow specific pattern.",
example = "otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII", required = true)
@ -34,8 +36,9 @@ public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfi
private String authUrl;
@Override
public TwoFactorAuthProviderType getProviderType() {
return TwoFactorAuthProviderType.TOTP;
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.TOTP;
}
}

14
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFactorAuthAccountConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java

@ -20,19 +20,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import lombok.Data;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "providerType")
@JsonSubTypes({
@Type(name = "TOTP", value = TotpTwoFactorAuthAccountConfig.class),
@Type(name = "SMS", value = SmsTwoFactorAuthAccountConfig.class)
@Type(name = "TOTP", value = TotpTwoFaAccountConfig.class),
@Type(name = "SMS", value = SmsTwoFaAccountConfig.class)
})
public interface TwoFactorAuthAccountConfig {
@Data
public abstract class TwoFaAccountConfig {
private boolean useByDefault;
@JsonIgnore
TwoFactorAuthProviderType getProviderType();
public abstract TwoFaProviderType getProviderType();
}

2
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFactorAuthProviderConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java

@ -21,7 +21,7 @@ import lombok.Data;
import javax.validation.constraints.Min;
@Data
public abstract class OtpBasedTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig {
public abstract class OtpBasedTwoFaProviderConfig implements TwoFaProviderConfig {
@ApiModelProperty(value = "Verification code lifetime in seconds. Verification codes with a lifetime bigger than this param " +
"will be considered incorrect", example = "60", required = true)
@Min(value = 1, message = "verification code lifetime is required")

8
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFactorAuthProviderConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java

@ -23,10 +23,10 @@ import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@ApiModel(parent = OtpBasedTwoFactorAuthProviderConfig.class)
@ApiModel(parent = OtpBasedTwoFaProviderConfig.class)
@EqualsAndHashCode(callSuper = true)
@Data
public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig {
public class SmsTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig {
@ApiModelProperty(value = "SMS verification message template. Available template variables are ${verificationCode} and ${userEmail}. " +
"It must not be blank and must contain verification code variable.",
@ -36,8 +36,8 @@ public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProvide
private String smsVerificationMessageTemplate;
@Override
public TwoFactorAuthProviderType getProviderType() {
return TwoFactorAuthProviderType.SMS;
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.SMS;
}
}

6
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFactorAuthProviderConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java

@ -23,7 +23,7 @@ import javax.validation.constraints.NotBlank;
@ApiModel
@Data
public class TotpTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig {
public class TotpTwoFaProviderConfig implements TwoFaProviderConfig {
@ApiModelProperty(value = "Issuer name that will be displayed in an authenticator app near a username. " +
"Must not be blank.", example = "ThingsBoard", required = true)
@ -31,8 +31,8 @@ public class TotpTwoFactorAuthProviderConfig implements TwoFactorAuthProviderCon
private String issuerName;
@Override
public TwoFactorAuthProviderType getProviderType() {
return TwoFactorAuthProviderType.TOTP;
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.TOTP;
}
}

8
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFactorAuthProviderConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java

@ -26,12 +26,12 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
use = JsonTypeInfo.Id.NAME,
property = "providerType")
@JsonSubTypes({
@Type(name = "TOTP", value = TotpTwoFactorAuthProviderConfig.class),
@Type(name = "SMS", value = SmsTwoFactorAuthProviderConfig.class)
@Type(name = "TOTP", value = TotpTwoFaProviderConfig.class),
@Type(name = "SMS", value = SmsTwoFaProviderConfig.class)
})
public interface TwoFactorAuthProviderConfig {
public interface TwoFaProviderConfig {
@JsonIgnore
TwoFactorAuthProviderType getProviderType();
TwoFaProviderType getProviderType();
}

2
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFactorAuthProviderType.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java

@ -15,7 +15,7 @@
*/
package org.thingsboard.server.common.data.security.model.mfa.provider;
public enum TwoFactorAuthProviderType {
public enum TwoFaProviderType {
TOTP,
SMS
}

6
common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java

@ -28,6 +28,7 @@ import java.time.Duration;
*/
public class TbRateLimits {
private final LocalBucket bucket;
private final String config;
public TbRateLimits(String limitsConfiguration) {
this(limitsConfiguration, false);
@ -48,6 +49,7 @@ public class TbRateLimits {
} else {
throw new IllegalArgumentException("Failed to parse rate limits configuration: " + limitsConfiguration);
}
this.config = limitsConfiguration;
}
public boolean tryConsume() {
@ -58,4 +60,8 @@ public class TbRateLimits {
return bucket.tryConsume(number);
}
public String getConfig() {
return config;
}
}

2
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -564,7 +564,7 @@ public class ModelConstants {
* */
public static final String USER_AUTH_SETTINGS_COLUMN_FAMILY_NAME = "user_auth_settings";
public static final String USER_AUTH_SETTINGS_USER_ID_PROPERTY = USER_ID_PROPERTY;
public static final String USER_AUTH_SETTINGS_TWO_FA_ACCOUNT_CONFIG_PROPERTY = "mfa_account_config";
public static final String USER_AUTH_SETTINGS_TWO_FA_SETTINGS = "two_fa_settings";
/**
* Cassandra attributes and timeseries constants.

15
dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java

@ -25,7 +25,8 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.UserAuthSettingsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
@ -47,8 +48,8 @@ public class UserAuthSettingsEntity extends BaseSqlEntity<UserAuthSettings> impl
@Column(name = ModelConstants.USER_AUTH_SETTINGS_USER_ID_PROPERTY, nullable = false, unique = true)
private UUID userId;
@Type(type = "json")
@Column(name = ModelConstants.USER_AUTH_SETTINGS_TWO_FA_ACCOUNT_CONFIG_PROPERTY)
private JsonNode twoFaAccountConfig;
@Column(name = ModelConstants.USER_AUTH_SETTINGS_TWO_FA_SETTINGS)
private JsonNode twoFaSettings;
public UserAuthSettingsEntity(UserAuthSettings userAuthSettings) {
if (userAuthSettings.getId() != null) {
@ -58,8 +59,8 @@ public class UserAuthSettingsEntity extends BaseSqlEntity<UserAuthSettings> impl
if (userAuthSettings.getUserId() != null) {
this.userId = userAuthSettings.getUserId().getId();
}
if (userAuthSettings.getTwoFaAccountConfig() != null) {
this.twoFaAccountConfig = JacksonUtil.valueToTree(userAuthSettings.getTwoFaAccountConfig());
if (userAuthSettings.getTwoFaSettings() != null) {
this.twoFaSettings = JacksonUtil.valueToTree(userAuthSettings.getTwoFaSettings());
}
}
@ -71,8 +72,8 @@ public class UserAuthSettingsEntity extends BaseSqlEntity<UserAuthSettings> impl
if (userId != null) {
userAuthSettings.setUserId(new UserId(userId));
}
if (twoFaAccountConfig != null) {
userAuthSettings.setTwoFaAccountConfig(JacksonUtil.treeToValue(twoFaAccountConfig, TwoFactorAuthAccountConfig.class));
if (twoFaSettings != null) {
userAuthSettings.setTwoFaSettings(JacksonUtil.treeToValue(twoFaSettings, AccountTwoFaSettings.class));
}
return userAuthSettings;
}

2
dao/src/main/resources/sql/schema-entities.sql

@ -699,5 +699,5 @@ CREATE TABLE IF NOT EXISTS user_auth_settings (
id uuid NOT NULL CONSTRAINT user_auth_settings_pkey PRIMARY KEY,
created_time bigint NOT NULL,
user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id),
mfa_account_config varchar
two_fa_settings varchar
);

Loading…
Cancel
Save