diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java index 87e54ec196..138b20b160 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java @@ -140,14 +140,18 @@ public class TwoFaConfigController extends BaseController { @PostMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@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) - @RequestParam String verificationCode) throws Exception { + @RequestParam(required = false) String verificationCode) throws Exception { 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; + if (accountConfig.getProviderType() != TwoFaProviderType.BACKUP_CODE) { + verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false); + } else { + verificationSuccess = true; + } if (verificationSuccess) { return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); } else { diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 5eb1f6530d..003b4ab450 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -16,7 +16,6 @@ package org.thingsboard.server.controller; import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -34,11 +33,11 @@ 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.account.EmailTwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; -import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; @@ -46,7 +45,6 @@ import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; - import java.util.Collections; import java.util.List; import java.util.Optional; @@ -90,7 +88,6 @@ public class TwoFactorAuthController extends BaseController { @PostMapping("/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") public JwtTokenPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType, - @ApiParam(value = "6-digit verification code", required = true) @RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index d38483d0de..5929f04468 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -122,7 +122,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); boolean verificationSuccess; - if (StringUtils.isNumeric(verificationCode) && verificationCode.length() == 6) { + if (StringUtils.isNotBlank(verificationCode)) { verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(user, verificationCode, providerConfig, accountConfig); } else { verificationSuccess = false; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java index e27d3967ad..bfe426d532 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoF import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; -import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.settings.AdminSettingsDao; import org.thingsboard.server.dao.settings.AdminSettingsService; @@ -39,6 +38,7 @@ import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.Map; import java.util.Optional; @Service @@ -74,7 +74,9 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { return newUserAuthSettings; }); userAuthSettings.setTwoFaSettings(settings); + settings.getConfigs().values().forEach(accountConfig -> accountConfig.setSerializeHiddenFields(true)); userAuthSettingsDao.save(tenantId, userAuthSettings); + settings.getConfigs().values().forEach(accountConfig -> accountConfig.setSerializeHiddenFields(false)); return settings; } @@ -96,12 +98,13 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { newSettings.setConfigs(new LinkedHashMap<>()); return newSettings; }); + Map configs = settings.getConfigs(); if (accountConfig.isUseByDefault()) { - settings.getConfigs().values().forEach(config -> config.setUseByDefault(false)); + configs.values().forEach(config -> config.setUseByDefault(false)); } - settings.getConfigs().put(accountConfig.getProviderType(), accountConfig); - if (settings.getConfigs().values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) { - settings.getConfigs().values().stream().findFirst().ifPresent(config -> config.setUseByDefault(true)); + configs.put(accountConfig.getProviderType(), accountConfig); + if (configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) { + configs.values().stream().findFirst().ifPresent(config -> config.setUseByDefault(true)); } return saveAccountTwoFaSettings(tenantId, userId, settings); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java index 19cc106657..2522d3e41b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java @@ -29,7 +29,7 @@ public interface TwoFaProvider { + + @Autowired @Lazy + private TwoFaConfigManager twoFaConfigManager; + + @Override + public BackupCodeTwoFaAccountConfig generateNewAccountConfig(User user, BackupCodeTwoFaProviderConfig providerConfig) { + BackupCodeTwoFaAccountConfig config = new BackupCodeTwoFaAccountConfig(); + config.setCodes(generateCodes(providerConfig.getCodesQuantity(), 8)); + config.setSerializeHiddenFields(true); + return config; + } + + private static Set generateCodes(int count, int length) { + return Stream.generate(() -> RandomStringUtils.random(length, "0123456789abcdef")) + .distinct().limit(count) + .collect(Collectors.toSet()); + } + + @Override + public boolean checkVerificationCode(SecurityUser user, String code, BackupCodeTwoFaProviderConfig providerConfig, BackupCodeTwoFaAccountConfig accountConfig) { + if (CollectionsUtil.contains(accountConfig.getCodes(), code)) { + accountConfig.getCodes().remove(code); + twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); + return true; + } else { + return false; + } + } + + @Override + public TwoFaProviderType getType() { + return TwoFaProviderType.BACKUP_CODE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java index f270316d2d..d8df97afa7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java @@ -48,7 +48,7 @@ public abstract class OtpBasedTwoFaProvider codes; + + @Override + public TwoFaProviderType getProviderType() { + return TwoFaProviderType.BACKUP_CODE; + } + + + @JsonGetter("codes") + private Set getCodesForJson() { + if (serializeHiddenFields) { + return codes; + } else { + return null; + } + } + + @JsonGetter + private Integer getCodesLeft() { + if (codes != null) { + return codes.size(); + } else { + return null; + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java index 6bf2ee8fd3..706ce776d8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java @@ -30,13 +30,17 @@ import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProvi @JsonSubTypes({ @Type(name = "TOTP", value = TotpTwoFaAccountConfig.class), @Type(name = "SMS", value = SmsTwoFaAccountConfig.class), - @Type(name = "EMAIL", value = EmailTwoFaAccountConfig.class) + @Type(name = "EMAIL", value = EmailTwoFaAccountConfig.class), + @Type(name = "BACKUP_CODE", value = BackupCodeTwoFaAccountConfig.class) }) @Data public abstract class TwoFaAccountConfig { private boolean useByDefault; + @JsonIgnore + protected transient boolean serializeHiddenFields; + @JsonIgnore public abstract TwoFaProviderType getProviderType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java new file mode 100644 index 0000000000..9dc2a00b23 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java @@ -0,0 +1,33 @@ +/** + * 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 javax.validation.constraints.Min; + +@Data +public class BackupCodeTwoFaProviderConfig implements TwoFaProviderConfig { + + @Min(0) + private int codesQuantity; + + @Override + public TwoFaProviderType getProviderType() { + return TwoFaProviderType.BACKUP_CODE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java index 65d9242900..5a87d58467 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java @@ -28,7 +28,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes({ @Type(name = "TOTP", value = TotpTwoFaProviderConfig.class), @Type(name = "SMS", value = SmsTwoFaProviderConfig.class), - @Type(name = "EMAIL", value = EmailTwoFaProviderConfig.class) + @Type(name = "EMAIL", value = EmailTwoFaProviderConfig.class), + @Type(name = "BACKUP_CODE", value = BackupCodeTwoFaProviderConfig.class) }) public interface TwoFaProviderConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java index dd36f24394..a2373319a2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java @@ -18,5 +18,6 @@ package org.thingsboard.server.common.data.security.model.mfa.provider; public enum TwoFaProviderType { TOTP, SMS, - EMAIL + EMAIL, + BACKUP_CODE } diff --git a/common/util/src/main/java/org/thingsboard/common/util/CollectionsUtil.java b/common/util/src/main/java/org/thingsboard/common/util/CollectionsUtil.java index c995aa1e11..7e39f9a273 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/CollectionsUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/CollectionsUtil.java @@ -34,4 +34,9 @@ public class CollectionsUtil { public static Set diffSets(Set a, Set b) { return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toSet()); } + + public static boolean contains(Collection collection, T element) { + return isNotEmpty(collection) && collection.contains(element); + } + }