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, id uuid NOT NULL CONSTRAINT user_auth_settings_pkey PRIMARY KEY,
created_time bigint NOT NULL, created_time bigint NOT NULL,
user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id), 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 com.google.zxing.qrcode.QRCodeWriter;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; 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.common.util.JacksonUtil;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; 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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
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.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.ServletOutputStream; import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -57,9 +59,9 @@ import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
@RequestMapping("/api/2fa") @RequestMapping("/api/2fa")
@TbCoreComponent @TbCoreComponent
@RequiredArgsConstructor @RequiredArgsConstructor
public class TwoFactorAuthConfigController extends BaseController { public class TwoFaConfigController extends BaseController {
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager; private final TwoFaConfigManager twoFaConfigManager;
private final TwoFactorAuthService twoFactorAuthService; private final TwoFactorAuthService twoFactorAuthService;
@ -71,27 +73,17 @@ public class TwoFactorAuthConfigController extends BaseController {
"```\n{\n" + "```\n{\n" +
" \"providerType\": \"TOTP\",\n" + " \"providerType\": \"TOTP\",\n" +
" \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\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 + "Response example for SMS 2FA: " + NEW_LINE +
"```\n{\n" + "```\n{\n" +
" \"providerType\": \"SMS\",\n" + " \"providerType\": \"SMS\",\n" +
" \"phoneNumber\": \"+380505005050\"\n" + " \"phoneNumber\": \"+380505005050\"\n" +
"}\n```") "}\n```")
@GetMapping("/account/config") @GetMapping("/account/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public TwoFactorAuthAccountConfig getTwoFaAccountConfig() throws ThingsboardException { public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null); return twoFaConfigManager.getAccountTwoFaSettings(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());
} }
@ -116,19 +108,19 @@ public class TwoFactorAuthConfigController extends BaseController {
"}\n```") "}\n```")
@PostMapping("/account/config/generate") @PostMapping("/account/config/generate")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @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) public TwoFaAccountConfig generateTwoFaAccountConfig(@ApiParam(value = "2FA provider type to generate new account config for", defaultValue = "TOTP", required = true)
@RequestParam TwoFactorAuthProviderType providerType) throws Exception { @RequestParam TwoFaProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
return twoFactorAuthService.generateNewAccountConfig(user, providerType); return twoFactorAuthService.generateNewAccountConfig(user, providerType);
} }
/* TMP */ /* TMP */
@PostMapping("/account/config/generate/qr") @PostMapping("/account/config/tmp/generate/qr")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void generateTwoFaAccountConfigWithQr(@RequestParam TwoFactorAuthProviderType providerType, HttpServletResponse response) throws Exception { public void generateTwoFaAccountConfigWithQr(@RequestParam TwoFaProviderType providerType, HttpServletResponse response) throws Exception {
TwoFactorAuthAccountConfig config = generateTwoFaAccountConfig(providerType); TwoFaAccountConfig config = generateTwoFaAccountConfig(providerType);
if (providerType == TwoFactorAuthProviderType.TOTP) { if (providerType == TwoFaProviderType.TOTP) {
BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFactorAuthAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200); BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFaAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200);
try (ServletOutputStream outputStream = response.getOutputStream()) { try (ServletOutputStream outputStream = response.getOutputStream()) {
MatrixToImageWriter.writeToStream(qr, "PNG", outputStream); MatrixToImageWriter.writeToStream(qr, "PNG", outputStream);
} }
@ -148,7 +140,7 @@ public class TwoFactorAuthConfigController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @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. " + 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) "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(); SecurityUser user = getCurrentUser();
twoFactorAuthService.prepareVerificationCode(user, accountConfig, false); twoFactorAuthService.prepareVerificationCode(user, accountConfig, false);
} }
@ -160,36 +152,57 @@ public class TwoFactorAuthConfigController extends BaseController {
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PostMapping("/account/config") @PostMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @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) public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@ApiParam(value = "2FA account config to save. Validation rules are the same as in submitTwoFaAccountConfig API method", required = true)
@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig, @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) @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 { @RequestParam String verificationCode) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false);
if (verificationSuccess) { if (verificationSuccess) {
twoFactorAuthConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
} else { } else {
throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.INVALID_ARGUMENTS); 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)", @ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)",
notes = "Delete user's 2FA config. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) notes = "Delete user's 2FA config. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@DeleteMapping("/account/config") @DeleteMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void deleteTwoFaAccountConfig() throws ThingsboardException { public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException {
SecurityUser user = getCurrentUser(); 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." + notes = "Get settings for 2FA. If 2FA is not configured, then an empty response will be returned." +
ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/settings") @GetMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public TwoFactorAuthSettings getTwoFaSettings() throws ThingsboardException { public PlatformTwoFaSettings getPlatformTwoFaSettings() throws ThingsboardException {
return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId(), false).orElse(null); return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), false).orElse(null);
} }
@ApiOperation(value = "Save 2FA settings (saveTwoFaSettings)", @ApiOperation(value = "Save 2FA settings (saveTwoFaSettings)",
@ -198,9 +211,15 @@ public class TwoFactorAuthConfigController extends BaseController {
ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/settings") @PostMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void saveTwoFaSettings(@ApiParam(value = "Settings value", required = true) public void savePlatformTwoFaSettings(@ApiParam(value = "Settings value", required = true)
@RequestBody TwoFactorAuthSettings twoFaSettings) throws ThingsboardException { @RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException {
twoFactorAuthConfigManager.saveTwoFaSettings(getTenantId(), twoFaSettings); 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.ApiOperation;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; 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.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser; 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 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; import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
@RestController @RestController
@ -50,7 +56,7 @@ import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
public class TwoFactorAuthController extends BaseController { public class TwoFactorAuthController extends BaseController {
private final TwoFactorAuthService twoFactorAuthService; private final TwoFactorAuthService twoFactorAuthService;
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager; private final TwoFaConfigManager twoFaConfigManager;
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final UserService userService; private final UserService userService;
@ -65,9 +71,9 @@ public class TwoFactorAuthController extends BaseController {
"and Too Many Requests error if rate limits are exceeded.") "and Too Many Requests error if rate limits are exceeded.")
@PostMapping("/verification/send") @PostMapping("/verification/send")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public void requestTwoFaVerificationCode() throws Exception { public void requestTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
twoFactorAuthService.prepareVerificationCode(user, true); twoFactorAuthService.prepareVerificationCode(user, providerType, true);
} }
@ApiOperation(value = "Check 2FA verification code (checkTwoFaVerificationCode)", @ApiOperation(value = "Check 2FA verification code (checkTwoFaVerificationCode)",
@ -79,9 +85,10 @@ public class TwoFactorAuthController extends BaseController {
@PostMapping("/verification/check") @PostMapping("/verification/check")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public JwtTokenPair checkTwoFaVerificationCode(@ApiParam(value = "6-digit verification code", required = true) public JwtTokenPair checkTwoFaVerificationCode(@ApiParam(value = "6-digit verification code", required = true)
@RequestParam TwoFaProviderType providerType,
@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception { @RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
if (verificationSuccess) { if (verificationSuccess) {
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null); systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null);
user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal()); 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')") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public TwoFactorAuthProviderType getCurrentlyUsedTwoFaProviderType() throws ThingsboardException { public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()) return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId())
.map(TwoFactorAuthAccountConfig::getProviderType).orElse(null); .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.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; 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.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -49,117 +49,129 @@ import java.util.concurrent.ConcurrentMap;
@TbCoreComponent @TbCoreComponent
public class DefaultTwoFactorAuthService implements TwoFactorAuthService { public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
private final TwoFactorAuthConfigManager configManager; private final TwoFaConfigManager configManager;
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final UserService userService; private final UserService userService;
private final Map<TwoFactorAuthProviderType, TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>> providers = new EnumMap<>(TwoFactorAuthProviderType.class); private final Map<TwoFaProviderType, TwoFaProvider<TwoFaProviderConfig, TwoFaAccountConfig>> providers = new EnumMap<>(TwoFaProviderType.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 static final ThingsboardException ACCOUNT_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS); 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_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 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 @Override
public void prepareVerificationCode(SecurityUser securityUser, boolean checkLimits) throws Exception { public void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception {
TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()) TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType)
.orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR);
prepareVerificationCode(securityUser, accountConfig, checkLimits); prepareVerificationCode(user, accountConfig, checkLimits);
} }
@Override @Override
public void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException { public void prepareVerificationCode(SecurityUser user, TwoFaAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException {
TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId(), true) PlatformTwoFaSettings twoFaSettings = configManager.getPlatformTwoFaSettings(user.getTenantId(), true)
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
if (checkLimits) { if (checkLimits) {
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { checkRateLimits(user.getId(), accountConfig.getProviderType(), twoFaSettings.getVerificationCodeSendRateLimit(), verificationCodeSendingRateLimits);
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);
}
}
} }
TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
getTwoFaProvider(accountConfig.getProviderType()).prepareVerificationCode(securityUser, providerConfig, accountConfig); getTwoFaProvider(accountConfig.getProviderType()).prepareVerificationCode(user, providerConfig, accountConfig);
} }
@Override @Override
public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean checkLimits) throws ThingsboardException { public boolean checkVerificationCode(SecurityUser user, TwoFaProviderType providerType, String verificationCode, boolean checkLimits) throws ThingsboardException {
TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()) TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType)
.orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR);
return checkVerificationCode(securityUser, verificationCode, accountConfig, checkLimits); return checkVerificationCode(user, verificationCode, accountConfig, checkLimits);
} }
@Override @Override
public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException { public boolean checkVerificationCode(SecurityUser user, String verificationCode, TwoFaAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException {
if (!userService.findUserCredentialsByUserId(securityUser.getTenantId(), securityUser.getId()).isEnabled()) { if (!userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()).isEnabled()) {
throw new ThingsboardException("User is disabled", ThingsboardErrorCode.AUTHENTICATION); 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); .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
if (checkLimits) { if (checkLimits) {
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) { checkRateLimits(user.getId(), accountConfig.getProviderType(), twoFaSettings.getVerificationCodeCheckRateLimit(), verificationCodeCheckingRateLimits);
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);
}
}
} }
TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
boolean verificationSuccess; boolean verificationSuccess;
if (StringUtils.isNumeric(verificationCode) && verificationCode.length() == 6) { 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 { } else {
verificationSuccess = false; verificationSuccess = false;
} }
if (checkLimits) { if (checkLimits) {
try { try {
systemSecurityService.validateTwoFaVerification(securityUser, verificationSuccess, twoFaSettings); systemSecurityService.validateTwoFaVerification(user, verificationSuccess, twoFaSettings);
} catch (LockedException e) { } catch (LockedException e) {
verificationCodeCheckingRateLimits.remove(securityUser.getId()); verificationCodeCheckingRateLimits.remove(user.getId());
verificationCodeSendingRateLimits.remove(securityUser.getId()); verificationCodeSendingRateLimits.remove(user.getId());
throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.AUTHENTICATION); throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.AUTHENTICATION);
} }
if (verificationSuccess) { if (verificationSuccess) {
verificationCodeCheckingRateLimits.remove(securityUser.getId()); verificationCodeCheckingRateLimits.remove(user.getId());
verificationCodeSendingRateLimits.remove(securityUser.getId()); verificationCodeSendingRateLimits.remove(user.getId());
} }
} }
return verificationSuccess; 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 @Override
public TwoFactorAuthAccountConfig generateNewAccountConfig(User user, TwoFactorAuthProviderType providerType) throws ThingsboardException { public TwoFaAccountConfig generateNewAccountConfig(User user, TwoFaProviderType providerType) throws ThingsboardException {
TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(user.getTenantId(), providerType); TwoFaProviderConfig providerConfig = getTwoFaProviderConfig(user.getTenantId(), providerType);
return getTwoFaProvider(providerType).generateNewAccountConfig(user, providerConfig); return getTwoFaProvider(providerType).generateNewAccountConfig(user, providerConfig);
} }
private TwoFactorAuthProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) throws ThingsboardException { private TwoFaProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException {
return configManager.getTwoFaSettings(tenantId, true) return configManager.getPlatformTwoFaSettings(tenantId, true)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)) .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType))
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); .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)) return Optional.ofNullable(providers.get(providerType))
.orElseThrow(() -> PROVIDER_NOT_AVAILABLE_ERROR); .orElseThrow(() -> PROVIDER_NOT_AVAILABLE_ERROR);
} }
@Autowired @Autowired
private void setProviders(Collection<TwoFactorAuthProvider> providers) { private void setProviders(Collection<TwoFaProvider> providers) {
providers.forEach(provider -> { providers.forEach(provider -> {
this.providers.put(provider.getType(), 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.User;
import org.thingsboard.server.common.data.exception.ThingsboardException; 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.id.TenantId;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; 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; import org.thingsboard.server.service.security.model.SecurityUser;
public interface TwoFactorAuthService { 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.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.DataConstants; 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.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.security.UserAuthSettings; 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.PlatformTwoFaSettings;
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.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; 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.attributes.AttributesService;
import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.settings.AdminSettingsDao; 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 org.thingsboard.server.dao.user.UserAuthSettingsDao;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigManager { public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
private final UserAuthSettingsDao userAuthSettingsDao; private final UserAuthSettingsDao userAuthSettingsDao;
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
@ -55,60 +57,77 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan
@Override @Override
public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) { public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId) {
return getTwoFaAccountConfig(tenantId, userId).isPresent(); 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 @Override
public Optional<TwoFactorAuthAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId) { public Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) return getAccountTwoFaSettings(tenantId, userId)
.flatMap(userAuthSettings -> Optional.ofNullable(userAuthSettings.getTwoFaAccountConfig())) .map(AccountTwoFaSettings::getConfigs)
.filter(twoFaAccountConfig -> getTwoFaProviderConfig(tenantId, twoFaAccountConfig.getProviderType()).isPresent()); .flatMap(configs -> Optional.ofNullable(configs.get(providerType)));
} }
@Override @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()) 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)) UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
.orElseGet(() -> { .orElseGet(() -> {
UserAuthSettings newUserAuthSettings = new UserAuthSettings(); UserAuthSettings newUserAuthSettings = new UserAuthSettings();
newUserAuthSettings.setUserId(userId); newUserAuthSettings.setUserId(userId);
AccountTwoFaSettings newAccountTwoFaSettings = new AccountTwoFaSettings();
newAccountTwoFaSettings.setConfigs(new LinkedHashMap<>());
newUserAuthSettings.setTwoFaSettings(newAccountTwoFaSettings);
return newUserAuthSettings; return newUserAuthSettings;
}); });
userAuthSettings.setTwoFaAccountConfig(accountConfig); updater.accept(userAuthSettings.getTwoFaSettings());
userAuthSettingsDao.save(tenantId, userAuthSettings); userAuthSettingsDao.save(tenantId, userAuthSettings);
} return userAuthSettings.getTwoFaSettings();
@Override
public void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId) {
Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
.ifPresent(userAuthSettings -> {
userAuthSettings.setTwoFaAccountConfig(null);
userAuthSettingsDao.save(tenantId, userAuthSettings);
});
} }
private Optional<TwoFactorAuthProviderConfig> getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) { private Optional<TwoFaProviderConfig> getTwoFaProviderConfig(TenantId tenantId, TwoFaProviderType providerType) {
return getTwoFaSettings(tenantId, true) return getPlatformTwoFaSettings(tenantId, true)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)); .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType));
} }
@SneakyThrows({InterruptedException.class, ExecutionException.class}) @SneakyThrows({InterruptedException.class, ExecutionException.class})
@Override @Override
public Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault) { public Optional<PlatformTwoFaSettings> getPlatformTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault) {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) { if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, TWO_FACTOR_AUTH_SETTINGS_KEY)) return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, TWO_FACTOR_AUTH_SETTINGS_KEY))
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TwoFactorAuthSettings.class)); .map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), PlatformTwoFaSettings.class));
} else { } else {
Optional<TwoFactorAuthSettings> tenantTwoFaSettings = attributesService.find(TenantId.SYS_TENANT_ID, tenantId, Optional<PlatformTwoFaSettings> tenantTwoFaSettings = attributesService.find(TenantId.SYS_TENANT_ID, tenantId,
DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get() DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get()
.map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class)); .map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), PlatformTwoFaSettings.class));
if (sysadminSettingsAsDefault) { if (sysadminSettingsAsDefault) {
if (tenantTwoFaSettings.isEmpty() || tenantTwoFaSettings.get().isUseSystemTwoFactorAuthSettings()) { if (tenantTwoFaSettings.isEmpty() || tenantTwoFaSettings.get().isUseSystemTwoFactorAuthSettings()) {
return getTwoFaSettings(TenantId.SYS_TENANT_ID, false); return getPlatformTwoFaSettings(TenantId.SYS_TENANT_ID, false);
} }
} }
return tenantTwoFaSettings; return tenantTwoFaSettings;
@ -117,7 +136,7 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan
@SneakyThrows({InterruptedException.class, ExecutionException.class}) @SneakyThrows({InterruptedException.class, ExecutionException.class})
@Override @Override
public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) { public void savePlatformTwoFaSettings(TenantId tenantId, PlatformTwoFaSettings twoFactorAuthSettings) {
if (tenantId.equals(TenantId.SYS_TENANT_ID) || !twoFactorAuthSettings.isUseSystemTwoFactorAuthSettings()) { if (tenantId.equals(TenantId.SYS_TENANT_ID) || !twoFactorAuthSettings.isUseSystemTwoFactorAuthSettings()) {
ConstraintValidator.validateFields(twoFactorAuthSettings); ConstraintValidator.validateFields(twoFactorAuthSettings);
} }
@ -139,7 +158,7 @@ public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigMan
@SneakyThrows({InterruptedException.class, ExecutionException.class}) @SneakyThrows({InterruptedException.class, ExecutionException.class})
@Override @Override
public void deleteTwoFaSettings(TenantId tenantId) { public void deletePlatformTwoFaSettings(TenantId tenantId) {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) { if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.ifPresent(adminSettings -> adminSettingsDao.removeById(tenantId, adminSettings.getId().getId())); .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.User;
import org.thingsboard.server.common.data.exception.ThingsboardException; 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.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
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.service.security.model.SecurityUser; 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); 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); 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.springframework.cache.CacheManager;
import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.exception.ThingsboardException; 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.account.OtpBasedTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.OtpBasedTwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.OtpBasedTwoFaProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.concurrent.TimeUnit; 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; private final Cache verificationCodesCache;
protected OtpBasedTwoFactorAuthProvider(CacheManager cacheManager) { protected OtpBasedTwoFaProvider(CacheManager cacheManager) {
this.verificationCodesCache = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE); 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 { public static class Otp {
private final long timestamp; private final long timestamp;
private final String value; 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.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException; 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.account.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig;
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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -31,23 +31,23 @@ import java.util.Map;
@Service @Service
@TbCoreComponent @TbCoreComponent
public class SmsTwoFactorAuthProvider extends OtpBasedTwoFactorAuthProvider<SmsTwoFactorAuthProviderConfig, SmsTwoFactorAuthAccountConfig> { public class SmsTwoFaProvider extends OtpBasedTwoFaProvider<SmsTwoFaProviderConfig, SmsTwoFaAccountConfig> {
private final SmsService smsService; private final SmsService smsService;
public SmsTwoFactorAuthProvider(CacheManager cacheManager, SmsService smsService) { public SmsTwoFaProvider(CacheManager cacheManager, SmsService smsService) {
super(cacheManager); super(cacheManager);
this.smsService = smsService; this.smsService = smsService;
} }
@Override @Override
public SmsTwoFactorAuthAccountConfig generateNewAccountConfig(User user, SmsTwoFactorAuthProviderConfig providerConfig) { public SmsTwoFaAccountConfig generateNewAccountConfig(User user, SmsTwoFaProviderConfig providerConfig) {
return new SmsTwoFactorAuthAccountConfig(); return new SmsTwoFaAccountConfig();
} }
@Override @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( Map<String, String> messageData = Map.of(
"verificationCode", verificationCode, "verificationCode", verificationCode,
"userEmail", user.getEmail() "userEmail", user.getEmail()
@ -60,8 +60,8 @@ public class SmsTwoFactorAuthProvider extends OtpBasedTwoFactorAuthProvider<SmsT
@Override @Override
public TwoFactorAuthProviderType getType() { public TwoFaProviderType getType() {
return TwoFactorAuthProviderType.SMS; 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.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.server.common.data.User; 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.queue.util.TbCoreComponent;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFaProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@TbCoreComponent @TbCoreComponent
public class TotpTwoFactorAuthProvider implements TwoFactorAuthProvider<TotpTwoFactorAuthProviderConfig, TotpTwoFactorAuthAccountConfig> { public class TotpTwoFaProvider implements TwoFaProvider<TotpTwoFaProviderConfig, TotpTwoFaAccountConfig> {
@Override @Override
public final TotpTwoFactorAuthAccountConfig generateNewAccountConfig(User user, TotpTwoFactorAuthProviderConfig providerConfig) { public final TotpTwoFaAccountConfig generateNewAccountConfig(User user, TotpTwoFaProviderConfig providerConfig) {
TotpTwoFactorAuthAccountConfig config = new TotpTwoFactorAuthAccountConfig(); TotpTwoFaAccountConfig config = new TotpTwoFaAccountConfig();
String secretKey = generateSecretKey(); String secretKey = generateSecretKey();
config.setAuthUrl(getTotpAuthUrl(user, secretKey, providerConfig)); config.setAuthUrl(getTotpAuthUrl(user, secretKey, providerConfig));
return config; return config;
} }
@Override @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"); String secretKey = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build().getQueryParams().getFirst("secret");
return new Totp(secretKey).verify(verificationCode); return new Totp(secretKey).verify(verificationCode);
} }
@SneakyThrows @SneakyThrows
private String getTotpAuthUrl(User user, String secretKey, TotpTwoFactorAuthProviderConfig providerConfig) { private String getTotpAuthUrl(User user, String secretKey, TotpTwoFaProviderConfig providerConfig) {
URIBuilder uri = new URIBuilder() URIBuilder uri = new URIBuilder()
.setScheme("otpauth") .setScheme("otpauth")
.setHost("totp") .setHost("totp")
@ -67,8 +67,8 @@ public class TotpTwoFactorAuthProvider implements TwoFactorAuthProvider<TotpTwoF
@Override @Override
public TwoFactorAuthProviderType getType() { public TwoFaProviderType getType() {
return TwoFactorAuthProviderType.TOTP; 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.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; 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.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -54,17 +54,17 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
private final UserService userService; private final UserService userService;
private final CustomerService customerService; private final CustomerService customerService;
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager; private final TwoFactorAuthService twoFactorAuthService;
@Autowired @Autowired
public RestAuthenticationProvider(final UserService userService, public RestAuthenticationProvider(final UserService userService,
final CustomerService customerService, final CustomerService customerService,
final SystemSecurityService systemSecurityService, final SystemSecurityService systemSecurityService,
TwoFactorAuthConfigManager twoFactorAuthConfigManager) { TwoFactorAuthService twoFactorAuthService) {
this.userService = userService; this.userService = userService;
this.customerService = customerService; this.customerService = customerService;
this.systemSecurityService = systemSecurityService; this.systemSecurityService = systemSecurityService;
this.twoFactorAuthConfigManager = twoFactorAuthConfigManager; this.twoFactorAuthService = twoFactorAuthService;
} }
@Override @Override
@ -82,7 +82,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
String username = userPrincipal.getValue(); String username = userPrincipal.getValue();
String password = (String) authentication.getCredentials(); String password = (String) authentication.getCredentials();
securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password); securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
if (twoFactorAuthConfigManager.isTwoFaEnabled(securityUser.getTenantId(), securityUser.getId())) { if (twoFactorAuthService.isTwoFaEnabled(securityUser.getTenantId(), securityUser.getId())) {
return new MfaAuthenticationToken(securityUser); return new MfaAuthenticationToken(securityUser);
} else { } else {
systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null); 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.common.data.security.Authority;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken; import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; 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.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -44,7 +44,7 @@ import java.util.concurrent.TimeUnit;
public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper mapper; private final ObjectMapper mapper;
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager; private final TwoFaConfigManager twoFaConfigManager;
private final RefreshTokenRepository refreshTokenRepository; private final RefreshTokenRepository refreshTokenRepository;
@Override @Override
@ -54,7 +54,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
JwtTokenPair tokenPair = new JwtTokenPair(); JwtTokenPair tokenPair = new JwtTokenPair();
if (authentication instanceof MfaAuthenticationToken) { 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)); .flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())).orElse((int) TimeUnit.MINUTES.toSeconds(30));
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken()); tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
tokenPair.setRefreshToken(null); 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.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl; 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.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -160,7 +160,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
} }
@Override @Override
public void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings) { public void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, PlatformTwoFaSettings twoFaSettings) {
TenantId tenantId = securityUser.getTenantId(); TenantId tenantId = securityUser.getTenantId();
UserId userId = securityUser.getId(); 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.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.dao.exception.DataValidationException; 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 org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest; 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 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; 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.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFactorAuthAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFactorAuthAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderType; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.impl.OtpBasedTwoFactorAuthProvider; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFactorAuthProvider; 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.Arrays;
import java.util.Collections; import java.util.Collections;
@ -58,13 +59,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@SpyBean @SpyBean
private TotpTwoFactorAuthProvider totpTwoFactorAuthProvider; private TotpTwoFaProvider totpTwoFactorAuthProvider;
@MockBean @MockBean
private SmsService smsService; private SmsService smsService;
@Autowired @Autowired
private CacheManager cacheManager; private CacheManager cacheManager;
@Autowired @Autowired
private TwoFactorAuthConfigManager twoFactorAuthConfigManager; private TwoFaConfigManager twoFaConfigManager;
@Autowired
private TwoFactorAuthService twoFactorAuthService;
@Before @Before
public void beforeEach() throws Exception { public void beforeEach() throws Exception {
@ -73,8 +76,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@After @After
public void afterEach() { public void afterEach() {
twoFactorAuthConfigManager.deleteTwoFaSettings(TenantId.SYS_TENANT_ID); twoFaConfigManager.deletePlatformTwoFaSettings(TenantId.SYS_TENANT_ID);
twoFactorAuthConfigManager.deleteTwoFaSettings(tenantId); twoFaConfigManager.deletePlatformTwoFaSettings(tenantId);
} }
@ -88,13 +91,13 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
} }
private void testSaveTestTwoFaSettings() throws Exception { private void testSaveTestTwoFaSettings() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb"); totpTwoFaProviderConfig.setIssuerName("tb");
SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}");
smsTwoFaProviderConfig.setVerificationCodeLifetime(60); smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig)); twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig));
twoFaSettings.setVerificationCodeSendRateLimit("1:60"); twoFaSettings.setVerificationCodeSendRateLimit("1:60");
twoFaSettings.setVerificationCodeCheckRateLimit("3:900"); twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
@ -103,7 +106,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); 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()).hasSize(2);
assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig); assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig);
@ -113,7 +116,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
public void testSaveTwoFaSettings_validationError() throws Exception { public void testSaveTwoFaSettings_validationError() throws Exception {
loginTenantAdmin(); loginTenantAdmin();
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Collections.emptyList()); twoFaSettings.setProviders(Collections.emptyList());
twoFaSettings.setVerificationCodeSendRateLimit("ab:aba"); twoFaSettings.setVerificationCodeSendRateLimit("ab:aba");
twoFaSettings.setVerificationCodeCheckRateLimit("0:12"); twoFaSettings.setVerificationCodeCheckRateLimit("0:12");
@ -146,19 +149,19 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test @Test
public void testGetTwoFaSettings_useSysadminSettingsAsDefault() throws Exception { public void testGetTwoFaSettings_useSysadminSettingsAsDefault() throws Exception {
loginSysAdmin(); loginSysAdmin();
TwoFactorAuthSettings sysadminTwoFaSettings = new TwoFactorAuthSettings(); PlatformTwoFaSettings sysadminTwoFaSettings = new PlatformTwoFaSettings();
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb"); totpTwoFaProviderConfig.setIssuerName("tb");
sysadminTwoFaSettings.setProviders(Collections.singletonList(totpTwoFaProviderConfig)); sysadminTwoFaSettings.setProviders(Collections.singletonList(totpTwoFaProviderConfig));
sysadminTwoFaSettings.setMaxVerificationFailuresBeforeUserLockout(25); sysadminTwoFaSettings.setMaxVerificationFailuresBeforeUserLockout(25);
doPost("/api/2fa/settings", sysadminTwoFaSettings).andExpect(status().isOk()); doPost("/api/2fa/settings", sysadminTwoFaSettings).andExpect(status().isOk());
loginTenantAdmin(); loginTenantAdmin();
TwoFactorAuthSettings tenantTwoFaSettings = new TwoFactorAuthSettings(); PlatformTwoFaSettings tenantTwoFaSettings = new PlatformTwoFaSettings();
tenantTwoFaSettings.setUseSystemTwoFactorAuthSettings(true); tenantTwoFaSettings.setUseSystemTwoFactorAuthSettings(true);
tenantTwoFaSettings.setProviders(Collections.emptyList()); tenantTwoFaSettings.setProviders(Collections.emptyList());
doPost("/api/2fa/settings", tenantTwoFaSettings).andExpect(status().isOk()); 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); assertThat(twoFaSettings).isEqualTo(tenantTwoFaSettings);
doPost("/api/2fa/account/config/generate?providerType=TOTP") doPost("/api/2fa/account/config/generate?providerType=TOTP")
@ -168,7 +171,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
tenantTwoFaSettings.setProviders(Collections.emptyList()); tenantTwoFaSettings.setProviders(Collections.emptyList());
tenantTwoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); tenantTwoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
doPost("/api/2fa/settings", tenantTwoFaSettings).andExpect(status().isOk()); 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(twoFaSettings).isEqualTo(tenantTwoFaSettings);
assertThat(getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=TOTP") assertThat(getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=TOTP")
@ -192,13 +195,13 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
.andExpect(status().isOk()); .andExpect(status().isOk());
loginSysAdmin(); 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); assertThat(twoFaSettings).isEqualTo(sysadminTwoFaSettings);
} }
@Test @Test
public void testSaveTotpTwoFaProviderConfig_validationError() throws Exception { public void testSaveTotpTwoFaProviderConfig_validationError() throws Exception {
TotpTwoFactorAuthProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); TotpTwoFaProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
invalidTotpTwoFaProviderConfig.setIssuerName(" "); invalidTotpTwoFaProviderConfig.setIssuerName(" ");
String errorResponse = saveTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig); String errorResponse = saveTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig);
@ -207,7 +210,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test @Test
public void testSaveSmsTwoFaProviderConfig_validationError() throws Exception { public void testSaveSmsTwoFaProviderConfig_validationError() throws Exception {
SmsTwoFactorAuthProviderConfig invalidSmsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); SmsTwoFaProviderConfig invalidSmsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code"); invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code");
invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(60); invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(60);
@ -221,8 +224,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
assertThat(errorResponse).containsIgnoringCase("verification code lifetime is required"); assertThat(errorResponse).containsIgnoringCase("verification code lifetime is required");
} }
private String saveTwoFaSettingsAndGetError(TwoFactorAuthProviderConfig invalidTwoFaProviderConfig) throws Exception { private String saveTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception {
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig));
return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
@ -235,12 +238,12 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
loginTenantAdmin(); loginTenantAdmin();
TwoFactorAuthProviderType notConfiguredProviderType = TwoFactorAuthProviderType.TOTP; TwoFaProviderType notConfiguredProviderType = TwoFaProviderType.TOTP;
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=" + notConfiguredProviderType) String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=" + notConfiguredProviderType)
.andExpect(status().isBadRequest())); .andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("provider is not configured"); 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"); notConfiguredProviderAccountConfig.setAuthUrl("otpauth://totp/aba:aba?issuer=aba&secret=ABA");
errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", notConfiguredProviderAccountConfig)); errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", notConfiguredProviderAccountConfig));
assertThat(errorMessage).containsIgnoringCase("provider is not configured"); assertThat(errorMessage).containsIgnoringCase("provider is not configured");
@ -248,7 +251,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test @Test
public void testGenerateTotpTwoFaAccountConfig() throws Exception { public void testGenerateTotpTwoFaAccountConfig() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin(); loginTenantAdmin();
@ -258,11 +261,11 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test @Test
public void testSubmitTotpTwoFaAccountConfig() throws Exception { public void testSubmitTotpTwoFaAccountConfig() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin(); loginTenantAdmin();
TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); TotpTwoFaAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
doPost("/api/2fa/account/config/submit", generatedTotpTwoFaAccountConfig).andExpect(status().isOk()); doPost("/api/2fa/account/config/submit", generatedTotpTwoFaAccountConfig).andExpect(status().isOk());
verify(totpTwoFactorAuthProvider).prepareVerificationCode(argThat(user -> user.getEmail().equals(TENANT_ADMIN_EMAIL)), verify(totpTwoFactorAuthProvider).prepareVerificationCode(argThat(user -> user.getEmail().equals(TENANT_ADMIN_EMAIL)),
eq(totpTwoFaProviderConfig), eq(generatedTotpTwoFaAccountConfig)); eq(totpTwoFaProviderConfig), eq(generatedTotpTwoFaAccountConfig));
@ -274,7 +277,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
loginTenantAdmin(); loginTenantAdmin();
TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = new TotpTwoFactorAuthAccountConfig(); TotpTwoFaAccountConfig totpTwoFaAccountConfig = new TotpTwoFaAccountConfig();
totpTwoFaAccountConfig.setAuthUrl(null); totpTwoFaAccountConfig.setAuthUrl(null);
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig) String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig)
@ -293,11 +296,11 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test @Test
public void testVerifyAndSaveTotpTwoFaAccountConfig() throws Exception { public void testVerifyAndSaveTotpTwoFaAccountConfig() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin(); loginTenantAdmin();
TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); TotpTwoFaAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
String secret = UriComponentsBuilder.fromUriString(generatedTotpTwoFaAccountConfig.getAuthUrl()).build() String secret = UriComponentsBuilder.fromUriString(generatedTotpTwoFaAccountConfig.getAuthUrl()).build()
.getQueryParams().getFirst("secret"); .getQueryParams().getFirst("secret");
@ -306,17 +309,17 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig) doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig)
.andExpect(status().isOk()); .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); assertThat(twoFaAccountConfig).isEqualTo(generatedTotpTwoFaAccountConfig);
} }
@Test @Test
public void testVerifyAndSaveTotpTwoFaAccountConfig_incorrectVerificationCode() throws Exception { public void testVerifyAndSaveTotpTwoFaAccountConfig_incorrectVerificationCode() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider(); TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin(); loginTenantAdmin();
TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig); TotpTwoFaAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
String incorrectVerificationCode = "100000"; String incorrectVerificationCode = "100000";
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + incorrectVerificationCode, generatedTotpTwoFaAccountConfig) 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"); assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
} }
private TotpTwoFactorAuthAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig) throws Exception { private TotpTwoFaAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFaProviderConfig totpTwoFaProviderConfig) throws Exception {
TwoFactorAuthAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP") TwoFaAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP")
.andExpect(status().isOk()), TwoFactorAuthAccountConfig.class); .andExpect(status().isOk()), TwoFaAccountConfig.class);
assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFactorAuthAccountConfig.class); assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFaAccountConfig.class);
assertThat(((TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> { assertThat(((TotpTwoFaAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> {
UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build(); UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build();
assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth"); assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth");
assertThat(otpAuthUrl.getHost()).isEqualTo("totp"); assertThat(otpAuthUrl.getHost()).isEqualTo("totp");
@ -340,14 +343,14 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
assertDoesNotThrow(() -> Base32.decode(secretKey)); assertDoesNotThrow(() -> Base32.decode(secretKey));
}); });
}); });
return (TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig; return (TotpTwoFaAccountConfig) generatedTwoFaAccountConfig;
} }
@Test @Test
public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception { public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception {
testVerifyAndSaveTotpTwoFaAccountConfig(); testVerifyAndSaveTotpTwoFaAccountConfig();
assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()),
TotpTwoFactorAuthAccountConfig.class)).isNotNull(); TotpTwoFaAccountConfig.class)).isNotNull();
loginSysAdmin(); loginSysAdmin();
@ -371,13 +374,13 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
loginTenantAdmin(); loginTenantAdmin();
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38054159785"); smsTwoFaAccountConfig.setPhoneNumber("+38054159785");
doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig).andExpect(status().isOk()); doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig).andExpect(status().isOk());
String verificationCode = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE) 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 -> { verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> {
return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber()); return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber());
@ -388,7 +391,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
public void testSubmitSmsTwoFaAccountConfig_validationError() throws Exception { public void testSubmitSmsTwoFaAccountConfig_validationError() throws Exception {
configureSmsTwoFaProvider("${verificationCode}"); configureSmsTwoFaProvider("${verificationCode}");
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
String blankPhoneNumber = ""; String blankPhoneNumber = "";
smsTwoFaAccountConfig.setPhoneNumber(blankPhoneNumber); smsTwoFaAccountConfig.setPhoneNumber(blankPhoneNumber);
@ -410,7 +413,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
loginTenantAdmin(); loginTenantAdmin();
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38051889445"); smsTwoFaAccountConfig.setPhoneNumber("+38051889445");
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class); 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) doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, smsTwoFaAccountConfig)
.andExpect(status().isOk()); .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); assertThat(accountConfig).isEqualTo(smsTwoFaAccountConfig);
} }
@ -434,7 +437,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
loginTenantAdmin(); loginTenantAdmin();
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38051889445"); smsTwoFaAccountConfig.setPhoneNumber("+38051889445");
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=100000", smsTwoFaAccountConfig) String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=100000", smsTwoFaAccountConfig)
@ -447,7 +450,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
configureSmsTwoFaProvider("${verificationCode}"); configureSmsTwoFaProvider("${verificationCode}");
loginTenantAdmin(); loginTenantAdmin();
SmsTwoFactorAuthAccountConfig initialSmsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); SmsTwoFaAccountConfig initialSmsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
initialSmsTwoFaAccountConfig.setPhoneNumber("+38051889445"); initialSmsTwoFaAccountConfig.setPhoneNumber("+38051889445");
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
@ -459,7 +462,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
String correctVerificationCode = verificationCodeCaptor.getValue(); String correctVerificationCode = verificationCodeCaptor.getValue();
SmsTwoFactorAuthAccountConfig anotherSmsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); SmsTwoFaAccountConfig anotherSmsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
anotherSmsTwoFaAccountConfig.setPhoneNumber("+38111111111"); anotherSmsTwoFaAccountConfig.setPhoneNumber("+38111111111");
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, anotherSmsTwoFaAccountConfig) String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, anotherSmsTwoFaAccountConfig)
.andExpect(status().isBadRequest())); .andExpect(status().isBadRequest()));
@ -467,20 +470,20 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, initialSmsTwoFaAccountConfig) doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, initialSmsTwoFaAccountConfig)
.andExpect(status().isOk()); .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); assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig);
} }
private TotpTwoFactorAuthProviderConfig configureTotpTwoFaProvider() throws Exception { private TotpTwoFaProviderConfig configureTotpTwoFaProvider() throws Exception {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb"); totpTwoFaProviderConfig.setIssuerName("tb");
saveProvidersConfigs(totpTwoFaProviderConfig); saveProvidersConfigs(totpTwoFaProviderConfig);
return totpTwoFaProviderConfig; return totpTwoFaProviderConfig;
} }
private SmsTwoFactorAuthProviderConfig configureSmsTwoFaProvider(String verificationMessageTemplate) throws Exception { private SmsTwoFaProviderConfig configureSmsTwoFaProvider(String verificationMessageTemplate) throws Exception {
SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate(verificationMessageTemplate); smsTwoFaProviderConfig.setSmsVerificationMessageTemplate(verificationMessageTemplate);
smsTwoFaProviderConfig.setVerificationCodeLifetime(60); smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
@ -488,8 +491,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
return smsTwoFaProviderConfig; return smsTwoFaProviderConfig;
} }
private void saveProvidersConfigs(TwoFactorAuthProviderConfig... providerConfigs) throws Exception { private void saveProvidersConfigs(TwoFaProviderConfig... providerConfigs) throws Exception {
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList())); twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList()));
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
@ -498,24 +501,24 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@Test @Test
public void testIsTwoFaEnabled() throws Exception { public void testIsTwoFaEnabled() throws Exception {
configureSmsTwoFaProvider("${verificationCode}"); configureSmsTwoFaProvider("${verificationCode}");
SmsTwoFactorAuthAccountConfig accountConfig = new SmsTwoFactorAuthAccountConfig(); SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig();
accountConfig.setPhoneNumber("+38050505050"); 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 @Test
public void testDeleteTwoFaAccountConfig() throws Exception { public void testDeleteTwoFaAccountConfig() throws Exception {
configureSmsTwoFaProvider("${verificationCode}"); configureSmsTwoFaProvider("${verificationCode}");
SmsTwoFactorAuthAccountConfig accountConfig = new SmsTwoFactorAuthAccountConfig(); SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig();
accountConfig.setPhoneNumber("+38050505050"); accountConfig.setPhoneNumber("+38050505050");
loginTenantAdmin(); 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); assertThat(savedAccountConfig).isEqualTo(accountConfig);
doDelete("/api/2fa/account/config").andExpect(status().isOk()); 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.audit.AuditLogService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.common.data.security.model.mfa.TwoFactorAuthSettings; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFactorAuthAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFactorAuthAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
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.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.JwtTokenPair; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class TwoFactorAuthTest extends AbstractControllerTest { public abstract class TwoFactorAuthTest extends AbstractControllerTest {
//
@Autowired // @Autowired
private TwoFactorAuthConfigManager twoFactorAuthConfigManager; // private TwoFaConfigManager twoFaConfigManager;
@Autowired // @Autowired
private TwoFactorAuthService twoFactorAuthService; // private TwoFactorAuthService twoFactorAuthService;
@MockBean // @MockBean
private SmsService smsService; // private SmsService smsService;
@Autowired // @Autowired
private AuditLogService auditLogService; // private AuditLogService auditLogService;
@Autowired // @Autowired
private UserService userService; // private UserService userService;
//
private User user; // private User user;
private String username; // private String username;
private String password; // private String password;
//
@Before // @Before
public void beforeEach() throws Exception { // public void beforeEach() throws Exception {
username = "mfa@tb.io"; // username = "mfa@tb.io";
password = "psswrd"; // password = "psswrd";
//
user = new User(); // user = new User();
user.setAuthority(Authority.TENANT_ADMIN); // user.setAuthority(Authority.TENANT_ADMIN);
user.setEmail(username); // user.setEmail(username);
user.setTenantId(tenantId); // user.setTenantId(tenantId);
//
loginSysAdmin(); // loginSysAdmin();
user = createUser(user, password); // user = createUser(user, password);
} // }
//
@After // @After
public void afterEach() { // public void afterEach() {
twoFactorAuthConfigManager.deleteTwoFaSettings(tenantId); // twoFaConfigManager.deletePlatformTwoFaSettings(tenantId);
twoFactorAuthConfigManager.deleteTwoFaSettings(TenantId.SYS_TENANT_ID); // twoFaConfigManager.deletePlatformTwoFaSettings(TenantId.SYS_TENANT_ID);
} // }
//
@Test // @Test
public void testTwoFa_totp() throws Exception { // public void testTwoFa_totp() throws Exception {
TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); // TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa();
//
logInWithPreVerificationToken(); // logInWithPreVerificationToken();
//
doPost("/api/auth/2fa/verification/send") // doPost("/api/auth/2fa/verification/send")
.andExpect(status().isOk()); // .andExpect(status().isOk());
//
String correctVerificationCode = getCorrectTotp(totpTwoFaAccountConfig); // String correctVerificationCode = getCorrectTotp(totpTwoFaAccountConfig);
//
JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) // JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode)
.andExpect(status().isOk()), JsonNode.class); // .andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenPair, username); // validateAndSetJwtToken(tokenPair, username);
//
User currentUser = readResponse(doGet("/api/auth/user") // User currentUser = readResponse(doGet("/api/auth/user")
.andExpect(status().isOk()), User.class); // .andExpect(status().isOk()), User.class);
assertThat(currentUser.getId()).isEqualTo(user.getId()); // assertThat(currentUser.getId()).isEqualTo(user.getId());
} // }
//
@Test // @Test
public void testTwoFa_sms() throws Exception { // public void testTwoFa_sms() throws Exception {
configureSmsTwoFa(); // configureSmsTwoFa();
//
logInWithPreVerificationToken(); // logInWithPreVerificationToken();
//
doPost("/api/auth/2fa/verification/send") // doPost("/api/auth/2fa/verification/send")
.andExpect(status().isOk()); // .andExpect(status().isOk());
//
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class); // ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); // verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture());
String correctVerificationCode = verificationCodeCaptor.getValue(); // String correctVerificationCode = verificationCodeCaptor.getValue();
//
JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) // JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode)
.andExpect(status().isOk()), JsonNode.class); // .andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenPair, username); // validateAndSetJwtToken(tokenPair, username);
//
User currentUser = readResponse(doGet("/api/auth/user") // User currentUser = readResponse(doGet("/api/auth/user")
.andExpect(status().isOk()), User.class); // .andExpect(status().isOk()), User.class);
assertThat(currentUser.getId()).isEqualTo(user.getId()); // assertThat(currentUser.getId()).isEqualTo(user.getId());
} // }
//
@Test // @Test
public void testTwoFaPreVerificationTokenLifetime() throws Exception { // public void testTwoFaPreVerificationTokenLifetime() throws Exception {
configureTotpTwoFa(twoFaSettings -> { // configureTotpTwoFa(twoFaSettings -> {
twoFaSettings.setTotalAllowedTimeForVerification(5); // twoFaSettings.setTotalAllowedTimeForVerification(5);
}); // });
//
logInWithPreVerificationToken(); // logInWithPreVerificationToken();
//
await("expiration of the pre-verification token") // await("expiration of the pre-verification token")
.atLeast(Duration.ofSeconds(3).plusMillis(500)) // .atLeast(Duration.ofSeconds(3).plusMillis(500))
.atMost(Duration.ofSeconds(6)) // .atMost(Duration.ofSeconds(6))
.untilAsserted(() -> { // .untilAsserted(() -> {
doPost("/api/auth/2fa/verification/send") // doPost("/api/auth/2fa/verification/send")
.andExpect(status().isUnauthorized()); // .andExpect(status().isUnauthorized());
}); // });
} // }
//
@Test // @Test
public void testCheckVerificationCode_userBlocked() throws Exception { // public void testCheckVerificationCode_userBlocked() throws Exception {
configureTotpTwoFa(twoFaSettings -> { // configureTotpTwoFa(twoFaSettings -> {
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); // twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
}); // });
//
logInWithPreVerificationToken(); // logInWithPreVerificationToken();
//
Stream.generate(() -> RandomStringUtils.randomNumeric(6)) // Stream.generate(() -> RandomStringUtils.randomNumeric(6))
.limit(9) // .limit(9)
.forEach(incorrectVerificationCode -> { // .forEach(incorrectVerificationCode -> {
try { // try {
String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + incorrectVerificationCode) // String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + incorrectVerificationCode)
.andExpect(status().isBadRequest())); // .andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); // assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
} catch (Exception e) { // } catch (Exception e) {
fail(); // fail();
} // }
}); // });
//
String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + RandomStringUtils.randomNumeric(6)) // String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + RandomStringUtils.randomNumeric(6))
.andExpect(status().isUnauthorized())); // .andExpect(status().isUnauthorized()));
assertThat(errorMessage).containsIgnoringCase("account was locked due to exceeded 2fa verification attempts"); // assertThat(errorMessage).containsIgnoringCase("account was locked due to exceeded 2fa verification attempts");
//
errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + RandomStringUtils.randomNumeric(6)) // errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + RandomStringUtils.randomNumeric(6))
.andExpect(status().isUnauthorized())); // .andExpect(status().isUnauthorized()));
assertThat(errorMessage).containsIgnoringCase("user is disabled"); // assertThat(errorMessage).containsIgnoringCase("user is disabled");
} // }
//
@Test // @Test
public void testSendVerificationCode_rateLimit() throws Exception { // public void testSendVerificationCode_rateLimit() throws Exception {
configureTotpTwoFa(twoFaSettings -> { // configureTotpTwoFa(twoFaSettings -> {
twoFaSettings.setVerificationCodeSendRateLimit("3:10"); // twoFaSettings.setVerificationCodeSendRateLimit("3:10");
}); // });
//
logInWithPreVerificationToken(); // logInWithPreVerificationToken();
//
for (int i = 0; i < 3; i++) { // for (int i = 0; i < 3; i++) {
doPost("/api/auth/2fa/verification/send") // doPost("/api/auth/2fa/verification/send")
.andExpect(status().isOk()); // .andExpect(status().isOk());
} // }
//
String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/send") // String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/send")
.andExpect(status().isTooManyRequests())); // .andExpect(status().isTooManyRequests()));
assertThat(rateLimitExceededError).containsIgnoringCase("too many verification code sending requests"); // assertThat(rateLimitExceededError).containsIgnoringCase("too many verification code sending requests");
//
await("verification code sending rate limit resetting") // await("verification code sending rate limit resetting")
.atLeast(Duration.ofSeconds(8)) // .atLeast(Duration.ofSeconds(8))
.atMost(Duration.ofSeconds(12)) // .atMost(Duration.ofSeconds(12))
.untilAsserted(() -> { // .untilAsserted(() -> {
doPost("/api/auth/2fa/verification/send") // doPost("/api/auth/2fa/verification/send")
.andExpect(status().isOk()); // .andExpect(status().isOk());
}); // });
} // }
//
@Test // @Test
public void testCheckVerificationCode_rateLimit() throws Exception { // public void testCheckVerificationCode_rateLimit() throws Exception {
TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(twoFaSettings -> { // TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(twoFaSettings -> {
twoFaSettings.setVerificationCodeCheckRateLimit("3:10"); // twoFaSettings.setVerificationCodeCheckRateLimit("3:10");
}); // });
//
logInWithPreVerificationToken(); // logInWithPreVerificationToken();
//
for (int i = 0; i < 3; i++) { // for (int i = 0; i < 3; i++) {
String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") // String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect")
.andExpect(status().isBadRequest())); // .andExpect(status().isBadRequest()));
assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); // assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect");
} // }
//
String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") // String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect")
.andExpect(status().isTooManyRequests())); // .andExpect(status().isTooManyRequests()));
assertThat(rateLimitExceededError).containsIgnoringCase("too many verification code checking requests"); // assertThat(rateLimitExceededError).containsIgnoringCase("too many verification code checking requests");
//
await("verification code checking rate limit resetting") // await("verification code checking rate limit resetting")
.atLeast(Duration.ofSeconds(8)) // .atLeast(Duration.ofSeconds(8))
.atMost(Duration.ofSeconds(12)) // .atMost(Duration.ofSeconds(12))
.untilAsserted(() -> { // .untilAsserted(() -> {
String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") // String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=incorrect")
.andExpect(status().isBadRequest())); // .andExpect(status().isBadRequest()));
assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); // assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect");
}); // });
//
doPost("/api/auth/2fa/verification/check?verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) // doPost("/api/auth/2fa/verification/check?verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig))
.andExpect(status().isOk()); // .andExpect(status().isOk());
} // }
//
@Test // @Test
public void testCheckVerificationCode_invalidVerificationCode() throws Exception { // public void testCheckVerificationCode_invalidVerificationCode() throws Exception {
configureTotpTwoFa(); // configureTotpTwoFa();
logInWithPreVerificationToken(); // logInWithPreVerificationToken();
//
for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) { // for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) {
String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + invalidVerificationCode) // String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + invalidVerificationCode)
.andExpect(status().isBadRequest())); // .andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); // assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
} // }
} // }
//
@Test // @Test
public void testCheckVerificationCode_codeExpiration() throws Exception { // public void testCheckVerificationCode_codeExpiration() throws Exception {
configureSmsTwoFa(smsTwoFaProviderConfig -> { // configureSmsTwoFa(smsTwoFaProviderConfig -> {
smsTwoFaProviderConfig.setVerificationCodeLifetime(10); // smsTwoFaProviderConfig.setVerificationCodeLifetime(10);
}); // });
//
logInWithPreVerificationToken(); // logInWithPreVerificationToken();
//
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class); // ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
doPost("/api/auth/2fa/verification/send").andExpect(status().isOk()); // doPost("/api/auth/2fa/verification/send").andExpect(status().isOk());
verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture()); // verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture());
//
String correctVerificationCode = verificationCodeCaptor.getValue(); // String correctVerificationCode = verificationCodeCaptor.getValue();
//
await("verification code expiration") // await("verification code expiration")
.pollDelay(10, TimeUnit.SECONDS) // .pollDelay(10, TimeUnit.SECONDS)
.atLeast(10, TimeUnit.SECONDS) // .atLeast(10, TimeUnit.SECONDS)
.atMost(12, TimeUnit.SECONDS) // .atMost(12, TimeUnit.SECONDS)
.untilAsserted(() -> { // .untilAsserted(() -> {
String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode) // String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?verificationCode=" + correctVerificationCode)
.andExpect(status().isBadRequest())); // .andExpect(status().isBadRequest()));
assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect"); // assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect");
}); // });
} // }
//
@Test // @Test
public void testTwoFa_logLoginAction() throws Exception { // public void testTwoFa_logLoginAction() throws Exception {
TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); // TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa();
//
logInWithPreVerificationToken(); // logInWithPreVerificationToken();
await("async audit log saving").during(1, TimeUnit.SECONDS); // await("async audit log saving").during(1, TimeUnit.SECONDS);
assertThat(getLogInAuditLogs()).isEmpty(); // assertThat(getLogInAuditLogs()).isEmpty();
assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() // assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo()
.get("lastLoginTs")).isNull(); // .get("lastLoginTs")).isNull();
//
doPost("/api/auth/2fa/verification/check?verificationCode=incorrect") // doPost("/api/auth/2fa/verification/check?verificationCode=incorrect")
.andExpect(status().isBadRequest()); // .andExpect(status().isBadRequest());
//
await("async audit log saving").atMost(1, TimeUnit.SECONDS) // await("async audit log saving").atMost(1, TimeUnit.SECONDS)
.until(() -> getLogInAuditLogs().size() == 1); // .until(() -> getLogInAuditLogs().size() == 1);
assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> { // assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> {
assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE); // assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE);
assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect"); // assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect");
assertThat(failedLogInAuditLog.getUserName()).isEqualTo(username); // assertThat(failedLogInAuditLog.getUserName()).isEqualTo(username);
}); // });
//
doPost("/api/auth/2fa/verification/check?verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) // doPost("/api/auth/2fa/verification/check?verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig))
.andExpect(status().isOk()); // .andExpect(status().isOk());
await("async audit log saving").atMost(1, TimeUnit.SECONDS) // await("async audit log saving").atMost(1, TimeUnit.SECONDS)
.until(() -> getLogInAuditLogs().size() == 2); // .until(() -> getLogInAuditLogs().size() == 2);
assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> { // assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> {
assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS); // assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS);
assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username); // assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username);
}); // });
assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() // assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo()
.get("lastLoginTs").asLong()) // .get("lastLoginTs").asLong())
.isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3)); // .isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3));
} // }
//
private List<AuditLog> getLogInAuditLogs() { // private List<AuditLog> getLogInAuditLogs() {
return auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, user.getId(), List.of(ActionType.LOGIN), // 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(); // new TimePageLink(new PageLink(10, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC)), 0L, System.currentTimeMillis())).getData();
} // }
//
@Test // @Test
public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException { // public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException {
configureTotpTwoFa(); // configureTotpTwoFa();
twoFactorAuthConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId()); // twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId(), );
//
assertDoesNotThrow(() -> { // assertDoesNotThrow(() -> {
login(username, password); // login(username, password);
}); // });
} // }
//
private void logInWithPreVerificationToken() throws Exception { // private void logInWithPreVerificationToken() throws Exception {
LoginRequest loginRequest = new LoginRequest(username, password); // LoginRequest loginRequest = new LoginRequest(username, password);
//
JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class); // JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class);
assertThat(response.getToken()).isNotNull(); // assertThat(response.getToken()).isNotNull();
assertThat(response.getRefreshToken()).isNull(); // assertThat(response.getRefreshToken()).isNull();
assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); // assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN);
//
this.token = response.getToken(); // this.token = response.getToken();
} // }
//
private TotpTwoFactorAuthAccountConfig configureTotpTwoFa(Consumer<TwoFactorAuthSettings>... customizer) throws ThingsboardException { // private TotpTwoFaAccountConfig configureTotpTwoFa(Consumer<PlatformTwoFaSettings>... customizer) throws ThingsboardException {
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig(); // TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb"); // totpTwoFaProviderConfig.setIssuerName("tb");
//
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); // PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setUseSystemTwoFactorAuthSettings(false); // twoFaSettings.setUseSystemTwoFactorAuthSettings(false);
twoFaSettings.setProviders(Arrays.stream(new TwoFactorAuthProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); // twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings)); // Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings));
twoFactorAuthConfigManager.saveTwoFaSettings(tenantId, twoFaSettings); // twoFaConfigManager.savePlatformTwoFaSettings(tenantId, twoFaSettings);
//
TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig = (TotpTwoFactorAuthAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFactorAuthProviderType.TOTP); // TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFaProviderType.TOTP);
twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig); // twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig);
return totpTwoFaAccountConfig; // return totpTwoFaAccountConfig;
} // }
//
private SmsTwoFactorAuthAccountConfig configureSmsTwoFa(Consumer<SmsTwoFactorAuthProviderConfig>... customizer) throws ThingsboardException { // private SmsTwoFaAccountConfig configureSmsTwoFa(Consumer<SmsTwoFaProviderConfig>... customizer) throws ThingsboardException {
SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig(); // SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
smsTwoFaProviderConfig.setVerificationCodeLifetime(60); // smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}"); // smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}");
Arrays.stream(customizer).forEach(c -> c.accept(smsTwoFaProviderConfig)); // Arrays.stream(customizer).forEach(c -> c.accept(smsTwoFaProviderConfig));
//
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings(); // PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setUseSystemTwoFactorAuthSettings(false); // twoFaSettings.setUseSystemTwoFactorAuthSettings(false);
twoFaSettings.setProviders(Arrays.stream(new TwoFactorAuthProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList())); // twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList()));
twoFactorAuthConfigManager.saveTwoFaSettings(tenantId, twoFaSettings); // twoFaConfigManager.savePlatformTwoFaSettings(tenantId, twoFaSettings);
//
SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig(); // SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38050505050"); // smsTwoFaAccountConfig.setPhoneNumber("+38050505050");
twoFactorAuthConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig); // twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig);
return smsTwoFaAccountConfig; // return smsTwoFaAccountConfig;
} // }
//
private String getCorrectTotp(TotpTwoFactorAuthAccountConfig totpTwoFaAccountConfig) { // private String getCorrectTotp(TotpTwoFaAccountConfig totpTwoFaAccountConfig) {
String secret = StringUtils.substringAfterLast(totpTwoFaAccountConfig.getAuthUrl(), "secret="); // String secret = StringUtils.substringAfterLast(totpTwoFaAccountConfig.getAuthUrl(), "secret=");
return new Totp(secret).now(); // 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.BaseData;
import org.thingsboard.server.common.data.id.UserAuthSettingsId; import org.thingsboard.server.common.data.id.UserAuthSettingsId;
import org.thingsboard.server.common.data.id.UserId; 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 @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ -29,6 +29,6 @@ public class UserAuthSettings extends BaseData<UserAuthSettingsId> {
private static final long serialVersionUID = 2628320657987010348L; private static final long serialVersionUID = 2628320657987010348L;
private UserId userId; 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.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
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.data.security.model.mfa.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;
@ -29,7 +29,7 @@ import java.util.Optional;
@Data @Data
@ApiModel @ApiModel
public class TwoFactorAuthSettings { public class PlatformTwoFaSettings {
@ApiModelProperty(value = "Option for tenant admins to use 2FA settings configured by sysadmin. " + @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 " + "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; private boolean useSystemTwoFactorAuthSettings;
@ApiModelProperty(value = "The list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list.") @ApiModelProperty(value = "The list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list.")
@Valid @Valid
private List<TwoFactorAuthProviderConfig> providers; private List<TwoFaProviderConfig> providers;
@ApiModelProperty(value = "Rate limit configuration for verification code sending. The format is standard: 'amountOfRequests:periodInSeconds'. " + @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) "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; private Integer totalAllowedTimeForVerification;
public Optional<TwoFactorAuthProviderConfig> getProviderConfig(TwoFactorAuthProviderType providerType) { public Optional<TwoFaProviderConfig> getProviderConfig(TwoFaProviderType providerType) {
return Optional.ofNullable(providers) return Optional.ofNullable(providers)
.flatMap(providersConfigs -> providersConfigs.stream() .flatMap(providersConfigs -> providersConfigs.stream()
.filter(providerConfig -> providerConfig.getProviderType() == providerType) .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; package org.thingsboard.server.common.data.security.model.mfa.account;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
@Data @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 io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; 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.NotBlank;
import javax.validation.constraints.Pattern; import javax.validation.constraints.Pattern;
@ -27,7 +27,7 @@ import javax.validation.constraints.Pattern;
@ApiModel @ApiModel
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data @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) @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") @NotBlank(message = "phone number cannot be blank")
@ -35,8 +35,8 @@ public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountC
private String phoneNumber; private String phoneNumber;
@Override @Override
public TwoFactorAuthProviderType getProviderType() { public TwoFaProviderType getProviderType() {
return TwoFactorAuthProviderType.SMS; 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.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; 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.NotBlank;
import javax.validation.constraints.Pattern; import javax.validation.constraints.Pattern;
@ApiModel @ApiModel // FIXME [viacheslav]
@Data @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.", @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) 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; private String authUrl;
@Override @Override
public TwoFactorAuthProviderType getProviderType() { public TwoFaProviderType getProviderType() {
return TwoFactorAuthProviderType.TOTP; 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;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; 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) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo( @JsonTypeInfo(
use = JsonTypeInfo.Id.NAME, use = JsonTypeInfo.Id.NAME,
property = "providerType") property = "providerType")
@JsonSubTypes({ @JsonSubTypes({
@Type(name = "TOTP", value = TotpTwoFactorAuthAccountConfig.class), @Type(name = "TOTP", value = TotpTwoFaAccountConfig.class),
@Type(name = "SMS", value = SmsTwoFactorAuthAccountConfig.class) @Type(name = "SMS", value = SmsTwoFaAccountConfig.class)
}) })
public interface TwoFactorAuthAccountConfig { @Data
public abstract class TwoFaAccountConfig {
private boolean useByDefault;
@JsonIgnore @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; import javax.validation.constraints.Min;
@Data @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 " + @ApiModelProperty(value = "Verification code lifetime in seconds. Verification codes with a lifetime bigger than this param " +
"will be considered incorrect", example = "60", required = true) "will be considered incorrect", example = "60", required = true)
@Min(value = 1, message = "verification code lifetime is required") @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.NotBlank;
import javax.validation.constraints.Pattern; import javax.validation.constraints.Pattern;
@ApiModel(parent = OtpBasedTwoFactorAuthProviderConfig.class) @ApiModel(parent = OtpBasedTwoFaProviderConfig.class)
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data @Data
public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProviderConfig { public class SmsTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig {
@ApiModelProperty(value = "SMS verification message template. Available template variables are ${verificationCode} and ${userEmail}. " + @ApiModelProperty(value = "SMS verification message template. Available template variables are ${verificationCode} and ${userEmail}. " +
"It must not be blank and must contain verification code variable.", "It must not be blank and must contain verification code variable.",
@ -36,8 +36,8 @@ public class SmsTwoFactorAuthProviderConfig extends OtpBasedTwoFactorAuthProvide
private String smsVerificationMessageTemplate; private String smsVerificationMessageTemplate;
@Override @Override
public TwoFactorAuthProviderType getProviderType() { public TwoFaProviderType getProviderType() {
return TwoFactorAuthProviderType.SMS; 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 @ApiModel
@Data @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. " + @ApiModelProperty(value = "Issuer name that will be displayed in an authenticator app near a username. " +
"Must not be blank.", example = "ThingsBoard", required = true) "Must not be blank.", example = "ThingsBoard", required = true)
@ -31,8 +31,8 @@ public class TotpTwoFactorAuthProviderConfig implements TwoFactorAuthProviderCon
private String issuerName; private String issuerName;
@Override @Override
public TwoFactorAuthProviderType getProviderType() { public TwoFaProviderType getProviderType() {
return TwoFactorAuthProviderType.TOTP; 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, use = JsonTypeInfo.Id.NAME,
property = "providerType") property = "providerType")
@JsonSubTypes({ @JsonSubTypes({
@Type(name = "TOTP", value = TotpTwoFactorAuthProviderConfig.class), @Type(name = "TOTP", value = TotpTwoFaProviderConfig.class),
@Type(name = "SMS", value = SmsTwoFactorAuthProviderConfig.class) @Type(name = "SMS", value = SmsTwoFaProviderConfig.class)
}) })
public interface TwoFactorAuthProviderConfig { public interface TwoFaProviderConfig {
@JsonIgnore @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; package org.thingsboard.server.common.data.security.model.mfa.provider;
public enum TwoFactorAuthProviderType { public enum TwoFaProviderType {
TOTP, TOTP,
SMS 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 { public class TbRateLimits {
private final LocalBucket bucket; private final LocalBucket bucket;
private final String config;
public TbRateLimits(String limitsConfiguration) { public TbRateLimits(String limitsConfiguration) {
this(limitsConfiguration, false); this(limitsConfiguration, false);
@ -48,6 +49,7 @@ public class TbRateLimits {
} else { } else {
throw new IllegalArgumentException("Failed to parse rate limits configuration: " + limitsConfiguration); throw new IllegalArgumentException("Failed to parse rate limits configuration: " + limitsConfiguration);
} }
this.config = limitsConfiguration;
} }
public boolean tryConsume() { public boolean tryConsume() {
@ -58,4 +60,8 @@ public class TbRateLimits {
return bucket.tryConsume(number); 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_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_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. * 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.UserAuthSettingsId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserAuthSettings; 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.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants; 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) @Column(name = ModelConstants.USER_AUTH_SETTINGS_USER_ID_PROPERTY, nullable = false, unique = true)
private UUID userId; private UUID userId;
@Type(type = "json") @Type(type = "json")
@Column(name = ModelConstants.USER_AUTH_SETTINGS_TWO_FA_ACCOUNT_CONFIG_PROPERTY) @Column(name = ModelConstants.USER_AUTH_SETTINGS_TWO_FA_SETTINGS)
private JsonNode twoFaAccountConfig; private JsonNode twoFaSettings;
public UserAuthSettingsEntity(UserAuthSettings userAuthSettings) { public UserAuthSettingsEntity(UserAuthSettings userAuthSettings) {
if (userAuthSettings.getId() != null) { if (userAuthSettings.getId() != null) {
@ -58,8 +59,8 @@ public class UserAuthSettingsEntity extends BaseSqlEntity<UserAuthSettings> impl
if (userAuthSettings.getUserId() != null) { if (userAuthSettings.getUserId() != null) {
this.userId = userAuthSettings.getUserId().getId(); this.userId = userAuthSettings.getUserId().getId();
} }
if (userAuthSettings.getTwoFaAccountConfig() != null) { if (userAuthSettings.getTwoFaSettings() != null) {
this.twoFaAccountConfig = JacksonUtil.valueToTree(userAuthSettings.getTwoFaAccountConfig()); this.twoFaSettings = JacksonUtil.valueToTree(userAuthSettings.getTwoFaSettings());
} }
} }
@ -71,8 +72,8 @@ public class UserAuthSettingsEntity extends BaseSqlEntity<UserAuthSettings> impl
if (userId != null) { if (userId != null) {
userAuthSettings.setUserId(new UserId(userId)); userAuthSettings.setUserId(new UserId(userId));
} }
if (twoFaAccountConfig != null) { if (twoFaSettings != null) {
userAuthSettings.setTwoFaAccountConfig(JacksonUtil.treeToValue(twoFaAccountConfig, TwoFactorAuthAccountConfig.class)); userAuthSettings.setTwoFaSettings(JacksonUtil.treeToValue(twoFaSettings, AccountTwoFaSettings.class));
} }
return userAuthSettings; 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, id uuid NOT NULL CONSTRAINT user_auth_settings_pkey PRIMARY KEY,
created_time bigint NOT NULL, created_time bigint NOT NULL,
user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id), 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