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 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;
|
||||
@ -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.exception.ThingsboardErrorCode;
|
||||
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.service.security.auth.mfa.TwoFactorAuthService;
|
||||
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
|
||||
@ -53,6 +53,7 @@ public class TwoFactorAuthController extends BaseController {
|
||||
private final TwoFactorAuthService twoFactorAuthService;
|
||||
private final JwtTokenFactory tokenFactory;
|
||||
private final SystemSecurityService systemSecurityService;
|
||||
private final UserService userService;
|
||||
|
||||
|
||||
@PostMapping("/verification/send")
|
||||
@ -69,9 +70,10 @@ public class TwoFactorAuthController extends BaseController {
|
||||
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, true);
|
||||
if (verificationSuccess) {
|
||||
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);
|
||||
} 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);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@ -16,9 +16,10 @@
|
||||
package org.thingsboard.server.service.security.auth.mfa;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
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;
|
||||
@ -76,7 +77,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
|
||||
if (checkLimits) {
|
||||
if (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeSendRateLimit())) {
|
||||
TbRateLimits rateLimits = verificationCodeSendingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> {
|
||||
return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit());
|
||||
return new TbRateLimits(twoFaSettings.getVerificationCodeSendRateLimit(), true);
|
||||
});
|
||||
if (!rateLimits.tryConsume()) {
|
||||
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 (StringUtils.isNotEmpty(twoFaSettings.getVerificationCodeCheckRateLimit())) {
|
||||
TbRateLimits rateLimits = verificationCodeCheckingRateLimits.computeIfAbsent(securityUser.getId(), sessionId -> {
|
||||
return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit());
|
||||
return new TbRateLimits(twoFaSettings.getVerificationCodeCheckRateLimit(), true);
|
||||
});
|
||||
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_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) {
|
||||
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) {
|
||||
verificationCodeCheckingRateLimits.remove(securityUser.getId());
|
||||
verificationCodeSendingRateLimits.remove(securityUser.getId());
|
||||
|
||||
@ -42,7 +42,7 @@ public class TwoFactorAuthSettings {
|
||||
@ApiModelProperty(example = "10")
|
||||
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
|
||||
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")
|
||||
private Integer totalAllowedTimeForVerification;
|
||||
|
||||
|
||||
@ -65,7 +65,6 @@ public abstract class OtpBasedTwoFactorAuthProvider<C extends OtpBasedTwoFactorA
|
||||
return false;
|
||||
}
|
||||
|
||||
// FIXME [viacheslav]: periodically clean up codes cache
|
||||
|
||||
@Data
|
||||
public static class Otp {
|
||||
|
||||
@ -35,7 +35,7 @@ public class SmsTwoFactorAuthProvider extends OtpBasedTwoFactorAuthProvider<SmsT
|
||||
|
||||
private final SmsService smsService;
|
||||
|
||||
public SmsTwoFactorAuthProvider(SmsService smsService, CacheManager cacheManager) {
|
||||
public SmsTwoFactorAuthProvider(CacheManager cacheManager, SmsService smsService) {
|
||||
super(cacheManager);
|
||||
this.smsService = smsService;
|
||||
}
|
||||
|
||||
@ -54,8 +54,8 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
|
||||
JwtTokenPair tokenPair = new JwtTokenPair();
|
||||
|
||||
if (authentication instanceof MfaAuthenticationToken) {
|
||||
int preVerificationTokenLifetime = (int) TimeUnit.MINUTES.toSeconds(twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId(), true)
|
||||
.flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())).orElse(30));
|
||||
int preVerificationTokenLifetime = twoFactorAuthConfigManager.getTwoFaSettings(securityUser.getTenantId(), true)
|
||||
.flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())).orElse((int) TimeUnit.MINUTES.toSeconds(30));
|
||||
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
|
||||
tokenPair.setRefreshToken(null);
|
||||
tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN);
|
||||
|
||||
@ -115,21 +115,22 @@ public class JwtTokenFactory {
|
||||
} else if (securityUser.getAuthority() == Authority.SYS_ADMIN) {
|
||||
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) {
|
||||
securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
|
||||
securityUser.setLastName(claims.get(LAST_NAME, String.class));
|
||||
securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
|
||||
boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
|
||||
UserPrincipal 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)));
|
||||
}
|
||||
principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
|
||||
} else {
|
||||
securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, subject));
|
||||
principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, subject);
|
||||
}
|
||||
securityUser.setUserPrincipal(principal);
|
||||
|
||||
return securityUser;
|
||||
}
|
||||
@ -164,10 +165,12 @@ public class JwtTokenFactory {
|
||||
}
|
||||
|
||||
public JwtToken createPreVerificationToken(SecurityUser user, Integer expirationTime) {
|
||||
String token = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime)
|
||||
.claim(TENANT_ID, user.getTenantId().toString())
|
||||
.compact();
|
||||
return new AccessJwtToken(token);
|
||||
JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime)
|
||||
.claim(TENANT_ID, user.getTenantId().toString());
|
||||
if (user.getCustomerId() != null) {
|
||||
jwtBuilder.claim(CUSTOMER_ID, user.getCustomerId().toString());
|
||||
}
|
||||
return new AccessJwtToken(jwtBuilder.compact());
|
||||
}
|
||||
|
||||
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.Bucket4j;
|
||||
import io.github.bucket4j.Refill;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
import io.github.bucket4j.local.LocalBucketBuilder;
|
||||
|
||||
@ -29,12 +30,17 @@ public class TbRateLimits {
|
||||
private final LocalBucket bucket;
|
||||
|
||||
public TbRateLimits(String limitsConfiguration) {
|
||||
this(limitsConfiguration, false);
|
||||
}
|
||||
|
||||
public TbRateLimits(String limitsConfiguration, boolean refillIntervally) {
|
||||
LocalBucketBuilder builder = Bucket4j.builder();
|
||||
boolean initialized = false;
|
||||
for (String limitSrc : limitsConfiguration.split(",")) {
|
||||
long capacity = Long.parseLong(limitSrc.split(":")[0]);
|
||||
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;
|
||||
}
|
||||
if (initialized) {
|
||||
@ -42,8 +48,6 @@ public class TbRateLimits {
|
||||
} else {
|
||||
throw new IllegalArgumentException("Failed to parse rate limits configuration: " + limitsConfiguration);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public boolean tryConsume() {
|
||||
|
||||
@ -323,6 +323,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
|
||||
}
|
||||
((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis());
|
||||
user.setAdditionalInfo(additionalInfo);
|
||||
saveUser(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user