2FA backup codes
This commit is contained in:
parent
0bb9d531cc
commit
8eb75f6e16
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<TwoFaProviderType, TwoFaAccountConfig> 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);
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ public interface TwoFaProvider<C extends TwoFaProviderConfig, A extends TwoFaAcc
|
||||
|
||||
default void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException {}
|
||||
|
||||
boolean checkVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig);
|
||||
boolean checkVerificationCode(SecurityUser user, String code, C providerConfig, A accountConfig);
|
||||
|
||||
default void check(TenantId tenantId) throws ThingsboardException {};
|
||||
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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.apache.commons.lang3.RandomStringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thingsboard.common.util.CollectionsUtil;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
import org.thingsboard.server.common.data.security.model.mfa.account.BackupCodeTwoFaAccountConfig;
|
||||
import org.thingsboard.server.common.data.security.model.mfa.provider.BackupCodeTwoFaProviderConfig;
|
||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
|
||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
|
||||
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
|
||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Service
|
||||
@TbCoreComponent
|
||||
public class BackupCodeTwoFaProvider implements TwoFaProvider<BackupCodeTwoFaProviderConfig, BackupCodeTwoFaAccountConfig> {
|
||||
|
||||
@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<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -48,7 +48,7 @@ public abstract class OtpBasedTwoFaProvider<C extends OtpBasedTwoFaProviderConfi
|
||||
|
||||
|
||||
@Override
|
||||
public final boolean checkVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) {
|
||||
public final boolean checkVerificationCode(SecurityUser user, String code, C providerConfig, A accountConfig) {
|
||||
Otp correctVerificationCode = verificationCodesCache.get(user.getId(), Otp.class);
|
||||
if (correctVerificationCode != null) {
|
||||
if (System.currentTimeMillis() - correctVerificationCode.getTimestamp()
|
||||
@ -56,7 +56,7 @@ public abstract class OtpBasedTwoFaProvider<C extends OtpBasedTwoFaProviderConfi
|
||||
verificationCodesCache.evict(user.getId());
|
||||
return false;
|
||||
}
|
||||
if (verificationCode.equals(correctVerificationCode.getValue())
|
||||
if (code.equals(correctVerificationCode.getValue())
|
||||
&& accountConfig.equals(correctVerificationCode.getAccountConfig())) {
|
||||
verificationCodesCache.evict(user.getId());
|
||||
return true;
|
||||
|
||||
@ -45,9 +45,9 @@ public class TotpTwoFaProvider implements TwoFaProvider<TotpTwoFaProviderConfig,
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean checkVerificationCode(SecurityUser user, String verificationCode, TotpTwoFaProviderConfig providerConfig, TotpTwoFaAccountConfig accountConfig) {
|
||||
public final boolean checkVerificationCode(SecurityUser user, String code, TotpTwoFaProviderConfig providerConfig, TotpTwoFaAccountConfig accountConfig) {
|
||||
String secretKey = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build().getQueryParams().getFirst("secret");
|
||||
return new Totp(secretKey).verify(verificationCode);
|
||||
return new Totp(secretKey).verify(code);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
|
||||
@ -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.common.data.security.model.mfa.account;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BackupCodeTwoFaAccountConfig extends TwoFaAccountConfig {
|
||||
|
||||
@NotEmpty
|
||||
private Set<String> codes;
|
||||
|
||||
@Override
|
||||
public TwoFaProviderType getProviderType() {
|
||||
return TwoFaProviderType.BACKUP_CODE;
|
||||
}
|
||||
|
||||
|
||||
@JsonGetter("codes")
|
||||
private Set<String> getCodesForJson() {
|
||||
if (serializeHiddenFields) {
|
||||
return codes;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonGetter
|
||||
private Integer getCodesLeft() {
|
||||
if (codes != null) {
|
||||
return codes.size();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
|
||||
@ -18,5 +18,6 @@ package org.thingsboard.server.common.data.security.model.mfa.provider;
|
||||
public enum TwoFaProviderType {
|
||||
TOTP,
|
||||
SMS,
|
||||
EMAIL
|
||||
EMAIL,
|
||||
BACKUP_CODE
|
||||
}
|
||||
|
||||
@ -34,4 +34,9 @@ public class CollectionsUtil {
|
||||
public static <T> Set<T> diffSets(Set<T> a, Set<T> b) {
|
||||
return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static <T> boolean contains(Collection<T> collection, T element) {
|
||||
return isNotEmpty(collection) && collection.contains(element);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user