Browse Source

2FA: lock account after X unsuccessful verification attempts; refactoring

pull/6235/head
Viacheslav Klimov 5 years ago
parent
commit
20a4f3cc4c
  1. 15
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  2. 3
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
  3. 38
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java
  5. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java
  6. 14
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFactorAuthProvider.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java
  8. 60
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  9. 5
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  10. 11
      application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
  11. 6
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  12. 88
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  13. 11
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  14. 11
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  15. 15
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

15
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;
}
}

3
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");

38
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<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 final ConcurrentMap<UserId, TbRateLimits> verificationCodeSendingRateLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, 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);
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<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> getTwoFaProvider(TwoFactorAuthProviderType providerType) throws ThingsboardException {
return Optional.ofNullable(providers.get(providerType))
.orElseThrow(() -> PROVIDER_NOT_AVAILABLE);
.orElseThrow(() -> PROVIDER_NOT_AVAILABLE_ERROR);
}
@Autowired

6
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;

4
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java

@ -25,9 +25,9 @@ public interface TwoFactorAuthProvider<C extends TwoFactorAuthProviderConfig, A
A generateNewAccountConfig(User user, C providerConfig);
default void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException {}
default void prepareVerificationCode(SecurityUser securityUser, C providerConfig, A accountConfig) throws ThingsboardException {}
boolean checkVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig);
boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, C providerConfig, A accountConfig);
TwoFactorAuthProviderType getType();

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

@ -38,27 +38,27 @@ public abstract class OtpBasedTwoFactorAuthProvider<C extends OtpBasedTwoFactorA
@Override
public final void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException {
public final void prepareVerificationCode(SecurityUser securityUser, C providerConfig, A accountConfig) throws ThingsboardException {
String verificationCode = RandomStringUtils.randomNumeric(6);
verificationCodesCache.put(user.getSessionId(), new Otp(System.currentTimeMillis(), verificationCode, accountConfig));
sendVerificationCode(user, verificationCode, providerConfig, accountConfig);
verificationCodesCache.put(securityUser.getId(), new Otp(System.currentTimeMillis(), verificationCode, accountConfig));
sendVerificationCode(securityUser, verificationCode, providerConfig, accountConfig);
}
protected abstract void sendVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) throws ThingsboardException;
@Override
public final boolean checkVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) {
Otp correctVerificationCode = verificationCodesCache.get(user.getSessionId(), Otp.class);
public final boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, C providerConfig, A accountConfig) {
Otp correctVerificationCode = verificationCodesCache.get(securityUser.getId(), Otp.class);
if (correctVerificationCode != null) {
if (System.currentTimeMillis() - correctVerificationCode.getTimestamp()
> 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;
}
}

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java

@ -45,7 +45,7 @@ public class TotpTwoFactorAuthProvider implements TwoFactorAuthProvider<TotpTwoF
}
@Override
public final boolean checkVerificationCode(SecurityUser user, String verificationCode, TotpTwoFactorAuthProviderConfig providerConfig, TotpTwoFactorAuthAccountConfig accountConfig) {
public final boolean checkVerificationCode(SecurityUser securityUser, String verificationCode, TotpTwoFactorAuthProviderConfig providerConfig, TotpTwoFactorAuthAccountConfig accountConfig) {
String secretKey = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build().getQueryParams().getFirst("secret");
return new Totp(secretKey).verify(verificationCode);
}

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

@ -36,7 +36,6 @@ 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.common.data.security.UserCredentials;
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;
@ -44,7 +43,6 @@ import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthConf
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import ua_parser.Client;
import java.util.UUID;
@ -56,19 +54,16 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
private final SystemSecurityService systemSecurityService;
private final UserService userService;
private final CustomerService customerService;
private final AuditLogService auditLogService;
private final TwoFactorAuthConfigManager twoFactorAuthConfigManager;
@Autowired
public RestAuthenticationProvider(final UserService userService,
final CustomerService customerService,
final SystemSecurityService systemSecurityService,
final AuditLogService auditLogService,
TwoFactorAuthConfigManager twoFactorAuthConfigManager) {
this.userService = userService;
this.customerService = customerService;
this.systemSecurityService = systemSecurityService;
this.auditLogService = auditLogService;
this.twoFactorAuthConfigManager = twoFactorAuthConfigManager;
}
@ -89,8 +84,9 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
if (twoFactorAuthConfigManager.isTwoFaEnabled(securityUser)) {
return new MfaAuthenticationToken(securityUser);
} else {
systemSecurityService.logLoginAction((User) authentication.getPrincipal(), authentication, ActionType.LOGIN, null);
}
logLoginAction((User) authentication.getPrincipal(), authentication, ActionType.LOGIN, null);
} else {
String publicId = userPrincipal.getValue();
securityUser = authenticateByPublicId(userPrincipal, publicId);
@ -115,7 +111,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
try {
systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password);
} catch (LockedException e) {
logLoginAction(user, authentication, ActionType.LOCKOUT, null);
systemSecurityService.logLoginAction(user, authentication, ActionType.LOCKOUT, null);
throw e;
}
@ -124,7 +120,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
} catch (Exception e) {
logLoginAction(user, authentication, ActionType.LOGIN, e);
systemSecurityService.logLoginAction(user, authentication, ActionType.LOGIN, e);
throw e;
}
}
@ -159,52 +155,4 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
private 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;
}
}
}
}
auditLogService.logEntityAction(
user.getTenantId(), user.getCustomerId(), user.getId(),
user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device);
}
}

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

@ -37,6 +37,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
@Component(value = "defaultAuthenticationSuccessHandler")
@RequiredArgsConstructor
@ -54,8 +55,8 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
JwtTokenPair tokenPair = new JwtTokenPair();
if (authentication instanceof MfaAuthenticationToken) {
int preVerificationTokenLifetime = twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId())
.map(TwoFactorAuthSettings::getTotalAllowedTimeForVerification).orElse(30);
int preVerificationTokenLifetime = (int) TimeUnit.MINUTES.toSeconds(twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId())
.map(TwoFactorAuthSettings::getTotalAllowedTimeForVerification).orElse(30));
tokenPair.setToken(tokenFactory.createTwoFaPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
tokenPair.setRefreshToken(null);
} else {

11
application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java

@ -21,7 +21,6 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.UserId;
import java.util.Collection;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -32,7 +31,6 @@ public class SecurityUser extends User {
private Collection<GrantedAuthority> 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<GrantedAuthority> 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;
}
}

6
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();

88
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;
}

11
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);
}

11
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -56,16 +56,19 @@ public interface UserService {
PageData<User> findUsersByTenantId(TenantId tenantId, PageLink pageLink);
PageData<User> findTenantAdmins(TenantId tenantId, PageLink pageLink);
void deleteTenantAdmins(TenantId tenantId);
PageData<User> 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);
}

15
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);

Loading…
Cancel
Save