Refactor 2FA; add refilling setting to TbRateLimits
This commit is contained in:
parent
bc6c38c36c
commit
8bbe6bafd8
@ -17,7 +17,6 @@ package org.thingsboard.server.controller;
|
|||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
@ -25,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import org.thingsboard.server.common.data.audit.ActionType;
|
import org.thingsboard.server.common.data.audit.ActionType;
|
||||||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
||||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
||||||
|
import org.thingsboard.server.dao.user.UserService;
|
||||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||||
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
|
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
|
||||||
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
|
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
|
||||||
@ -53,6 +53,7 @@ public class TwoFactorAuthController extends BaseController {
|
|||||||
private final TwoFactorAuthService twoFactorAuthService;
|
private final TwoFactorAuthService twoFactorAuthService;
|
||||||
private final JwtTokenFactory tokenFactory;
|
private final JwtTokenFactory tokenFactory;
|
||||||
private final SystemSecurityService systemSecurityService;
|
private final SystemSecurityService systemSecurityService;
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/verification/send")
|
@PostMapping("/verification/send")
|
||||||
@ -69,9 +70,10 @@ public class TwoFactorAuthController extends BaseController {
|
|||||||
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true);
|
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true);
|
||||||
if (verificationSuccess) {
|
if (verificationSuccess) {
|
||||||
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null);
|
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null);
|
||||||
|
user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal());
|
||||||
return tokenFactory.createTokenPair(user);
|
return tokenFactory.createTokenPair(user);
|
||||||
} else {
|
} else {
|
||||||
ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.AUTHENTICATION);
|
ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
|
||||||
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error);
|
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,9 +16,10 @@
|
|||||||
package org.thingsboard.server.service.security.auth.mfa;
|
package org.thingsboard.server.service.security.auth.mfa;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.stereotype.Service;
|
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.User;
|
||||||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
||||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
||||||
@ -76,7 +77,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
|
|||||||
if (checkLimits) {
|
if (checkLimits) {
|
||||||
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) {
|
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) {
|
||||||
TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> {
|
TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> {
|
||||||
return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit());
|
return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit(), true);
|
||||||
});
|
});
|
||||||
if (!rateLimits.tryConsume()) {
|
if (!rateLimits.tryConsume()) {
|
||||||
throw new ThingsboardException("Too many verification code sending requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
|
throw new ThingsboardException("Too many verification code sending requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
|
||||||
@ -107,19 +108,30 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
|
|||||||
if (checkLimits) {
|
if (checkLimits) {
|
||||||
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) {
|
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) {
|
||||||
TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> {
|
TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> {
|
||||||
return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit());
|
return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit(), true);
|
||||||
});
|
});
|
||||||
if (!rateLimits.tryConsume()) {
|
if (!rateLimits.tryConsume()) {
|
||||||
throw new ThingsboardException("Too many verification code checking requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
|
throw new ThingsboardException("Too many verification code checking requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
|
TwoFactorAuthProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
|
||||||
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
|
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
|
||||||
boolean verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(securityUser, verificationCode, providerConfig, accountConfig);
|
|
||||||
|
boolean verificationSuccess;
|
||||||
|
if (StringUtils.isNumeric(verificationCode) && verificationCode.length() == 6) {
|
||||||
|
verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(securityUser, verificationCode, providerConfig, accountConfig);
|
||||||
|
} else {
|
||||||
|
verificationSuccess = false;
|
||||||
|
}
|
||||||
if (checkLimits) {
|
if (checkLimits) {
|
||||||
systemSecurityService.validateTwoFaVerification(securityUser, verificationSuccess, twoFaSettings);
|
try {
|
||||||
|
systemSecurityService.validateTwoFaVerification(securityUser, verificationSuccess, twoFaSettings);
|
||||||
|
} catch (LockedException e) {
|
||||||
|
verificationCodeCheckingRateLimits.remove(securityUser.getId());
|
||||||
|
verificationCodeSendingRateLimits.remove(securityUser.getId());
|
||||||
|
throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.AUTHENTICATION);
|
||||||
|
}
|
||||||
if (verificationSuccess) {
|
if (verificationSuccess) {
|
||||||
verificationCodeCheckingRateLimits.remove(securityUser.getId());
|
verificationCodeCheckingRateLimits.remove(securityUser.getId());
|
||||||
verificationCodeSendingRateLimits.remove(securityUser.getId());
|
verificationCodeSendingRateLimits.remove(securityUser.getId());
|
||||||
|
|||||||
@ -42,7 +42,7 @@ public class TwoFactorAuthSettings {
|
|||||||
@ApiModelProperty(example = "10")
|
@ApiModelProperty(example = "10")
|
||||||
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
|
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
|
||||||
private int maxVerificationFailuresBeforeUserLockout;
|
private int maxVerificationFailuresBeforeUserLockout;
|
||||||
@ApiModelProperty(value = "in minutes", example = "60")
|
@ApiModelProperty(value = "in seconds", example = "3600 (60 minutes)")
|
||||||
@Min(value = 1, message = "total amount of time allotted for verification must be greater than 0")
|
@Min(value = 1, message = "total amount of time allotted for verification must be greater than 0")
|
||||||
private Integer totalAllowedTimeForVerification;
|
private Integer totalAllowedTimeForVerification;
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,6 @@ public abstract class OtpBasedTwoFactorAuthProvider<C extends OtpBasedTwoFactorA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME [viacheslav]: periodically clean up codes cache
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Otp {
|
public static class Otp {
|
||||||
|
|||||||
@ -35,7 +35,7 @@ public class SmsTwoFactorAuthProvider extends OtpBasedTwoFactorAuthProvider<SmsT
|
|||||||
|
|
||||||
private final SmsService smsService;
|
private final SmsService smsService;
|
||||||
|
|
||||||
public SmsTwoFactorAuthProvider(SmsService smsService, CacheManager cacheManager) {
|
public SmsTwoFactorAuthProvider(CacheManager cacheManager, SmsService smsService) {
|
||||||
super(cacheManager);
|
super(cacheManager);
|
||||||
this.smsService = smsService;
|
this.smsService = smsService;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,8 +54,8 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
|
|||||||
JwtTokenPair tokenPair = new JwtTokenPair();
|
JwtTokenPair tokenPair = new JwtTokenPair();
|
||||||
|
|
||||||
if (authentication instanceof MfaAuthenticationToken) {
|
if (authentication instanceof MfaAuthenticationToken) {
|
||||||
int preVerificationTokenLifetime = (int) TimeUnit.MINUTES.toSeconds(twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId(), true)
|
int preVerificationTokenLifetime = twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId(), true)
|
||||||
.flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())).orElse(30));
|
.flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())).orElse((int) TimeUnit.MINUTES.toSeconds(30));
|
||||||
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
|
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
|
||||||
tokenPair.setRefreshToken(null);
|
tokenPair.setRefreshToken(null);
|
||||||
tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN);
|
tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN);
|
||||||
|
|||||||
@ -115,21 +115,22 @@ public class JwtTokenFactory {
|
|||||||
} else if (securityUser.getAuthority() == Authority.SYS_ADMIN) {
|
} else if (securityUser.getAuthority() == Authority.SYS_ADMIN) {
|
||||||
securityUser.setTenantId(TenantId.SYS_TENANT_ID);
|
securityUser.setTenantId(TenantId.SYS_TENANT_ID);
|
||||||
}
|
}
|
||||||
|
String customerId = claims.get(CUSTOMER_ID, String.class);
|
||||||
|
if (customerId != null) {
|
||||||
|
securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPrincipal principal;
|
||||||
if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) {
|
if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) {
|
||||||
securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
|
securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
|
||||||
securityUser.setLastName(claims.get(LAST_NAME, String.class));
|
securityUser.setLastName(claims.get(LAST_NAME, String.class));
|
||||||
securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
|
securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
|
||||||
boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
|
boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
|
||||||
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
|
principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
|
||||||
securityUser.setUserPrincipal(principal);
|
|
||||||
String customerId = claims.get(CUSTOMER_ID, String.class);
|
|
||||||
if (customerId != null) {
|
|
||||||
securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId)));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, subject));
|
principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, subject);
|
||||||
}
|
}
|
||||||
|
securityUser.setUserPrincipal(principal);
|
||||||
|
|
||||||
return securityUser;
|
return securityUser;
|
||||||
}
|
}
|
||||||
@ -164,10 +165,12 @@ public class JwtTokenFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public JwtToken createPreVerificationToken(SecurityUser user, Integer expirationTime) {
|
public JwtToken createPreVerificationToken(SecurityUser user, Integer expirationTime) {
|
||||||
String token = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime)
|
JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime)
|
||||||
.claim(TENANT_ID, user.getTenantId().toString())
|
.claim(TENANT_ID, user.getTenantId().toString());
|
||||||
.compact();
|
if (user.getCustomerId() != null) {
|
||||||
return new AccessJwtToken(token);
|
jwtBuilder.claim(CUSTOMER_ID, user.getCustomerId().toString());
|
||||||
|
}
|
||||||
|
return new AccessJwtToken(jwtBuilder.compact());
|
||||||
}
|
}
|
||||||
|
|
||||||
private JwtBuilder setUpToken(SecurityUser securityUser, List<String> scopes, long expirationTime) {
|
private JwtBuilder setUpToken(SecurityUser securityUser, List<String> scopes, long expirationTime) {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ package org.thingsboard.server.common.msg.tools;
|
|||||||
|
|
||||||
import io.github.bucket4j.Bandwidth;
|
import io.github.bucket4j.Bandwidth;
|
||||||
import io.github.bucket4j.Bucket4j;
|
import io.github.bucket4j.Bucket4j;
|
||||||
|
import io.github.bucket4j.Refill;
|
||||||
import io.github.bucket4j.local.LocalBucket;
|
import io.github.bucket4j.local.LocalBucket;
|
||||||
import io.github.bucket4j.local.LocalBucketBuilder;
|
import io.github.bucket4j.local.LocalBucketBuilder;
|
||||||
|
|
||||||
@ -29,12 +30,17 @@ public class TbRateLimits {
|
|||||||
private final LocalBucket bucket;
|
private final LocalBucket bucket;
|
||||||
|
|
||||||
public TbRateLimits(String limitsConfiguration) {
|
public TbRateLimits(String limitsConfiguration) {
|
||||||
|
this(limitsConfiguration, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TbRateLimits(String limitsConfiguration, boolean refillIntervally) {
|
||||||
LocalBucketBuilder builder = Bucket4j.builder();
|
LocalBucketBuilder builder = Bucket4j.builder();
|
||||||
boolean initialized = false;
|
boolean initialized = false;
|
||||||
for (String limitSrc : limitsConfiguration.split(",")) {
|
for (String limitSrc : limitsConfiguration.split(",")) {
|
||||||
long capacity = Long.parseLong(limitSrc.split(":")[0]);
|
long capacity = Long.parseLong(limitSrc.split(":")[0]);
|
||||||
long duration = Long.parseLong(limitSrc.split(":")[1]);
|
long duration = Long.parseLong(limitSrc.split(":")[1]);
|
||||||
builder.addLimit(Bandwidth.simple(capacity, Duration.ofSeconds(duration)));
|
Refill refill = refillIntervally ? Refill.intervally(capacity, Duration.ofSeconds(duration)) : Refill.greedy(capacity, Duration.ofSeconds(duration));
|
||||||
|
builder.addLimit(Bandwidth.classic(capacity, refill));
|
||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
@ -42,8 +48,6 @@ public class TbRateLimits {
|
|||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Failed to parse rate limits configuration: " + limitsConfiguration);
|
throw new IllegalArgumentException("Failed to parse rate limits configuration: " + limitsConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean tryConsume() {
|
public boolean tryConsume() {
|
||||||
|
|||||||
@ -323,6 +323,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
|
|||||||
}
|
}
|
||||||
((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis());
|
((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis());
|
||||||
user.setAdditionalInfo(additionalInfo);
|
user.setAdditionalInfo(additionalInfo);
|
||||||
|
saveUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user