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 76cc8f2f14..125febeb6a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -17,24 +17,25 @@ package org.thingsboard.server.controller; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; 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 org.thingsboard.server.service.security.system.SystemSecurityService; /* * TODO [viacheslav]: * - Tests for 2FA * - Swagger documentation - * * */ -// TODO [viacheslav]: maybe get rid of sessionId concept.. /* * @@ -51,6 +52,7 @@ public class TwoFactorAuthController extends BaseController { private final TwoFactorAuthService twoFactorAuthService; private final JwtTokenFactory tokenFactory; + private final SystemSecurityService systemSecurityService; @PostMapping("/verification/send") @@ -62,14 +64,17 @@ public class TwoFactorAuthController extends BaseController { @PostMapping("/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws Exception { + public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode, Authentication authentication) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true); if (verificationSuccess) { - // FIXME [viacheslav]: log login action + systemSecurityService.logLoginAction(user, authentication, ActionType.LOGIN, null); return tokenFactory.createTokenPair(user); } else { - throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION); + ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION); + // FIXME [viacheslav]: log login action: no authentication details + systemSecurityService.logLoginAction(user, authentication, ActionType.LOGIN, error); + throw error; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index b744adcdaf..665451b61f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -33,11 +33,11 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; +import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -66,7 +66,6 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide } else { securityUser = authenticateByPublicId(principal.getValue()); } - securityUser.setSessionId(unsafeUser.getSessionId()); if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) { throw new CredentialsExpiredException("Token is outdated"); 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 98cbeb0786..a06184ff6c 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 @@ -23,6 +23,7 @@ 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.msg.tools.TbRateLimits; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConfigManager; @@ -50,30 +51,29 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private final UserService userService; private final Map> 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 verificationCodeSendingRateLimits = new ConcurrentHashMap<>(); - private final ConcurrentMap verificationCodeCheckingRateLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap verificationCodeSendingRateLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap 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); + private static final ThingsboardException ACCOUNT_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + private static final ThingsboardException PROVIDER_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + private static final ThingsboardException PROVIDER_NOT_AVAILABLE_ERROR = 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); + .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); 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); + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); if (rateLimit) { if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) { - TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getSessionId(), sessionId -> { + TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> { return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit()); }); if (!rateLimits.tryConsume()) { @@ -83,14 +83,14 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { } TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) - .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); 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); + .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); return checkVerificationCode(securityUser, verificationCode, accountConfig, rateLimit); } @@ -101,10 +101,10 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { } TwoFactorAuthSettings twoFaSettings = configManager.getTwoFaSettings(securityUser.getTenantId()) - .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); if (rateLimit) { if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) { - TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getSessionId(), sessionId -> { + TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> { return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit()); }); if (!rateLimits.tryConsume()) { @@ -114,10 +114,14 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { } TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) - .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); boolean verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(securityUser, verificationCode, providerConfig, accountConfig); if (rateLimit) { - systemSecurityService.validateTwoFaVerification(securityUser.getTenantId(), securityUser.getId(), verificationSuccess, twoFaSettings); + systemSecurityService.validateTwoFaVerification(securityUser, verificationSuccess, twoFaSettings); + if (verificationSuccess) { + verificationCodeCheckingRateLimits.remove(securityUser.getId()); + verificationCodeSendingRateLimits.remove(securityUser.getId()); + } } return verificationSuccess; } @@ -132,12 +136,12 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private TwoFactorAuthProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) throws ThingsboardException { return configManager.getTwoFaSettings(tenantId) .flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType)) - .orElseThrow(() -> PROVIDER_NOT_CONFIGURED); + .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); } private TwoFactorAuthProvider getTwoFaProvider(TwoFactorAuthProviderType providerType) throws ThingsboardException { return Optional.ofNullable(providers.get(providerType)) - .orElseThrow(() -> PROVIDER_NOT_AVAILABLE); + .orElseThrow(() -> PROVIDER_NOT_AVAILABLE_ERROR); } @Autowired diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java index e70742267b..0866aafbe1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java @@ -17,14 +17,11 @@ 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; import java.util.List; import java.util.Optional; @@ -42,9 +39,10 @@ public class TwoFactorAuthSettings { @ApiModelProperty(example = "3:900 (3 requests per 15 minutes)") @Pattern(regexp = "[^0]\\d+:[^0]\\d+", message = "Rate limit configuration is invalid") private String verificationCodeCheckRateLimit; + @ApiModelProperty(example = "10") @Min(0) private int maxCodeVerificationFailuresBeforeUserLockout; - @ApiModelProperty(value = "in seconds") + @ApiModelProperty(value = "in minutes", example = "60") @Min(1) private int totalAllowedTimeForVerification; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java index 858b5995f7..030482ac6d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java @@ -25,9 +25,9 @@ public interface TwoFactorAuthProvider TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) { - verificationCodesCache.evict(user.getSessionId()); + verificationCodesCache.evict(securityUser.getId()); return false; } if (verificationCode.equals(correctVerificationCode.getValue()) && correctVerificationCode.getConfig().equals(accountConfig)) { - verificationCodesCache.evict(user.getSessionId()); + verificationCodesCache.evict(securityUser.getId()); return true; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java index 65574fc760..83767ee3d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java @@ -45,7 +45,7 @@ public class TotpTwoFactorAuthProvider implements TwoFactorAuthProvider authorities; private boolean enabled; private UserPrincipal userPrincipal; - private String sessionId; public SecurityUser() { super(); @@ -46,7 +44,6 @@ public class SecurityUser extends User { super(user); this.enabled = enabled; this.userPrincipal = userPrincipal; - this.sessionId = UUID.randomUUID().toString(); } public Collection getAuthorities() { @@ -74,12 +71,4 @@ public class SecurityUser extends User { this.userPrincipal = userPrincipal; } - public String getSessionId() { - return sessionId; - } - - public void setSessionId(String sessionId) { - this.sessionId = sessionId; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 36f64b3566..8121ebf834 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -60,7 +60,6 @@ public class JwtTokenFactory { private static final String IS_PUBLIC = "isPublic"; private static final String TENANT_ID = "tenantId"; private static final String CUSTOMER_ID = "customerId"; - private static final String SESSION_ID = "sessionId"; private final JwtSettings settings; @@ -116,7 +115,6 @@ public class JwtTokenFactory { } else if (securityUser.getAuthority() == Authority.SYS_ADMIN) { securityUser.setTenantId(TenantId.SYS_TENANT_ID); } - securityUser.setSessionId(claims.get(SESSION_ID, String.class)); if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); @@ -162,7 +160,6 @@ public class JwtTokenFactory { UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); securityUser.setUserPrincipal(principal); - securityUser.setSessionId(claims.get(SESSION_ID, String.class)); return securityUser; } @@ -183,9 +180,6 @@ public class JwtTokenFactory { Claims claims = Jwts.claims().setSubject(principal.getValue()); claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(SCOPES, scopes); - if (securityUser.getSessionId() != null) { - claims.put(SESSION_ID, securityUser.getSessionId()); - } ZonedDateTime currentTime = ZonedDateTime.now(); diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 646562f9ed..6e6815ff5e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -34,6 +34,7 @@ import org.springframework.cache.annotation.Cacheable; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.LockedException; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -41,6 +42,7 @@ 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.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -48,20 +50,23 @@ 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; +import org.thingsboard.server.dao.audit.AuditLogService; 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.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.utils.MiscUtils; +import ua_parser.Client; import javax.annotation.Resource; 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; @@ -82,6 +87,9 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @Autowired private MailService mailService; + @Autowired + private AuditLogService auditLogService; + @Resource private SystemSecurityService self; @@ -124,7 +132,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { @Override 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()); + int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId()); SecuritySettings securitySettings = self.getSecuritySettings(tenantId); if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) { if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) { @@ -139,8 +147,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()); + userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId()); SecuritySettings securitySettings = self.getSecuritySettings(tenantId); if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) { @@ -154,27 +161,21 @@ 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); + public void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings) { + TenantId tenantId = securityUser.getTenantId(); + UserId userId = securityUser.getId(); + int failedVerificationAttempts = 0; if (!verificationSuccess) { - failedVerificationAttempts++; - // TODO [viacheslav]: maybe use userService.onUserLoginIncorrectCredentials() + failedVerificationAttempts = userService.increaseFailedLoginAttempts(tenantId, userId); } else { - failedVerificationAttempts = 0; - // and set last login ts + userService.resetFailedLoginAttempts(tenantId, userId); } if (twoFaSettings.getMaxCodeVerificationFailuresBeforeUserLockout() > 0 && failedVerificationAttempts >= twoFaSettings.getMaxCodeVerificationFailuresBeforeUserLockout()) { userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); - lockAccount(userId, user.getEmail(), self.getSecuritySettings(tenantId)); + lockAccount(userId, securityUser.getEmail(), self.getSecuritySettings(tenantId)); throw new LockedException("User account was locked due to exceeded 2FA verification attempts"); } } @@ -255,6 +256,59 @@ public class DefaultSystemSecurityService implements SystemSecurityService { return baseUrl; } + @Override + public void logLoginAction(User user, Authentication authentication, ActionType actionType, Exception e) { + String clientAddress = "Unknown"; + String browser = "Unknown"; + String os = "Unknown"; + String device = "Unknown"; + if (authentication != null && authentication.getDetails() != null) { + if (authentication.getDetails() instanceof RestAuthenticationDetails) { + RestAuthenticationDetails details = (RestAuthenticationDetails) authentication.getDetails(); + clientAddress = details.getClientAddress(); + if (details.getUserAgent() != null) { + Client userAgent = details.getUserAgent(); + if (userAgent.userAgent != null) { + browser = userAgent.userAgent.family; + if (userAgent.userAgent.major != null) { + browser += " " + userAgent.userAgent.major; + if (userAgent.userAgent.minor != null) { + browser += "." + userAgent.userAgent.minor; + if (userAgent.userAgent.patch != null) { + browser += "." + userAgent.userAgent.patch; + } + } + } + } + if (userAgent.os != null) { + os = userAgent.os.family; + if (userAgent.os.major != null) { + os += " " + userAgent.os.major; + if (userAgent.os.minor != null) { + os += "." + userAgent.os.minor; + if (userAgent.os.patch != null) { + os += "." + userAgent.os.patch; + if (userAgent.os.patchMinor != null) { + os += "." + userAgent.os.patchMinor; + } + } + } + } + } + if (userAgent.device != null) { + device = userAgent.device.family; + } + } + } + } + if (actionType == ActionType.LOGIN && e == null) { + userService.setLastLoginTs(user.getTenantId(), user.getId()); + } + auditLogService.logEntityAction( + user.getTenantId(), user.getCustomerId(), user.getId(), + user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device); + } + private static boolean isPositiveInteger(Integer val) { return val != null && val.intValue() > 0; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index 453251bf03..f6b401da4e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -15,14 +15,17 @@ */ package org.thingsboard.server.service.security.system; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; 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.dao.exception.DataValidationException; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; +import org.thingsboard.server.service.security.model.SecurityUser; import javax.servlet.http.HttpServletRequest; @@ -34,10 +37,12 @@ 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 validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, TwoFactorAuthSettings twoFaSettings); void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException; String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest); + void logLoginAction(User user, Authentication authentication, ActionType actionType, Exception e); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index e8ddce587e..d5d29efa40 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -56,16 +56,19 @@ public interface UserService { PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink); PageData findTenantAdmins(TenantId tenantId, PageLink pageLink); - + void deleteTenantAdmins(TenantId tenantId); PageData findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink); - + void deleteCustomerUsers(TenantId tenantId, CustomerId customerId); void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled); - void onUserLoginSuccessful(TenantId tenantId, UserId userId); + void resetFailedLoginAttempts(TenantId tenantId, UserId userId); + + int increaseFailedLoginAttempts(TenantId tenantId, UserId userId); + + void setLastLoginTs(TenantId tenantId, UserId userId); - int onUserLoginIncorrectCredentials(TenantId tenantId, UserId userId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 1ee78d4068..b8c7b8e9f9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -298,34 +298,35 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic @Override - public void onUserLoginSuccessful(TenantId tenantId, UserId userId) { + public void resetFailedLoginAttempts(TenantId tenantId, UserId userId) { log.trace("Executing onUserLoginSuccessful [{}]", userId); User user = findUserById(tenantId, userId); - setLastLoginTs(user); // FIXME [viacheslav]: move to logLoginAction ? resetFailedLoginAttempts(user); saveUser(user); } - private void setLastLoginTs(User user) { + private void resetFailedLoginAttempts(User user) { JsonNode additionalInfo = user.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { additionalInfo = JacksonUtil.newObjectNode(); } - ((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis()); + ((ObjectNode) additionalInfo).put(FAILED_LOGIN_ATTEMPTS, 0); user.setAdditionalInfo(additionalInfo); } - private void resetFailedLoginAttempts(User user) { + @Override + public void setLastLoginTs(TenantId tenantId, UserId userId) { + User user = findUserById(tenantId, userId); JsonNode additionalInfo = user.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { additionalInfo = JacksonUtil.newObjectNode(); } - ((ObjectNode) additionalInfo).put(FAILED_LOGIN_ATTEMPTS, 0); + ((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis()); user.setAdditionalInfo(additionalInfo); } @Override - public int onUserLoginIncorrectCredentials(TenantId tenantId, UserId userId) { + public int increaseFailedLoginAttempts(TenantId tenantId, UserId userId) { log.trace("Executing onUserLoginIncorrectCredentials [{}]", userId); User user = findUserById(tenantId, userId); int failedLoginAttempts = increaseFailedLoginAttempts(user);