Merge pull request #8089 from dashevchenko/secureRNG

Updated RNG for reset password token to secure one
This commit is contained in:
Andrew Shvayka 2023-02-28 18:26:21 +02:00 committed by GitHub
commit 0ba8336ee9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 38 additions and 5 deletions

View File

@ -20,6 +20,7 @@ import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@ -41,11 +42,13 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
@ -62,6 +65,8 @@ import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@RestController
@TbCoreComponent
@ -69,6 +74,10 @@ import java.net.URISyntaxException;
@Slf4j
@RequiredArgsConstructor
public class AuthController extends BaseController {
@Value("${server.rest.rate_limits.reset_password_per_user:5:3600}")
private String defaultLimitsConfiguration;
private final ConcurrentMap<UserId, TbRateLimits> resetPasswordRateLimits = new ConcurrentHashMap<>();
private final BCryptPasswordEncoder passwordEncoder;
private final JwtTokenFactory tokenFactory;
private final MailService mailService;
@ -211,7 +220,12 @@ public class AuthController extends BaseController {
HttpStatus responseStatus;
String resetURI = "/login/resetPassword";
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
if (userCredentials != null) {
TbRateLimits tbRateLimits = getTbRateLimits(userCredentials.getUserId());
if (!tbRateLimits.tryConsume()) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
try {
URI location = new URI(resetURI + "?resetToken=" + resetToken);
headers.setLocation(location);
@ -323,4 +337,9 @@ public class AuthController extends BaseController {
throw handleException(e);
}
}
private TbRateLimits getTbRateLimits(UserId userId) {
return resetPasswordRateLimits.computeIfAbsent(userId,
key -> new TbRateLimits(defaultLimitsConfiguration, true));
}
}

View File

@ -73,6 +73,8 @@ server:
min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}"
# Default value of the server side RPC timeout.
default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}"
rate_limits:
reset_password_per_user: "${RESET_PASSWORD_PER_USER_RATE_LIMIT_CONFIGURATION:5:3600}"
# Application info
app:
@ -1210,3 +1212,4 @@ management:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
include: '${METRICS_ENDPOINTS_EXPOSE:info}'

View File

@ -18,9 +18,14 @@ package org.thingsboard.server.common.data;
import com.google.common.base.Splitter;
import org.apache.commons.lang3.RandomStringUtils;
import java.security.SecureRandom;
import java.util.Base64;
import static org.apache.commons.lang3.StringUtils.repeat;
public class StringUtils {
public static final SecureRandom RANDOM = new SecureRandom();
public static final String EMPTY = "";
public static final int INDEX_NOT_FOUND = -1;
@ -180,4 +185,11 @@ public class StringUtils {
return RandomStringUtils.randomAlphabetic(count);
}
public static String generateSafeToken(int length) {
byte[] bytes = new byte[length];
RANDOM.nextBytes(bytes);
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
return encoder.encodeToString(bytes);
}
}

View File

@ -29,7 +29,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -40,7 +39,6 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
@ -51,6 +49,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.thingsboard.server.common.data.StringUtils.generateSafeToken;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
import static org.thingsboard.server.dao.service.Validator.validateString;
@ -126,7 +125,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
if (user.getId() == null) {
UserCredentials userCredentials = new UserCredentials();
userCredentials.setEnabled(false);
userCredentials.setActivateToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
userCredentials.setUserId(new UserId(savedUser.getUuidId()));
saveUserCredentialsAndPasswordHistory(user.getTenantId(), userCredentials);
}
@ -192,7 +191,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
if (!userCredentials.isEnabled()) {
throw new DisabledException(String.format("User credentials not enabled [%s]", email));
}
userCredentials.setResetToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
return saveUserCredentials(tenantId, userCredentials);
}
@ -202,7 +201,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
if (!userCredentials.isEnabled()) {
throw new IncorrectParameterException("Unable to reset password for inactive user");
}
userCredentials.setResetToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
return saveUserCredentials(tenantId, userCredentials);
}