Email 2FA provider; 2FA API improvements
This commit is contained in:
parent
bcc736991e
commit
4480badd78
@ -33,7 +33,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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.ThingsboardException;
|
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
||||||
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
|
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.AccountTwoFaSettings;
|
||||||
@ -51,6 +50,7 @@ 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.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
|
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
|
||||||
@ -65,20 +65,15 @@ public class TwoFaConfigController extends BaseController {
|
|||||||
private final TwoFactorAuthService twoFactorAuthService;
|
private final TwoFactorAuthService twoFactorAuthService;
|
||||||
|
|
||||||
|
|
||||||
@ApiOperation(value = "Get 2FA account config (getTwoFaAccountConfig)",
|
@ApiOperation(value = "Get account 2FA settings (getAccountTwoFaSettings)",
|
||||||
notes = "Get user's account 2FA configuration. Returns empty result if user did not configured 2FA, " +
|
notes = "Get user's account 2FA configuration. Configuration contains configs for different 2FA providers." + NEW_LINE +
|
||||||
"or if a provider for previously set up account config is not now configured." + NEW_LINE +
|
"Example:\n" +
|
||||||
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE +
|
"```\n{\n \"configs\": {\n" +
|
||||||
"Response example for TOTP 2FA: " + NEW_LINE +
|
" \"EMAIL\": {\n \"providerType\": \"EMAIL\",\n \"useByDefault\": true,\n \"email\": \"tenant@thingsboard.org\"\n },\n" +
|
||||||
"```\n{\n" +
|
" \"TOTP\": {\n \"providerType\": \"TOTP\",\n \"useByDefault\": false,\n \"authUrl\": \"otpauth://totp/TB%202FA:tenant@thingsboard.org?issuer=TB+2FA&secret=P6Z2TLYTASOGP6LCJZAD24ETT5DACNNX\"\n },\n" +
|
||||||
" \"providerType\": \"TOTP\",\n" +
|
" \"SMS\": {\n \"providerType\": \"SMS\",\n \"useByDefault\": false,\n \"phoneNumber\": \"+380501253652\"\n }\n" +
|
||||||
" \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" +
|
" }\n}\n```" +
|
||||||
"}\n```" + NEW_LINE +
|
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
|
||||||
"Response example for SMS 2FA: " + NEW_LINE +
|
|
||||||
"```\n{\n" +
|
|
||||||
" \"providerType\": \"SMS\",\n" +
|
|
||||||
" \"phoneNumber\": \"+380505005050\"\n" +
|
|
||||||
"}\n```")
|
|
||||||
@GetMapping("/account/settings")
|
@GetMapping("/account/settings")
|
||||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||||
public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException {
|
public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException {
|
||||||
@ -88,24 +83,29 @@ public class TwoFaConfigController extends BaseController {
|
|||||||
|
|
||||||
|
|
||||||
@ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)",
|
@ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)",
|
||||||
notes = "Generate new 2FA account config for specified provider type. " +
|
notes = "Generate new 2FA account config template for specified provider type. " + NEW_LINE +
|
||||||
"This method is only useful for TOTP 2FA, as there is nothing to generate for other provider types. " +
|
|
||||||
"For TOTP, this will return a corresponding account config template " +
|
"For TOTP, this will return a corresponding account config template " +
|
||||||
"with a generated OTP auth URL (with new random secret key for each API call) that can be then " +
|
"with a generated OTP auth URL (with new random secret key for each API call) that can be then " +
|
||||||
"converted to a QR code to scan with an authenticator app. " +
|
"converted to a QR code to scan with an authenticator app. Example:\n" +
|
||||||
"For other provider types, this method will return an empty config. " + NEW_LINE +
|
|
||||||
"Will throw an error (Bad Request) if the provider is not configured for usage. " +
|
|
||||||
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + NEW_LINE +
|
|
||||||
"Example of a generated account config for TOTP 2FA: " + NEW_LINE +
|
|
||||||
"```\n{\n" +
|
"```\n{\n" +
|
||||||
" \"providerType\": \"TOTP\",\n" +
|
" \"providerType\": \"TOTP\",\n" +
|
||||||
" \"authUrl\": \"otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII\"\n" +
|
" \"useByDefault\": false,\n" +
|
||||||
|
" \"authUrl\": \"otpauth://totp/TB%202FA:tenant@thingsboard.org?issuer=TB+2FA&secret=PNJDNWJVAK4ZTUYT7RFGPQLXA7XGU7PX\"\n" +
|
||||||
"}\n```" + NEW_LINE +
|
"}\n```" + NEW_LINE +
|
||||||
"For SMS provider type it will return something like: " + NEW_LINE +
|
"For EMAIL, the generated config will contain email from user's account:\n" +
|
||||||
|
"```\n{\n" +
|
||||||
|
" \"providerType\": \"EMAIL\",\n" +
|
||||||
|
" \"useByDefault\": false,\n" +
|
||||||
|
" \"email\": \"tenant@thingsboard.org\"\n" +
|
||||||
|
"}\n```" + NEW_LINE +
|
||||||
|
"For SMS 2FA this method will just return a config with empty/default values as there is nothing to generate/preset:\n" +
|
||||||
"```\n{\n" +
|
"```\n{\n" +
|
||||||
" \"providerType\": \"SMS\",\n" +
|
" \"providerType\": \"SMS\",\n" +
|
||||||
|
" \"useByDefault\": false,\n" +
|
||||||
" \"phoneNumber\": null\n" +
|
" \"phoneNumber\": null\n" +
|
||||||
"}\n```")
|
"}\n```" + NEW_LINE +
|
||||||
|
"Will throw an error (Bad Request) if the provider is not configured for usage. " +
|
||||||
|
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
|
||||||
@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 TwoFaAccountConfig 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)
|
||||||
@ -133,51 +133,84 @@ public class TwoFaConfigController extends BaseController {
|
|||||||
notes = "Submit 2FA account config to prepare for a future verification. " +
|
notes = "Submit 2FA account config to prepare for a future verification. " +
|
||||||
"Basically, this method will send a verification code for a given account config, if this has " +
|
"Basically, this method will send a verification code for a given account config, if this has " +
|
||||||
"sense for a chosen 2FA provider. This code is needed to then verify and save the account config." + NEW_LINE +
|
"sense for a chosen 2FA provider. This code is needed to then verify and save the account config." + NEW_LINE +
|
||||||
|
"Example of EMAIL 2FA account config:\n" +
|
||||||
|
"```\n{\n" +
|
||||||
|
" \"providerType\": \"EMAIL\",\n" +
|
||||||
|
" \"useByDefault\": true,\n" +
|
||||||
|
" \"email\": \"separate-email-for-2fa@thingsboard.org\"\n" +
|
||||||
|
"}\n```" + NEW_LINE +
|
||||||
|
"Example of SMS 2FA account config:\n" +
|
||||||
|
"```\n{\n" +
|
||||||
|
" \"providerType\": \"SMS\",\n" +
|
||||||
|
" \"useByDefault\": false,\n" +
|
||||||
|
" \"phoneNumber\": \"+38012312321\"\n" +
|
||||||
|
"}\n```" + NEW_LINE +
|
||||||
|
"For TOTP this method does nothing." + NEW_LINE +
|
||||||
"Will throw an error (Bad Request) if submitted account config is not valid, " +
|
"Will throw an error (Bad Request) if submitted account config is not valid, " +
|
||||||
"or if the provider is not configured for usage. " +
|
"or if the provider is not configured for usage. " +
|
||||||
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
|
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
|
||||||
@PostMapping("/account/config/submit")
|
@PostMapping("/account/config/submit")
|
||||||
@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(@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception {
|
||||||
"For SMS 2FA, phoneNumber property must not be blank and must be of E.164 phone number format.", required = true)
|
|
||||||
@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception {
|
|
||||||
SecurityUser user = getCurrentUser();
|
SecurityUser user = getCurrentUser();
|
||||||
twoFactorAuthService.prepareVerificationCode(user, accountConfig, false);
|
twoFactorAuthService.prepareVerificationCode(user, accountConfig, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation(value = "Verify and save 2FA account config (verifyAndSaveTwoFaAccountConfig)",
|
@ApiOperation(value = "Verify and save 2FA account config (verifyAndSaveTwoFaAccountConfig)",
|
||||||
notes = "Checks the verification code for submitted config, and if it is correct, saves the provided account config. " +
|
notes = "Checks the verification code for submitted config, and if it is correct, saves the provided account config. " + NEW_LINE +
|
||||||
"The config is stored in the user's additionalInfo. " + NEW_LINE +
|
"Returns whole account's 2FA settings object.\n" +
|
||||||
"Will throw an error (Bad Request) if the provider is not configured for usage. " +
|
"Will throw an error (Bad Request) if the provider is not configured for usage. " +
|
||||||
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 AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@ApiParam(value = "2FA account config to save. Validation rules are the same as in submitTwoFaAccountConfig API method", required = true)
|
public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig 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 or email message in case of SMS or EMAIL 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();
|
||||||
|
if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig.getProviderType()).isPresent()) {
|
||||||
|
throw new IllegalArgumentException("2FA provider is already configured");
|
||||||
|
}
|
||||||
|
|
||||||
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false);
|
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false);
|
||||||
if (verificationSuccess) {
|
if (verificationSuccess) {
|
||||||
return twoFaConfigManager.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 IllegalArgumentException("Verification code is incorrect");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "Update 2FA account config (updateTwoFaAccountConfig)", notes =
|
||||||
|
"Update config for a given provider type. \n" +
|
||||||
|
"Update request example:\n" +
|
||||||
|
"```\n{\n \"useByDefault\": true\n}\n```\n" +
|
||||||
|
"Returns whole account's 2FA settings object.\n" +
|
||||||
|
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
|
||||||
@PutMapping("/account/config")
|
@PutMapping("/account/config")
|
||||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||||
public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType,
|
public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType,
|
||||||
@RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException {
|
@RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException {
|
||||||
SecurityUser user = getCurrentUser();
|
SecurityUser user = getCurrentUser();
|
||||||
TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("No 2FA config for provider " + providerType));
|
|
||||||
|
|
||||||
|
AccountTwoFaSettings settings = twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("No 2FA config found"));
|
||||||
|
Map<TwoFaProviderType, TwoFaAccountConfig> configs = settings.getConfigs();
|
||||||
|
|
||||||
|
TwoFaAccountConfig accountConfig;
|
||||||
|
if ((accountConfig = configs.get(providerType)) == null) {
|
||||||
|
throw new IllegalArgumentException("Config for " + providerType + " 2FA provider not found");
|
||||||
|
}
|
||||||
|
if (updateRequest.isUseByDefault()) {
|
||||||
|
configs.values().forEach(config -> config.setUseByDefault(false));
|
||||||
|
}
|
||||||
accountConfig.setUseByDefault(updateRequest.isUseByDefault());
|
accountConfig.setUseByDefault(updateRequest.isUseByDefault());
|
||||||
return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
|
|
||||||
|
return twoFaConfigManager.saveAccountTwoFaSettings(user.getTenantId(), user.getId(), settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)",
|
@ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", notes =
|
||||||
notes = "Delete user's 2FA config. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
|
"Delete 2FA config for a given 2FA provider type. \n" +
|
||||||
|
"Returns whole account's 2FA settings object.\n" +
|
||||||
|
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 AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException {
|
public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException {
|
||||||
@ -186,6 +219,12 @@ public class TwoFaConfigController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
|
||||||
|
"Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" +
|
||||||
|
"Example of response:\n" +
|
||||||
|
"```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" +
|
||||||
|
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER
|
||||||
|
)
|
||||||
@GetMapping("/providers")
|
@GetMapping("/providers")
|
||||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||||
public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException {
|
public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException {
|
||||||
@ -196,8 +235,9 @@ public class TwoFaConfigController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ApiOperation(value = "Get 2FA settings (getTwoFaSettings)", // FIXME [viacheslav]
|
@ApiOperation(value = "Get platform 2FA settings (getPlatformTwoFaSettings)",
|
||||||
notes = "Get settings for 2FA. If 2FA is not configured, then an empty response will be returned." +
|
notes = "Get platform settings for 2FA. The settings are described for savePlatformTwoFaSettings API method. " +
|
||||||
|
"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')")
|
||||||
@ -205,9 +245,49 @@ public class TwoFaConfigController extends BaseController {
|
|||||||
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), false).orElse(null);
|
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), false).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation(value = "Save 2FA settings (saveTwoFaSettings)",
|
@ApiOperation(value = "Save platform 2FA settings (savePlatformTwoFaSettings)",
|
||||||
notes = "Save settings for 2FA. If a user is sysadmin - the settings are saved as AdminSettings; " +
|
notes = "Save 2FA settings for platform. The settings have following properties:\n" +
|
||||||
"if it is a tenant admin - as a tenant attribute." +
|
"- `useSystemTwoFactorAuthSettings` - 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 it is a tenant admin; for sysadmin this param is ignored).\n" +
|
||||||
|
"- `providers` - the list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list. \n\n" +
|
||||||
|
"- `verificationCodeSendRateLimit` - 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.\n" +
|
||||||
|
"- `verificationCodeCheckRateLimit` - rate limit configuration for verification code checking.\n" +
|
||||||
|
"- `maxVerificationFailuresBeforeUserLockout` - maximum number of verification failures before a user gets disabled.\n" +
|
||||||
|
"- `totalAllowedTimeForVerification` - total amount of time in seconds allotted for verification. " +
|
||||||
|
"Basically, this property sets a lifetime for pre-verification token. If not set, default value of 30 minutes is used.\n" + NEW_LINE +
|
||||||
|
"TOTP 2FA provider config has following settings:\n" +
|
||||||
|
"- `issuerName` - issuer name that will be displayed in an authenticator app near a username. Must not be blank.\n\n" +
|
||||||
|
"For SMS 2FA provider:\n" +
|
||||||
|
"- `smsVerificationMessageTemplate` - verification message template. Available template variables " +
|
||||||
|
"are ${verificationCode} and ${userEmail}. It must not be blank and must contain verification code variable.\n" +
|
||||||
|
"- `verificationCodeLifetime` - verification code lifetime in seconds. Required to be positive.\n\n" +
|
||||||
|
"For EMAIL provider type:\n" +
|
||||||
|
"- `verificationCodeLifetime` - the same as for SMS." + NEW_LINE +
|
||||||
|
"Example of the settings:\n" +
|
||||||
|
"```\n{\n" +
|
||||||
|
" \"useSystemTwoFactorAuthSettings\": false,\n" +
|
||||||
|
" \"providers\": [\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"providerType\": \"TOTP\",\n" +
|
||||||
|
" \"issuerName\": \"TB\"\n" +
|
||||||
|
" },\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"providerType\": \"EMAIL\",\n" +
|
||||||
|
" \"verificationCodeLifetime\": 60\n" +
|
||||||
|
" },\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"providerType\": \"SMS\",\n" +
|
||||||
|
" \"verificationCodeLifetime\": 60,\n" +
|
||||||
|
" \"smsVerificationMessageTemplate\": \"Here is your verification code: ${verificationCode}\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
" ],\n" +
|
||||||
|
" \"verificationCodeSendRateLimit\": \"1:60\",\n" +
|
||||||
|
" \"verificationCodeCheckRateLimit\": \"3:900\",\n" +
|
||||||
|
" \"maxVerificationFailuresBeforeUserLockout\": 10,\n" +
|
||||||
|
" \"totalAllowedTimeForVerification\": 600\n" +
|
||||||
|
"}\n```" +
|
||||||
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')")
|
||||||
|
|||||||
@ -84,8 +84,8 @@ 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/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(@RequestParam TwoFaProviderType providerType,
|
||||||
@RequestParam TwoFaProviderType providerType,
|
@ApiParam(value = "6-digit verification code", required = true)
|
||||||
@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, providerType, verificationCode, true);
|
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
|
||||||
@ -101,6 +101,13 @@ public class TwoFactorAuthController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
|
||||||
|
"Get the list of 2FA provider infos available for user to use. Example:\n" +
|
||||||
|
"```\n[\n" +
|
||||||
|
" {\n \"type\": \"EMAIL\",\n \"default\": true\n },\n" +
|
||||||
|
" {\n \"type\": \"TOTP\",\n \"default\": false\n },\n" +
|
||||||
|
" {\n \"type\": \"SMS\",\n \"default\": false\n }\n" +
|
||||||
|
"]\n```")
|
||||||
@GetMapping("/providers")
|
@GetMapping("/providers")
|
||||||
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
|
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
|
||||||
public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException {
|
public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException {
|
||||||
|
|||||||
@ -40,7 +40,6 @@ import org.thingsboard.server.common.data.ApiUsageStateValue;
|
|||||||
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.id.CustomerId;
|
import org.thingsboard.server.common.data.id.CustomerId;
|
||||||
import org.thingsboard.server.common.data.id.EntityId;
|
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
import org.thingsboard.server.dao.exception.IncorrectParameterException;
|
import org.thingsboard.server.dao.exception.IncorrectParameterException;
|
||||||
import org.thingsboard.server.dao.settings.AdminSettingsService;
|
import org.thingsboard.server.dao.settings.AdminSettingsService;
|
||||||
@ -311,6 +310,14 @@ public class DefaultMailService implements MailService {
|
|||||||
sendMail(mailSender, mailFrom, email, subject, message);
|
sendMail(mailSender, mailFrom, email, subject, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendTwoFaVerificationEmail(String email, String verificationCode) throws ThingsboardException { // TODO [viacheslav]: mail template
|
||||||
|
String subject = "ThingsBoard two-factor authentication";
|
||||||
|
String message = "Your 2FA verification code: " + verificationCode;
|
||||||
|
|
||||||
|
sendMail(mailSender, mailFrom, email, subject, message);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException {
|
public void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException {
|
||||||
String subject = messages.getMessage("api.usage.state", null, Locale.US);
|
String subject = messages.getMessage("api.usage.state", null, Locale.US);
|
||||||
|
|||||||
@ -68,6 +68,20 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings) {
|
||||||
|
UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
|
||||||
|
.orElseGet(() -> {
|
||||||
|
UserAuthSettings newUserAuthSettings = new UserAuthSettings();
|
||||||
|
newUserAuthSettings.setUserId(userId);
|
||||||
|
return newUserAuthSettings;
|
||||||
|
});
|
||||||
|
userAuthSettings.setTwoFaSettings(settings);
|
||||||
|
userAuthSettingsDao.save(tenantId, userAuthSettings);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
|
public Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
|
||||||
return getAccountTwoFaSettings(tenantId, userId)
|
return getAccountTwoFaSettings(tenantId, userId)
|
||||||
@ -80,33 +94,21 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
|
|||||||
getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
|
getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
|
||||||
.orElseThrow(() -> new IllegalArgumentException("2FA provider is not configured"));
|
.orElseThrow(() -> new IllegalArgumentException("2FA provider is not configured"));
|
||||||
|
|
||||||
return createOrUpdateAccountTwoFaSettings(tenantId, userId, accountTwoFaSettings -> {
|
AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId).orElseGet(() -> {
|
||||||
Map<TwoFaProviderType, TwoFaAccountConfig> configs = accountTwoFaSettings.getConfigs();
|
AccountTwoFaSettings newSettings = new AccountTwoFaSettings();
|
||||||
configs.put(accountConfig.getProviderType(), accountConfig);
|
newSettings.setConfigs(new LinkedHashMap<>());
|
||||||
|
return newSettings;
|
||||||
});
|
});
|
||||||
|
settings.getConfigs().put(accountConfig.getProviderType(), accountConfig);
|
||||||
|
return saveAccountTwoFaSettings(tenantId, userId, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
|
public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
|
||||||
return createOrUpdateAccountTwoFaSettings(tenantId, userId, accountTwoFaSettings -> {
|
AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId)
|
||||||
accountTwoFaSettings.getConfigs().keySet().removeIf(providerType::equals);
|
.orElseThrow(() -> new IllegalArgumentException("2FA not configured"));
|
||||||
});
|
settings.getConfigs().remove(providerType);
|
||||||
}
|
return saveAccountTwoFaSettings(tenantId, userId, settings);
|
||||||
|
|
||||||
private AccountTwoFaSettings createOrUpdateAccountTwoFaSettings(TenantId tenantId, UserId userId, Consumer<AccountTwoFaSettings> updater) {
|
|
||||||
UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
|
|
||||||
.orElseGet(() -> {
|
|
||||||
UserAuthSettings newUserAuthSettings = new UserAuthSettings();
|
|
||||||
newUserAuthSettings.setUserId(userId);
|
|
||||||
|
|
||||||
AccountTwoFaSettings newAccountTwoFaSettings = new AccountTwoFaSettings();
|
|
||||||
newAccountTwoFaSettings.setConfigs(new LinkedHashMap<>());
|
|
||||||
newUserAuthSettings.setTwoFaSettings(newAccountTwoFaSettings);
|
|
||||||
return newUserAuthSettings;
|
|
||||||
});
|
|
||||||
updater.accept(userAuthSettings.getTwoFaSettings());
|
|
||||||
userAuthSettingsDao.save(tenantId, userAuthSettings);
|
|
||||||
return userAuthSettings.getTwoFaSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -26,9 +26,10 @@ import java.util.Optional;
|
|||||||
|
|
||||||
public interface TwoFaConfigManager {
|
public interface TwoFaConfigManager {
|
||||||
|
|
||||||
|
|
||||||
Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId);
|
Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId);
|
||||||
|
|
||||||
|
AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings);
|
||||||
|
|
||||||
Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType);
|
Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType);
|
||||||
|
|
||||||
AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig);
|
AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig);
|
||||||
|
|||||||
@ -26,9 +26,9 @@ public interface TwoFaProvider<C extends TwoFaProviderConfig, A extends TwoFaAcc
|
|||||||
|
|
||||||
A generateNewAccountConfig(User user, C providerConfig);
|
A generateNewAccountConfig(User user, C providerConfig);
|
||||||
|
|
||||||
default void prepareVerificationCode(SecurityUser securityUser, C providerConfig, A accountConfig) throws ThingsboardException {}
|
default void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException {}
|
||||||
|
|
||||||
boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, C providerConfig, A accountConfig);
|
boolean checkVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig);
|
||||||
|
|
||||||
|
|
||||||
TwoFaProviderType getType();
|
TwoFaProviderType getType();
|
||||||
|
|||||||
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* 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.provider.impl;
|
||||||
|
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.thingsboard.rule.engine.api.MailService;
|
||||||
|
import org.thingsboard.server.common.data.User;
|
||||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
||||||
|
import org.thingsboard.server.common.data.security.model.mfa.account.EmailTwoFaAccountConfig;
|
||||||
|
import org.thingsboard.server.common.data.security.model.mfa.provider.EmailTwoFaProviderConfig;
|
||||||
|
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
|
||||||
|
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||||
|
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@TbCoreComponent
|
||||||
|
public class EmailTwoFaProvider extends OtpBasedTwoFaProvider<EmailTwoFaProviderConfig, EmailTwoFaAccountConfig> {
|
||||||
|
|
||||||
|
private final MailService mailService;
|
||||||
|
|
||||||
|
protected EmailTwoFaProvider(CacheManager cacheManager, MailService mailService) {
|
||||||
|
super(cacheManager);
|
||||||
|
this.mailService = mailService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EmailTwoFaAccountConfig generateNewAccountConfig(User user, EmailTwoFaProviderConfig providerConfig) {
|
||||||
|
EmailTwoFaAccountConfig config = new EmailTwoFaAccountConfig();
|
||||||
|
config.setEmail(user.getEmail());
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void sendVerificationCode(SecurityUser user, String verificationCode, EmailTwoFaProviderConfig providerConfig, EmailTwoFaAccountConfig accountConfig) throws ThingsboardException {
|
||||||
|
mailService.sendTwoFaVerificationEmail(accountConfig.getEmail(), verificationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TwoFaProviderType getType() {
|
||||||
|
return TwoFaProviderType.EMAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -38,27 +38,27 @@ public abstract class OtpBasedTwoFaProvider<C extends OtpBasedTwoFaProviderConfi
|
|||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void prepareVerificationCode(SecurityUser securityUser, C providerConfig, A accountConfig) throws ThingsboardException {
|
public final void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException {
|
||||||
String verificationCode = RandomStringUtils.randomNumeric(6);
|
String verificationCode = RandomStringUtils.randomNumeric(6);
|
||||||
verificationCodesCache.put(securityUser.getId(), new Otp(System.currentTimeMillis(), verificationCode, accountConfig));
|
sendVerificationCode(user, verificationCode, providerConfig, accountConfig);
|
||||||
sendVerificationCode(securityUser, verificationCode, providerConfig, accountConfig);
|
verificationCodesCache.put(user.getId(), new Otp(System.currentTimeMillis(), verificationCode, accountConfig));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void sendVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) throws ThingsboardException;
|
protected abstract void sendVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) throws ThingsboardException;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, C providerConfig, A accountConfig) {
|
public final boolean checkVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) {
|
||||||
Otp correctVerificationCode = verificationCodesCache.get(securityUser.getId(), Otp.class);
|
Otp correctVerificationCode = verificationCodesCache.get(user.getId(), Otp.class);
|
||||||
if (correctVerificationCode != null) {
|
if (correctVerificationCode != null) {
|
||||||
if (System.currentTimeMillis() - correctVerificationCode.getTimestamp()
|
if (System.currentTimeMillis() - correctVerificationCode.getTimestamp()
|
||||||
> TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) {
|
> TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) {
|
||||||
verificationCodesCache.evict(securityUser.getId());
|
verificationCodesCache.evict(user.getId());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (verificationCode.equals(correctVerificationCode.getValue())
|
if (verificationCode.equals(correctVerificationCode.getValue())
|
||||||
&& accountConfig.equals(correctVerificationCode.getAccountConfig())) {
|
&& accountConfig.equals(correctVerificationCode.getAccountConfig())) {
|
||||||
verificationCodesCache.evict(securityUser.getId());
|
verificationCodesCache.evict(user.getId());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ public class TotpTwoFaProvider implements TwoFaProvider<TotpTwoFaProviderConfig,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TotpTwoFaProviderConfig providerConfig, TotpTwoFaAccountConfig accountConfig) {
|
public final boolean checkVerificationCode(SecurityUser user, 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,11 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.common.data.security.model.mfa;
|
package org.thingsboard.server.common.data.security.model.mfa;
|
||||||
|
|
||||||
import io.swagger.annotations.ApiModel;
|
|
||||||
import io.swagger.annotations.ApiModelProperty;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
|
|
||||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
|
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
|
||||||
|
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.Min;
|
import javax.validation.constraints.Min;
|
||||||
@ -28,29 +26,18 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@ApiModel
|
|
||||||
public class PlatformTwoFaSettings {
|
public class PlatformTwoFaSettings {
|
||||||
|
|
||||||
@ApiModelProperty(value = "Option for tenant admins to use 2FA settings configured by sysadmin. " +
|
|
||||||
"If this param is set to true, then the settings will not be validated for constraints " +
|
|
||||||
"(if it is a tenant admin; for sysadmin this param is ignored)")
|
|
||||||
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.")
|
|
||||||
@Valid
|
@Valid
|
||||||
private List<TwoFaProviderConfig> providers;
|
private List<TwoFaProviderConfig> providers;
|
||||||
|
|
||||||
@ApiModelProperty(value = "Rate limit configuration for verification code sending. The format is standard: 'amountOfRequests:periodInSeconds'. " +
|
|
||||||
"The value of '1:60' would limit verification code sending requests to one per minute.", example = "1:60", required = false)
|
|
||||||
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code send rate limit configuration is invalid")
|
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code send rate limit configuration is invalid")
|
||||||
private String verificationCodeSendRateLimit;
|
private String verificationCodeSendRateLimit;
|
||||||
@ApiModelProperty(value = "Rate limit configuration for verification code checking.", example = "3:900", required = false)
|
|
||||||
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid")
|
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid")
|
||||||
private String verificationCodeCheckRateLimit;
|
private String verificationCodeCheckRateLimit;
|
||||||
@ApiModelProperty(value = "Maximum number of verification failures before a user gets disabled.", example = "10", required = false)
|
|
||||||
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
|
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
|
||||||
private int maxVerificationFailuresBeforeUserLockout;
|
private int maxVerificationFailuresBeforeUserLockout;
|
||||||
@ApiModelProperty(value = "Total amount of time in seconds allotted for verification. " +
|
|
||||||
"Basically, this property sets a lifetime for pre-verification token. If not set, default value of 30 minutes is used.", example = "3600", required = false)
|
|
||||||
@Min(value = 1, message = "total amount of time allotted for verification must be greater than 0")
|
@Min(value = 1, message = "total amount of time allotted for verification must be greater than 0")
|
||||||
private Integer totalAllowedTimeForVerification;
|
private Integer totalAllowedTimeForVerification;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 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 lombok.EqualsAndHashCode;
|
||||||
|
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Email;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class EmailTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Email
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TwoFaProviderType getProviderType() {
|
||||||
|
return TwoFaProviderType.EMAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.common.data.security.model.mfa.account;
|
package org.thingsboard.server.common.data.security.model.mfa.account;
|
||||||
|
|
||||||
import io.swagger.annotations.ApiModel;
|
|
||||||
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.TwoFaProviderType;
|
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
|
||||||
@ -24,12 +22,10 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi
|
|||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.Pattern;
|
import javax.validation.constraints.Pattern;
|
||||||
|
|
||||||
@ApiModel
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Data
|
@Data
|
||||||
public class SmsTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig {
|
public class SmsTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig {
|
||||||
|
|
||||||
@ApiModelProperty(value = "Phone number to use for 2FA. Must no be blank and must be of E.164 number format.", required = true)
|
|
||||||
@NotBlank(message = "phone number cannot be blank")
|
@NotBlank(message = "phone number cannot be blank")
|
||||||
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format")
|
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format")
|
||||||
private String phoneNumber;
|
private String phoneNumber;
|
||||||
|
|||||||
@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.common.data.security.model.mfa.account;
|
package org.thingsboard.server.common.data.security.model.mfa.account;
|
||||||
|
|
||||||
import io.swagger.annotations.ApiModel;
|
|
||||||
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.TwoFaProviderType;
|
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
|
||||||
@ -24,13 +22,10 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi
|
|||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.Pattern;
|
import javax.validation.constraints.Pattern;
|
||||||
|
|
||||||
@ApiModel // FIXME [viacheslav]
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class TotpTwoFaAccountConfig extends TwoFaAccountConfig {
|
public class TotpTwoFaAccountConfig extends TwoFaAccountConfig {
|
||||||
|
|
||||||
@ApiModelProperty(value = "OTP auth URL used to generate a QR code to scan with an authenticator app. Must not be blank and must follow specific pattern.",
|
|
||||||
example = "otpauth://totp/ThingsBoard:tenant@thingsboard.org?issuer=ThingsBoard&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII", required = true)
|
|
||||||
@NotBlank(message = "OTP auth URL cannot be blank")
|
@NotBlank(message = "OTP auth URL cannot be blank")
|
||||||
@Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid")
|
@Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid")
|
||||||
private String authUrl;
|
private String authUrl;
|
||||||
|
|||||||
@ -29,7 +29,8 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi
|
|||||||
property = "providerType")
|
property = "providerType")
|
||||||
@JsonSubTypes({
|
@JsonSubTypes({
|
||||||
@Type(name = "TOTP", value = TotpTwoFaAccountConfig.class),
|
@Type(name = "TOTP", value = TotpTwoFaAccountConfig.class),
|
||||||
@Type(name = "SMS", value = SmsTwoFaAccountConfig.class)
|
@Type(name = "SMS", value = SmsTwoFaAccountConfig.class),
|
||||||
|
@Type(name = "EMAIL", value = EmailTwoFaAccountConfig.class)
|
||||||
})
|
})
|
||||||
@Data
|
@Data
|
||||||
public abstract class TwoFaAccountConfig {
|
public abstract class TwoFaAccountConfig {
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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.provider;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class EmailTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TwoFaProviderType getProviderType() {
|
||||||
|
return TwoFaProviderType.EMAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -15,15 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.common.data.security.model.mfa.provider;
|
package org.thingsboard.server.common.data.security.model.mfa.provider;
|
||||||
|
|
||||||
import io.swagger.annotations.ApiModelProperty;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.validation.constraints.Min;
|
import javax.validation.constraints.Min;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public abstract class OtpBasedTwoFaProviderConfig implements TwoFaProviderConfig {
|
public abstract class OtpBasedTwoFaProviderConfig implements TwoFaProviderConfig {
|
||||||
@ApiModelProperty(value = "Verification code lifetime in seconds. Verification codes with a lifetime bigger than this param " +
|
|
||||||
"will be considered incorrect", example = "60", required = true)
|
|
||||||
@Min(value = 1, message = "verification code lifetime is required")
|
@Min(value = 1, message = "verification code lifetime is required")
|
||||||
private int verificationCodeLifetime;
|
private int verificationCodeLifetime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,22 +15,16 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.common.data.security.model.mfa.provider;
|
package org.thingsboard.server.common.data.security.model.mfa.provider;
|
||||||
|
|
||||||
import io.swagger.annotations.ApiModel;
|
|
||||||
import io.swagger.annotations.ApiModelProperty;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
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 = OtpBasedTwoFaProviderConfig.class)
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Data
|
@Data
|
||||||
public class SmsTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig {
|
public class SmsTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig {
|
||||||
|
|
||||||
@ApiModelProperty(value = "SMS verification message template. Available template variables are ${verificationCode} and ${userEmail}. " +
|
|
||||||
"It must not be blank and must contain verification code variable.",
|
|
||||||
example = "Here is your verification code: ${verificationCode}", required = true)
|
|
||||||
@NotBlank(message = "verification message template is required")
|
@NotBlank(message = "verification message template is required")
|
||||||
@Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code")
|
@Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code")
|
||||||
private String smsVerificationMessageTemplate;
|
private String smsVerificationMessageTemplate;
|
||||||
|
|||||||
@ -15,18 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.common.data.security.model.mfa.provider;
|
package org.thingsboard.server.common.data.security.model.mfa.provider;
|
||||||
|
|
||||||
import io.swagger.annotations.ApiModel;
|
|
||||||
import io.swagger.annotations.ApiModelProperty;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
@ApiModel
|
|
||||||
@Data
|
@Data
|
||||||
public class TotpTwoFaProviderConfig implements TwoFaProviderConfig {
|
public class TotpTwoFaProviderConfig implements TwoFaProviderConfig {
|
||||||
|
|
||||||
@ApiModelProperty(value = "Issuer name that will be displayed in an authenticator app near a username. " +
|
|
||||||
"Must not be blank.", example = "ThingsBoard", required = true)
|
|
||||||
@NotBlank(message = "issuer name must not be blank")
|
@NotBlank(message = "issuer name must not be blank")
|
||||||
private String issuerName;
|
private String issuerName;
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
|||||||
property = "providerType")
|
property = "providerType")
|
||||||
@JsonSubTypes({
|
@JsonSubTypes({
|
||||||
@Type(name = "TOTP", value = TotpTwoFaProviderConfig.class),
|
@Type(name = "TOTP", value = TotpTwoFaProviderConfig.class),
|
||||||
@Type(name = "SMS", value = SmsTwoFaProviderConfig.class)
|
@Type(name = "SMS", value = SmsTwoFaProviderConfig.class),
|
||||||
|
@Type(name = "EMAIL", value = EmailTwoFaProviderConfig.class)
|
||||||
})
|
})
|
||||||
public interface TwoFaProviderConfig {
|
public interface TwoFaProviderConfig {
|
||||||
|
|
||||||
|
|||||||
@ -17,5 +17,6 @@ package org.thingsboard.server.common.data.security.model.mfa.provider;
|
|||||||
|
|
||||||
public enum TwoFaProviderType {
|
public enum TwoFaProviderType {
|
||||||
TOTP,
|
TOTP,
|
||||||
SMS
|
SMS,
|
||||||
|
EMAIL
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,8 +24,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
|
|||||||
import org.thingsboard.server.common.data.id.CustomerId;
|
import org.thingsboard.server.common.data.id.CustomerId;
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public interface MailService {
|
public interface MailService {
|
||||||
|
|
||||||
void updateMailConfiguration();
|
void updateMailConfiguration();
|
||||||
@ -46,6 +44,8 @@ public interface MailService {
|
|||||||
|
|
||||||
void sendAccountLockoutEmail(String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException;
|
void sendAccountLockoutEmail(String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException;
|
||||||
|
|
||||||
|
void sendTwoFaVerificationEmail(String email, String verificationCode) throws ThingsboardException;
|
||||||
|
|
||||||
void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail) throws ThingsboardException;
|
void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail) throws ThingsboardException;
|
||||||
|
|
||||||
void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender) throws ThingsboardException;
|
void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender) throws ThingsboardException;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user