Browse Source

2FA: rate limiting, validation, refactoring

pull/6235/head
Viacheslav Klimov 5 years ago
parent
commit
0c36d4809c
  1. 3
      application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
  2. 124
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java
  3. 142
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  4. 151
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  5. 163
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java
  6. 139
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java
  7. 41
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthConfigManager.java
  8. 20
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java
  9. 13
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java
  11. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java
  12. 7
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java
  13. 7
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java
  14. 14
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java
  15. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  16. 10
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  17. 54
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  18. 4
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  19. 1
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  20. 2
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

3
application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java

@ -15,8 +15,11 @@
*/
package org.thingsboard.server.config;
import io.github.bucket4j.Bucket4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.jcache.JCacheCacheManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

124
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java

@ -0,0 +1,124 @@
/**
* 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.controller;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/2fa")
@RequiredArgsConstructor
public class TwoFactorAuthConfigController extends BaseController {
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager;
private final TwoFactorAuthService twoFactorAuthService;
@GetMapping("/account/config")
@PreAuthorize("isAuthenticated()")
public TwoFactorAuthAccountConfig getTwoFaAccountConfig() throws ThingsboardException {
SecurityUser user = getCurrentUser();
return twoFactorAuthConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null);
}
@PostMapping("/account/config/generate")
@PreAuthorize("isAuthenticated()")
public TwoFactorAuthAccountConfig generateTwoFaAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser();
return twoFactorAuthService.generateNewAccountConfig(user, providerType);
}
/* TMP */
@PostMapping("/account/config/generate/qr")
@PreAuthorize("isAuthenticated()")
public void generateTwoFaAccountConfigWithQr(@RequestParam TwoFactorAuthProviderType providerType, HttpServletResponse response) throws Exception {
TwoFactorAuthAccountConfig config = generateTwoFaAccountConfig(providerType);
if (providerType == TwoFactorAuthProviderType.TOTP) {
BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFactorAuthAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200);
try (ServletOutputStream outputStream = response.getOutputStream()) {
MatrixToImageWriter.writeToStream(qr, "PNG", outputStream);
}
}
response.setHeader("config", JacksonUtil.toString(config));
}
/* TMP */
@PostMapping("/account/config/submit")
@PreAuthorize("isAuthenticated()")
public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception {
SecurityUser user = getCurrentUser();
twoFactorAuthService.prepareVerificationCode(user, accountConfig, false);
}
@PostMapping("/account/config")
@PreAuthorize("isAuthenticated()")
public void verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig,
@RequestParam String verificationCode) throws Exception {
SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false);
if (verificationSuccess) {
twoFactorAuthConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
} else {
throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.INVALID_ARGUMENTS);
}
}
@DeleteMapping("/account/config")
@PreAuthorize("isAuthenticated()")
public void deleteTwoFactorAuthAccountConfig() throws ThingsboardException {
SecurityUser user = getCurrentUser();
twoFactorAuthConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId());
}
@GetMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException {
return twoFactorAuthConfigManager.getTwoFaSettings(getTenantId()).orElse(null);
}
@PostMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void saveTwoFactorAuthSettings(@RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException {
twoFactorAuthConfigManager.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings);
}
}

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

@ -15,42 +15,22 @@
*/
package org.thingsboard.server.controller;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
/*
*
* TODO [viacheslav]:
* - Configurable softlock after XX (3) attempts: XX (15) mins - on session level
* - Configurable hardlock (user blocking) after a total of XX (10) unsuccessful attempts - on user level
*
* FIXME [viacheslav]:
@ -69,7 +49,7 @@ import java.util.Map;
* token to configure 2FA account config); also will need to make users configure 2FA during activation and password setup...
* */
@RestController
@RequestMapping("/api")
@RequestMapping("/api/auth/2fa")
@RequiredArgsConstructor
public class TwoFactorAuthController extends BaseController {
@ -77,132 +57,22 @@ public class TwoFactorAuthController extends BaseController {
private final JwtTokenFactory tokenFactory;
@GetMapping("/2fa/account/config")
@PreAuthorize("isAuthenticated()")
public TwoFactorAuthAccountConfig getTwoFactorAuthAccountConfig() throws ThingsboardException {
SecurityUser user = getCurrentUser();
return twoFactorAuthService.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null);
}
@PostMapping("/2fa/account/config/generate")
@PreAuthorize("isAuthenticated()")
public TwoFactorAuthAccountConfig generateTwoFactorAuthAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser();
return twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), providerType,
(provider, providerConfig) -> {
return provider.generateNewAccountConfig(user, providerConfig);
});
}
// temporary endpoint for testing purposes
@PostMapping("/2fa/account/config/generate/qr")
@PreAuthorize("isAuthenticated()")
public void generateTwoFactorAuthAccountConfigWithQr(@RequestParam TwoFactorAuthProviderType providerType, HttpServletResponse response) throws Exception {
TwoFactorAuthAccountConfig config = generateTwoFactorAuthAccountConfig(providerType);
if (providerType == TwoFactorAuthProviderType.TOTP) {
BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFactorAuthAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200);
try (ServletOutputStream outputStream = response.getOutputStream()) {
MatrixToImageWriter.writeToStream(qr, "PNG", outputStream);
}
}
response.setHeader("config", JacksonUtil.toString(config));
}
@PostMapping("/2fa/account/config/submit")
@PreAuthorize("isAuthenticated()")
public void submitTwoFactorAuthAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception {
SecurityUser user = getCurrentUser();
twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(),
(provider, providerConfig) -> {
provider.prepareVerificationCode(user, providerConfig, accountConfig);
});
}
@PostMapping("/2fa/account/config")
@PreAuthorize("isAuthenticated()")
public void verifyAndSaveTwoFactorAuthAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig,
@RequestParam String verificationCode) throws Exception {
SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(),
(provider, providerConfig) -> {
return provider.checkVerificationCode(user, verificationCode, providerConfig, accountConfig);
});
if (verificationSuccess) {
twoFactorAuthService.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
} else {
throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.INVALID_ARGUMENTS);
}
}
@GetMapping("/2fa/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException {
return twoFactorAuthService.getTwoFaSettings(getTenantId()).orElse(null);
}
@PostMapping("/2fa/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void saveTwoFactorAuthSettings(@Valid @RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException {
twoFactorAuthService.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings);
}
private final Map<String, TbRateLimits> verificationCodeSendRateLimits = new HashMap<>();
private final Map<String, TbRateLimits> verificationCodeCheckRateLimits = new HashMap<>();
@PostMapping("/auth/2fa/verification/send")
@PostMapping("/verification/send")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public void sendTwoFaVerificationCode() throws Exception {
SecurityUser user = getCurrentUser();
TwoFactorAuthSettings twoFaSettings = twoFactorAuthService.getTwoFaSettings(user.getTenantId()).get();
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) {
TbRateLimits rateLimits = verificationCodeSendRateLimits.computeIfAbsent(user.getSessionId(), sessionId -> {
return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit());
});
if (!rateLimits.tryConsume()) {
throw new ThingsboardException(ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
}
twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(),
(provider, providerConfig, accountConfig) -> {
provider.prepareVerificationCode(user, providerConfig, accountConfig);
});
twoFactorAuthService.prepareVerificationCode(user, true);
}
@PostMapping("/auth/2fa/verification/check")
@PostMapping("/verification/check")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws Exception {
SecurityUser user = getCurrentUser();
// FIXME [viacheslav]: rate limits for verification code check
boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(),
(provider, providerConfig, accountConfig) -> {
return provider.checkVerificationCode(user, verificationCode, providerConfig, accountConfig);
});
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true);
if (verificationSuccess) {
// FIXME [viacheslav]: log login action
return tokenFactory.createTokenPair(user);
} else {
TwoFactorAuthSettings twoFaSettings = twoFactorAuthService.getTwoFaSettings(user.getTenantId()).get();
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) {
TbRateLimits rateLimits = verificationCodeSendRateLimits.computeIfAbsent(user.getSessionId(), sessionId -> {
return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit());
});
if (!rateLimits.tryConsume()) {
throw new ThingsboardException(ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
}
throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION);
}
}

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

@ -0,0 +1,151 @@
/**
* 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;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
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.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Service
@RequiredArgsConstructor
public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
private final TwoFactorAuthConfigManager configManager;
private final SystemSecurityService systemSecurityService;
private final UserService userService;
private final Map<TwoFactorAuthProviderType, TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>> providers = new EnumMap<>(TwoFactorAuthProviderType.class);
// FIXME [viacheslav]: remove from the map
// TODO [viacheslav]: these rate limits are local, and will work bad in the cluster
private final ConcurrentMap<String, TbRateLimits> verificationCodeSendingRateLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<String, TbRateLimits> verificationCodeCheckingRateLimits = new ConcurrentHashMap<>();
private static final ThingsboardException ACCOUNT_NOT_CONFIGURED = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
private static final ThingsboardException PROVIDER_NOT_CONFIGURED = new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
private static final ThingsboardException PROVIDER_NOT_AVAILABLE = new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.GENERAL);
@Override
public void prepareVerificationCode(SecurityUser securityUser, boolean rateLimit) throws Exception {
TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId())
.orElseThrow(() -> ACCOUNT_NOT_CONFIGURED);
prepareVerificationCode(securityUser, accountConfig, rateLimit);
}
@Override
public void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException {
TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId())
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED);
if (rateLimit) {
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) {
TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getSessionId(), sessionId -> {
return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit());
});
if (!rateLimits.tryConsume()) {
throw new ThingsboardException("Too many verification code sending requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
}
}
TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED);
getTwoFaProvider(accountConfig.getProviderType()).prepareVerificationCode(securityUser, providerConfig, accountConfig);
}
@Override
public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean rateLimit) throws ThingsboardException {
TwoFactorAuthAccountConfig accountConfig = configManager.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId())
.orElseThrow(() -> ACCOUNT_NOT_CONFIGURED);
return checkVerificationCode(securityUser, verificationCode, accountConfig, rateLimit);
}
@Override
public boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException {
if (!userService.findUserCredentialsByUserId(securityUser.getTenantId(), securityUser.getId()).isEnabled()) {
throw new ThingsboardException("User is disabled", ThingsboardErrorCode.AUTHENTICATION);
}
TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId())
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED);
if (rateLimit) {
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) {
TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getSessionId(), sessionId -> {
return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit());
});
if (!rateLimits.tryConsume()) {
throw new ThingsboardException("Too many verification code checking requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
}
}
TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED);
boolean verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(securityUser, verificationCode, providerConfig, accountConfig);
if (rateLimit) {
systemSecurityService.validateTwoFaVerification(securityUser.getTenantId(), securityUser.getId(), verificationSuccess, twoFaSettings);
}
return verificationSuccess;
}
@Override
public TwoFactorAuthAccountConfig generateNewAccountConfig(User user, TwoFactorAuthProviderType providerType) throws ThingsboardException {
TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(user.getTenantId(), providerType);
return getTwoFaProvider(providerType).generateNewAccountConfig(user, providerConfig);
}
private TwoFactorAuthProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) throws ThingsboardException {
return configManager.getTwoFaSettings(tenantId)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType))
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED);
}
private TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> getTwoFaProvider(TwoFactorAuthProviderType providerType) throws ThingsboardException {
return Optional.ofNullable(providers.get(providerType))
.orElseThrow(() -> PROVIDER_NOT_AVAILABLE);
}
@Autowired
private void setProviders(Collection<TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>> providers) {
providers.forEach(provider -> {
this.providers.put(provider.getType(), provider);
});
}
}

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

@ -15,171 +15,22 @@
*/
package org.thingsboard.server.service.security.auth.mfa;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThrowingBiConsumer;
import org.thingsboard.common.util.ThrowingBiFunction;
import org.thingsboard.common.util.ThrowingTripleConsumer;
import org.thingsboard.common.util.ThrowingTripleFunction;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
public interface TwoFactorAuthService {
@Service
@RequiredArgsConstructor
public class TwoFactorAuthService {
void prepareVerificationCode(SecurityUser securityUser, boolean rateLimit) throws Exception;
private final UserService userService;
private final AdminSettingsService adminSettingsService;
private final AttributesService attributesService;
private final Map<TwoFactorAuthProviderType, TwoFactorAuthProvider<?, ?>> providers = new EnumMap<>(TwoFactorAuthProviderType.class);
void prepareVerificationCode(SecurityUser securityUser, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException;
protected static final String TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY = "twoFaConfig";
protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings";
boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, boolean rateLimit) throws ThingsboardException;
boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TwoFactorAuthAccountConfig accountConfig, boolean rateLimit) throws ThingsboardException;
public <R> R processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, ThrowingBiFunction<TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>, TwoFactorAuthProviderConfig, R> function) throws Exception {
TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, providerType)
.orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS));
TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> provider = getTwoFaProvider(providerType)
.orElseThrow(() -> new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.ITEM_NOT_FOUND));
return function.apply(provider, providerConfig);
}
public void processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, ThrowingBiConsumer<TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>, TwoFactorAuthProviderConfig> function) throws Exception {
processByTwoFaProvider(tenantId, providerType, (provider, providerConfig) -> {
function.accept(provider, providerConfig);
return null;
});
}
public <R> R processByTwoFaProvider(TenantId tenantId, UserId userId, ThrowingTripleFunction<TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig, R> function) throws Exception {
TwoFactorAuthAccountConfig accountConfig = getTwoFaAccountConfig(tenantId, userId)
.orElseThrow(() -> new ThingsboardException("2FA is not configured for user", ThingsboardErrorCode.BAD_REQUEST_PARAMS));
TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
.orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS));
TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> provider = getTwoFaProvider(accountConfig.getProviderType())
.orElseThrow(() -> new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.ITEM_NOT_FOUND));
return function.apply(provider, providerConfig, accountConfig);
}
public void processByTwoFaProvider(TenantId tenantId, UserId userId, ThrowingTripleConsumer<TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> function) throws Exception {
processByTwoFaProvider(tenantId, userId, (provider, providerConfig, accountConfig) -> {
function.accept(provider, providerConfig, accountConfig);
return null;
});
}
public Optional<TwoFactorAuthAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId) {
User user = userService.findUserById(tenantId, userId);
return Optional.ofNullable(user.getAdditionalInfo())
.flatMap(additionalInfo -> Optional.ofNullable(additionalInfo.get(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY)).filter(jsonNode -> !jsonNode.isNull()))
.map(jsonNode -> JacksonUtil.treeToValue(jsonNode, TwoFactorAuthAccountConfig.class))
.filter(twoFactorAuthAccountConfig -> {
return getTwoFaProviderConfig(tenantId, twoFactorAuthAccountConfig.getProviderType()).isPresent();
});
}
public void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException {
getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
.orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS));
User user = userService.findUserById(tenantId, userId);
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo())
.orElseGet(JacksonUtil::newObjectNode);
additionalInfo.set(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY, JacksonUtil.valueToTree(accountConfig));
user.setAdditionalInfo(additionalInfo);
userService.saveUser(user);
}
public void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId) {
User user = userService.findUserById(tenantId, userId);
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo())
.orElseGet(JacksonUtil::newObjectNode);
additionalInfo.remove(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY);
user.setAdditionalInfo(additionalInfo);
userService.saveUser(user);
}
@SneakyThrows({InterruptedException.class, ExecutionException.class})
public Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId) {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TwoFactorAuthSettings.class));
} else {
return attributesService.find(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get()
.map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class))
.filter(tenantTwoFactorAuthSettings -> !tenantTwoFactorAuthSettings.isUseSystemTwoFactorAuthSettings())
.or(() -> getTwoFaSettings(TenantId.SYS_TENANT_ID));
}
}
@SneakyThrows({InterruptedException.class, ExecutionException.class})
public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.orElseGet(() -> {
AdminSettings newSettings = new AdminSettings();
newSettings.setKey(TWO_FACTOR_AUTH_SETTINGS_KEY);
return newSettings;
});
settings.setJsonValue(JacksonUtil.valueToTree(twoFactorAuthSettings));
adminSettingsService.saveAdminSettings(tenantId, settings);
} else {
attributesService.save(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, Collections.singletonList(
new BaseAttributeKvEntry(new JsonDataEntry(TWO_FACTOR_AUTH_SETTINGS_KEY, JacksonUtil.toString(twoFactorAuthSettings)), System.currentTimeMillis())
)).get();
}
}
private <A extends TwoFactorAuthAccountConfig, C extends TwoFactorAuthProviderConfig> Optional<TwoFactorAuthProvider<C, A>> getTwoFaProvider(TwoFactorAuthProviderType providerType) {
return Optional.of((TwoFactorAuthProvider<C, A>) providers.get(providerType));
}
private <C extends TwoFactorAuthProviderConfig> Optional<C> getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) {
return getTwoFaSettings(tenantId)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType))
.map(providerConfig -> (C) providerConfig);
}
@Autowired
private void setProviders(Collection<TwoFactorAuthProvider<?, ?>> providers) {
providers.forEach(provider -> {
this.providers.put(provider.getType(), provider);
});
}
TwoFactorAuthAccountConfig generateNewAccountConfig(User user, TwoFactorAuthProviderType providerType) throws ThingsboardException;
}

139
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFactorAuthConfigManager.java

@ -0,0 +1,139 @@
/**
* 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 com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@Service
@RequiredArgsConstructor
public class DefaultTwoFactorAuthConfigManager implements TwoFactorAuthConfigManager {
private final UserService userService;
private final AdminSettingsService adminSettingsService;
private final AttributesService attributesService;
protected static final String TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY = "twoFaConfig";
protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings";
@Override
public boolean isTwoFaEnabled(User user) {
return getTwoFaAccountConfig(user.getTenantId(), user.getId()).isPresent();
}
@Override
public Optional<TwoFactorAuthAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId) {
User user = userService.findUserById(tenantId, userId);
return Optional.ofNullable(user.getAdditionalInfo())
.flatMap(additionalInfo -> Optional.ofNullable(additionalInfo.get(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY)).filter(jsonNode -> !jsonNode.isNull()))
.map(jsonNode -> JacksonUtil.treeToValue(jsonNode, TwoFactorAuthAccountConfig.class))
.filter(twoFactorAuthAccountConfig -> {
return getTwoFaProviderConfig(tenantId, twoFactorAuthAccountConfig.getProviderType()).isPresent();
});
}
@Override
public void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException {
getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
.orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS));
User user = userService.findUserById(tenantId, userId);
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo())
.orElseGet(JacksonUtil::newObjectNode);
additionalInfo.set(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY, JacksonUtil.valueToTree(accountConfig));
user.setAdditionalInfo(additionalInfo);
userService.saveUser(user);
}
@Override
public void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId) {
User user = userService.findUserById(tenantId, userId);
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo())
.orElseGet(JacksonUtil::newObjectNode);
additionalInfo.remove(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY);
user.setAdditionalInfo(additionalInfo);
userService.saveUser(user);
}
private Optional<TwoFactorAuthProviderConfig> getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) {
return getTwoFaSettings(tenantId)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType));
}
@SneakyThrows({InterruptedException.class, ExecutionException.class})
@Override
public Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId) {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TwoFactorAuthSettings.class));
} else {
return attributesService.find(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get()
.map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class))
.filter(tenantTwoFactorAuthSettings -> !tenantTwoFactorAuthSettings.isUseSystemTwoFactorAuthSettings())
.or(() -> getTwoFaSettings(TenantId.SYS_TENANT_ID));
}
}
@SneakyThrows({InterruptedException.class, ExecutionException.class})
@Override
public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) {
if (tenantId.equals(TenantId.SYS_TENANT_ID) || !twoFactorAuthSettings.isUseSystemTwoFactorAuthSettings()) {
ConstraintValidator.validateFields(twoFactorAuthSettings);
}
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.orElseGet(() -> {
AdminSettings newSettings = new AdminSettings();
newSettings.setKey(TWO_FACTOR_AUTH_SETTINGS_KEY);
return newSettings;
});
settings.setJsonValue(JacksonUtil.valueToTree(twoFactorAuthSettings));
adminSettingsService.saveAdminSettings(tenantId, settings);
} else {
attributesService.save(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, Collections.singletonList(
new BaseAttributeKvEntry(new JsonDataEntry(TWO_FACTOR_AUTH_SETTINGS_KEY, JacksonUtil.toString(twoFactorAuthSettings)), System.currentTimeMillis())
)).get();
}
}
}

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

@ -0,0 +1,41 @@
/**
* 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.User;
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.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
import java.util.Optional;
public interface TwoFactorAuthConfigManager {
boolean isTwoFaEnabled(User user);
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);
void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings);
}

20
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java

@ -15,12 +15,14 @@
*/
package org.thingsboard.server.service.security.auth.mfa.config;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.checkerframework.checker.index.qual.NonNegative;
import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import javax.validation.Valid;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@ -30,17 +32,21 @@ import java.util.Optional;
@Data
public class TwoFactorAuthSettings {
@NotNull
private Boolean useSystemTwoFactorAuthSettings;
private boolean useSystemTwoFactorAuthSettings;
@Valid
private List<TwoFactorAuthProviderConfig> providers;
@Pattern(regexp = "\\d+:\\d+")
private String verificationCodeSendRateLimit; // 1:60 - one time in a minute
@Pattern(regexp = "\\d+:\\d+")
private String verificationCodeCheckRateLimit; // soft lockout, on session level
@ApiModelProperty(example = "1:60 (1 request per minute)")
@Pattern(regexp = "[^0]\\d+:[^0]\\d+", message = "Rate limit configuration is invalid")
private String verificationCodeSendRateLimit;
@ApiModelProperty(example = "3:900 (3 requests per 15 minutes)")
@Pattern(regexp = "[^0]\\d+:[^0]\\d+", message = "Rate limit configuration is invalid")
private String verificationCodeCheckRateLimit;
@Min(0)
private Integer maxVerificationCodeSubmitAttemptsBeforeUserBlocking;
private int maxCodeVerificationFailuresBeforeUserLockout;
@ApiModelProperty(value = "in seconds")
@Min(1)
private int totalAllowedTimeForVerification;
public Optional<TwoFactorAuthProviderConfig> getProviderConfig(TwoFactorAuthProviderType providerType) {

13
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/EmailTwoFactorAuthAccountConfig.java

@ -17,13 +17,18 @@ package org.thingsboard.server.service.security.auth.mfa.config.account;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Email;
@EqualsAndHashCode(callSuper = true)
@Data
public class EmailTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig {
private boolean useAccountEmail; // TODO [viacheslav]: validate
private boolean useAccountEmail;
@Email(message = "Email is not valid")
private String email;
@Override
@ -31,4 +36,10 @@ public class EmailTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccoun
return TwoFactorAuthProviderType.EMAIL;
}
@AssertTrue(message = "Email must be specified") // TODO [viacheslav]: test !
private boolean isValid() {
return useAccountEmail || StringUtils.isNotEmpty(email);
}
}

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java

@ -20,12 +20,14 @@ import lombok.EqualsAndHashCode;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@EqualsAndHashCode(callSuper = true)
@Data
public class SmsTwoFactorAuthAccountConfig extends OtpBasedTwoFactorAuthAccountConfig {
@NotBlank
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "Phone number is not of E.164 format")
private String phoneNumber;
@Override

6
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.auth.mfa.config.account;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
@ -26,8 +27,9 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr
use = JsonTypeInfo.Id.NAME,
property = "providerType")
@JsonSubTypes({
@JsonSubTypes.Type(value = TotpTwoFactorAuthAccountConfig.class, name = "TOTP"),
@JsonSubTypes.Type(value = SmsTwoFactorAuthAccountConfig.class, name = "SMS"),
@Type(name = "TOTP", value = TotpTwoFactorAuthAccountConfig.class ),
@Type(name = "SMS", value = SmsTwoFactorAuthAccountConfig.class),
@Type(name = "EMAIL", value = EmailTwoFactorAuthAccountConfig.class)
})
public interface TwoFactorAuthAccountConfig {

7
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/OtpBasedTwoFactorAuthProviderConfig.java

@ -15,9 +15,14 @@
*/
package org.thingsboard.server.service.security.auth.mfa.config.provider;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Min;
@Data
public abstract class OtpBasedTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig {
private Integer verificationCodeLifetime; // seconds
@ApiModelProperty(value = "in seconds", example = "60")
@Min(1) // TODO [viacheslav]: test
private int verificationCodeLifetime;
}

7
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java

@ -18,7 +18,9 @@ package org.thingsboard.server.service.security.auth.mfa.config.provider;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.service.security.auth.mfa.config.account.EmailTwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
@JsonIgnoreProperties(ignoreUnknown = true)
@ -26,8 +28,9 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr
use = JsonTypeInfo.Id.NAME,
property = "providerType")
@JsonSubTypes({
@JsonSubTypes.Type(value = TotpTwoFactorAuthProviderConfig.class, name = "TOTP"),
@JsonSubTypes.Type(value = SmsTwoFactorAuthProviderConfig.class, name = "SMS"),
@Type(name = "TOTP", value = TotpTwoFactorAuthProviderConfig.class),
@Type(name = "SMS", value = SmsTwoFactorAuthProviderConfig.class),
@Type(name = "EMAIL", value = EmailTwoFactorAuthAccountConfig.class)
})
public interface TwoFactorAuthProviderConfig {

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

@ -40,8 +40,7 @@ public abstract class OtpBasedTwoFactorAuthProvider<C extends OtpBasedTwoFactorA
@Override
public final void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException {
String verificationCode = RandomStringUtils.randomNumeric(6);
verificationCodesCache.put(user.getSessionId(), new Otp(System.currentTimeMillis(), verificationCode));
verificationCodesCache.put(user.getSessionId(), new Otp(System.currentTimeMillis(), verificationCode, accountConfig));
sendVerificationCode(user, verificationCode, providerConfig, accountConfig);
}
@ -51,8 +50,14 @@ public abstract class OtpBasedTwoFactorAuthProvider<C extends OtpBasedTwoFactorA
@Override
public final boolean checkVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) {
Otp correctVerificationCode = verificationCodesCache.get(user.getSessionId(), Otp.class);
if (correctVerificationCode != null && verificationCode.equals(correctVerificationCode.getValue())) {
if (System.currentTimeMillis() - correctVerificationCode.getTimestamp() <= TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) {
if (correctVerificationCode != null) {
if (System.currentTimeMillis() - correctVerificationCode.getTimestamp()
> TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) {
verificationCodesCache.evict(user.getSessionId());
return false;
}
if (verificationCode.equals(correctVerificationCode.getValue())
&& correctVerificationCode.getConfig().equals(accountConfig)) {
verificationCodesCache.evict(user.getSessionId());
return true;
}
@ -65,6 +70,7 @@ public abstract class OtpBasedTwoFactorAuthProvider<C extends OtpBasedTwoFactorA
private static class Otp {
private final long timestamp;
private final String value;
private final OtpBasedTwoFactorAuthAccountConfig config;
}
}

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

@ -40,7 +40,7 @@ import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -57,19 +57,19 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
private final UserService userService;
private final CustomerService customerService;
private final AuditLogService auditLogService;
private final TwoFactorAuthService twoFactorAuthService;
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager;
@Autowired
public RestAuthenticationProvider(final UserService userService,
final CustomerService customerService,
final SystemSecurityService systemSecurityService,
final AuditLogService auditLogService,
TwoFactorAuthService twoFactorAuthService) {
TwoFactorAuthConfigManager twoFactorAuthConfigManager) {
this.userService = userService;
this.customerService = customerService;
this.systemSecurityService = systemSecurityService;
this.auditLogService = auditLogService;
this.twoFactorAuthService = twoFactorAuthService;
this.twoFactorAuthConfigManager = twoFactorAuthConfigManager;
}
@Override
@ -87,7 +87,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
String username = userPrincipal.getValue();
String password = (String) authentication.getCredentials();
securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
if (twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()).isPresent()) {
if (twoFactorAuthConfigManager.isTwoFaEnabled(securityUser)) {
return new MfaAuthenticationToken(securityUser);
}
logLoginAction((User) authentication.getPrincipal(), authentication, ActionType.LOGIN, null);

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

@ -26,7 +26,7 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand
import org.springframework.stereotype.Component;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -44,7 +44,7 @@ import java.io.IOException;
public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper mapper;
private final JwtTokenFactory tokenFactory;
private final TwoFactorAuthService twoFactorAuthService;
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager;
private final RefreshTokenRepository refreshTokenRepository;
@Override
@ -54,9 +54,9 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
JwtTokenPair tokenPair = new JwtTokenPair();
if (authentication instanceof MfaAuthenticationToken) {
TwoFactorAuthSettings twoFaSettings = twoFactorAuthService.getTwoFaSettings(securityUser.getTenantId()).get();
// FIXME [viacheslav]: define the logic: pre-verification token lifetime,
tokenPair.setToken(tokenFactory.createTwoFaPreVerificationToken(securityUser, ).getToken());
int preVerificationTokenLifetime = twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId())
.map(TwoFactorAuthSettings::getTotalAllowedTimeForVerification).orElse(30);
tokenPair.setToken(tokenFactory.createTwoFaPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
tokenPair.setRefreshToken(null);
} else {
tokenPair.setToken(tokenFactory.createAccessJwtToken(securityUser).getToken());

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

@ -37,12 +37,14 @@ import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
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.UserPasswordPolicy;
@ -50,7 +52,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.utils.MiscUtils;
@ -59,6 +61,7 @@ import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
@ -122,17 +125,10 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException {
if (!encoder.matches(password, userCredentials.getPassword())) {
int failedLoginAttempts = userService.onUserLoginIncorrectCredentials(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = getSecuritySettings(tenantId);
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) {
if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) {
userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userCredentials.getUserId(), false);
if (StringUtils.isNoneBlank(securitySettings.getUserLockoutNotificationEmail())) {
try {
mailService.sendAccountLockoutEmail(username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
} catch (ThingsboardException e) {
log.warn("Can't send email regarding user account [{}] lockout to provided email [{}]", username, securitySettings.getUserLockoutNotificationEmail(), e);
}
}
lockAccount(userCredentials.getUserId(), username, securitySettings);
throw new LockedException("Authentication Failed. Username was locked due to security policy.");
}
}
@ -143,6 +139,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
throw new DisabledException("User is not active");
}
// FIXME [viacheslav]: don't do that in case of 2FA. maybe just move underlying setLastLoginTs to logLoginAction ?
userService.onUserLoginSuccessful(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
@ -156,6 +153,43 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
}
}
@Override
public void validateTwoFaVerification(TenantId tenantId, UserId userId, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings) {
User user = userService.findUserById(tenantId, userId);
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo())
.filter(jsonNode -> jsonNode instanceof ObjectNode)
.orElseGet(JacksonUtil::newObjectNode);
// TODO [viacheslav]: test !
int failedVerificationAttempts = Optional.ofNullable(additionalInfo.get("failedTwoFaVerificationAttempts"))
.map(JsonNode::asInt).orElse(0);
if (!verificationSuccess) {
failedVerificationAttempts++;
// TODO [viacheslav]: maybe use userService.onUserLoginIncorrectCredentials()
} else {
failedVerificationAttempts = 0;
// and set last login ts
}
if (twoFaSettings.getMaxCodeVerificationFailuresBeforeUserLockout() > 0
&& failedVerificationAttempts >= twoFaSettings.getMaxCodeVerificationFailuresBeforeUserLockout()) {
userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false);
lockAccount(userId, user.getEmail(), self.getSecuritySettings(tenantId));
throw new LockedException("User account was locked due to exceeded 2FA verification attempts");
}
}
private void lockAccount(UserId userId, String username, SecuritySettings securitySettings) {
userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false);
if (StringUtils.isNoneBlank(securitySettings.getUserLockoutNotificationEmail())) {
try {
mailService.sendAccountLockoutEmail(username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
} catch (ThingsboardException e) {
log.warn("Can't send email regarding user account [{}] lockout to provided email [{}]", username, securitySettings.getUserLockoutNotificationEmail(), e);
}
}
}
@Override
public void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException {
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);

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

@ -18,9 +18,11 @@ package org.thingsboard.server.service.security.system;
import org.springframework.security.core.AuthenticationException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
import javax.servlet.http.HttpServletRequest;
@ -32,6 +34,8 @@ public interface SystemSecurityService {
void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException;
void validateTwoFaVerification(TenantId tenantId, UserId userId, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings);
void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException;
String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest);

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

@ -45,6 +45,7 @@ import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
// TODO [viacheslav]: test sessionId
// TODO [viacheslav]: test validation for all account configs, provider configs and two factor auth settings
public abstract class TwoFactorAuthTest extends AbstractControllerTest {
@SpyBean

2
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -301,7 +301,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
public void onUserLoginSuccessful(TenantId tenantId, UserId userId) {
log.trace("Executing onUserLoginSuccessful [{}]", userId);
User user = findUserById(tenantId, userId);
setLastLoginTs(user);
setLastLoginTs(user); // FIXME [viacheslav]: move to logLoginAction ?
resetFailedLoginAttempts(user);
saveUser(user);
}

Loading…
Cancel
Save