Implement TTL for password reset and user activation links; refactoring and improvements
This commit is contained in:
parent
6fa9fb5f58
commit
976e1e1e1f
@ -14,3 +14,19 @@
|
||||
-- limitations under the License.
|
||||
--
|
||||
|
||||
-- USER CREDENTIALS UPDATE START
|
||||
|
||||
ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS activate_token_exp_time BIGINT;
|
||||
-- Setting 24-hour TTL for existing activation tokens
|
||||
UPDATE user_credentials SET activate_token_exp_time = cast(extract(EPOCH FROM NOW()) * 1000 AS BIGINT) + 86400000
|
||||
WHERE activate_token IS NOT NULL AND activate_token_exp_time IS NULL;
|
||||
|
||||
ALTER TABLE user_credentials ADD COLUMN IF NOT EXISTS reset_token_exp_time BIGINT;
|
||||
-- Setting 24-hour TTL for existing password reset tokens
|
||||
UPDATE user_credentials SET reset_token_exp_time = cast(extract(EPOCH FROM NOW()) * 1000 AS BIGINT) + 86400000
|
||||
WHERE reset_token IS NOT NULL AND reset_token_exp_time IS NULL;
|
||||
|
||||
UPDATE admin_settings SET json_value = (json_value::jsonb || '{"userActivationTokenTtl":24,"passwordResetTokenTtl":24}'::jsonb)::varchar
|
||||
WHERE key = 'securitySettings';
|
||||
|
||||
-- USER CREDENTIALS UPDATE END
|
||||
|
||||
@ -73,6 +73,7 @@ import org.thingsboard.server.common.data.sync.vc.VcUtils;
|
||||
import org.thingsboard.server.config.annotations.ApiOperation;
|
||||
import org.thingsboard.server.dao.audit.AuditLogService;
|
||||
import org.thingsboard.server.dao.settings.AdminSettingsService;
|
||||
import org.thingsboard.server.dao.settings.SecuritySettingsService;
|
||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
|
||||
import org.thingsboard.server.service.security.auth.oauth2.CookieUtils;
|
||||
@ -109,6 +110,7 @@ public class AdminController extends BaseController {
|
||||
private final SmsService smsService;
|
||||
private final AdminSettingsService adminSettingsService;
|
||||
private final SystemSecurityService systemSecurityService;
|
||||
private final SecuritySettingsService securitySettingsService;
|
||||
private final JwtSettingsService jwtSettingsService;
|
||||
private final JwtTokenFactory tokenFactory;
|
||||
private final EntitiesVersionControlService versionControlService;
|
||||
@ -167,7 +169,7 @@ public class AdminController extends BaseController {
|
||||
@ResponseBody
|
||||
public SecuritySettings getSecuritySettings() throws ThingsboardException {
|
||||
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
|
||||
return checkNotNull(systemSecurityService.getSecuritySettings());
|
||||
return checkNotNull(securitySettingsService.getSecuritySettings());
|
||||
}
|
||||
|
||||
@ApiOperation(value = "Update Security Settings (saveSecuritySettings)",
|
||||
@ -179,7 +181,7 @@ public class AdminController extends BaseController {
|
||||
@Parameter(description = "A JSON value representing the Security Settings.")
|
||||
@RequestBody SecuritySettings securitySettings) throws ThingsboardException {
|
||||
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
|
||||
securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(securitySettings));
|
||||
securitySettings = checkNotNull(securitySettingsService.saveSecuritySettings(securitySettings));
|
||||
return securitySettings;
|
||||
}
|
||||
|
||||
|
||||
@ -26,9 +26,10 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
@ -48,6 +49,7 @@ import org.thingsboard.server.common.data.security.model.JwtPair;
|
||||
import org.thingsboard.server.common.data.security.model.SecuritySettings;
|
||||
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
|
||||
import org.thingsboard.server.config.annotations.ApiOperation;
|
||||
import org.thingsboard.server.dao.settings.SecuritySettingsService;
|
||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
|
||||
import org.thingsboard.server.service.security.model.ActivateUserRequest;
|
||||
@ -75,6 +77,7 @@ public class AuthController extends BaseController {
|
||||
private final JwtTokenFactory tokenFactory;
|
||||
private final MailService mailService;
|
||||
private final SystemSecurityService systemSecurityService;
|
||||
private final SecuritySettingsService securitySettingsService;
|
||||
private final RateLimitService rateLimitService;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@ -82,7 +85,7 @@ public class AuthController extends BaseController {
|
||||
@ApiOperation(value = "Get current User (getUser)",
|
||||
notes = "Get the information about the User which credentials are used to perform this REST API call.")
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@RequestMapping(value = "/auth/user", method = RequestMethod.GET)
|
||||
@GetMapping(value = "/auth/user")
|
||||
public @ResponseBody
|
||||
User getUser() throws ThingsboardException {
|
||||
SecurityUser securityUser = getCurrentUser();
|
||||
@ -92,7 +95,7 @@ public class AuthController extends BaseController {
|
||||
@ApiOperation(value = "Logout (logout)",
|
||||
notes = "Special API call to record the 'logout' of the user to the Audit Logs. Since platform uses [JWT](https://jwt.io/), the actual logout is the procedure of clearing the [JWT](https://jwt.io/) token on the client side. ")
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@RequestMapping(value = "/auth/logout", method = RequestMethod.POST)
|
||||
@PostMapping(value = "/auth/logout")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public void logout(HttpServletRequest request) throws ThingsboardException {
|
||||
logLogoutAction(request);
|
||||
@ -101,8 +104,7 @@ public class AuthController extends BaseController {
|
||||
@ApiOperation(value = "Change password for current User (changePassword)",
|
||||
notes = "Change the password for the User which credentials are used to perform this REST API call. Be aware that previously generated [JWT](https://jwt.io/) tokens will be still valid until they expire.")
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
@PostMapping(value = "/auth/changePassword")
|
||||
public JwtPair changePassword(@Parameter(description = "Change Password Request")
|
||||
@RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException {
|
||||
String currentPassword = changePasswordRequest.getCurrentPassword();
|
||||
@ -125,11 +127,9 @@ public class AuthController extends BaseController {
|
||||
|
||||
@ApiOperation(value = "Get the current User password policy (getUserPasswordPolicy)",
|
||||
notes = "API call to get the password policy for the password validation form(s).")
|
||||
@RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/noauth/userPasswordPolicy")
|
||||
public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException {
|
||||
SecuritySettings securitySettings =
|
||||
checkNotNull(systemSecurityService.getSecuritySettings());
|
||||
SecuritySettings securitySettings = checkNotNull(securitySettingsService.getSecuritySettings());
|
||||
return securitySettings.getPasswordPolicy();
|
||||
}
|
||||
|
||||
@ -137,14 +137,14 @@ public class AuthController extends BaseController {
|
||||
notes = "Checks the activation token and forwards user to 'Create Password' page. " +
|
||||
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " +
|
||||
"If token is not valid, returns '409 Conflict'.")
|
||||
@RequestMapping(value = "/noauth/activate", params = {"activateToken"}, method = RequestMethod.GET)
|
||||
@GetMapping(value = "/noauth/activate", params = {"activateToken"})
|
||||
public ResponseEntity<String> checkActivateToken(
|
||||
@Parameter(description = "The activate token string.")
|
||||
@RequestParam(value = "activateToken") String activateToken) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
HttpStatus responseStatus;
|
||||
UserCredentials userCredentials = userService.findUserCredentialsByActivateToken(TenantId.SYS_TENANT_ID, activateToken);
|
||||
if (userCredentials != null) {
|
||||
if (userCredentials != null && !userCredentials.isActivationTokenExpired()) {
|
||||
String createURI = "/login/createPassword";
|
||||
try {
|
||||
URI location = new URI(createURI + "?activateToken=" + activateToken);
|
||||
@ -163,8 +163,7 @@ public class AuthController extends BaseController {
|
||||
@ApiOperation(value = "Request reset password email (requestResetPasswordByEmail)",
|
||||
notes = "Request to send the reset password email if the user with specified email address is present in the database. " +
|
||||
"Always return '200 OK' status for security purposes.")
|
||||
@RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
@PostMapping(value = "/noauth/resetPasswordByEmail")
|
||||
public void requestResetPasswordByEmail(
|
||||
@Parameter(description = "The JSON object representing the reset password email request.")
|
||||
@RequestBody ResetPasswordEmailRequest resetPasswordByEmailRequest,
|
||||
@ -187,7 +186,7 @@ public class AuthController extends BaseController {
|
||||
notes = "Checks the password reset token and forwards user to 'Reset Password' page. " +
|
||||
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " +
|
||||
"If token is not valid, returns '409 Conflict'.")
|
||||
@RequestMapping(value = "/noauth/resetPassword", params = {"resetToken"}, method = RequestMethod.GET)
|
||||
@GetMapping(value = "/noauth/resetPassword", params = {"resetToken"})
|
||||
public ResponseEntity<String> checkResetToken(
|
||||
@Parameter(description = "The reset token string.")
|
||||
@RequestParam(value = "resetToken") String resetToken) {
|
||||
@ -196,7 +195,7 @@ public class AuthController extends BaseController {
|
||||
String resetURI = "/login/resetPassword";
|
||||
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
|
||||
|
||||
if (userCredentials != null) {
|
||||
if (userCredentials != null && !userCredentials.isResetTokenExpired()) {
|
||||
if (!rateLimitService.checkRateLimit(LimitedApi.PASSWORD_RESET, userCredentials.getUserId(), defaultLimitsConfiguration)) {
|
||||
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
|
||||
}
|
||||
@ -220,15 +219,12 @@ public class AuthController extends BaseController {
|
||||
"The response already contains the [JWT](https://jwt.io) activation and refresh tokens, " +
|
||||
"to simplify the user activation flow and avoid asking user to input password again after activation. " +
|
||||
"If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
|
||||
"If token is not valid, returns '404 Bad Request'.")
|
||||
@RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
@ResponseBody
|
||||
public JwtPair activateUser(
|
||||
@Parameter(description = "Activate user request.")
|
||||
@RequestBody ActivateUserRequest activateRequest,
|
||||
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
|
||||
HttpServletRequest request) throws ThingsboardException {
|
||||
"If token is not valid, returns '400 Bad Request'.")
|
||||
@PostMapping(value = "/noauth/activate")
|
||||
public JwtPair activateUser(@Parameter(description = "Activate user request.")
|
||||
@RequestBody ActivateUserRequest activateRequest,
|
||||
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
|
||||
HttpServletRequest request) {
|
||||
String activateToken = activateRequest.getActivateToken();
|
||||
String password = activateRequest.getPassword();
|
||||
systemSecurityService.validatePassword(password, null);
|
||||
@ -258,18 +254,18 @@ public class AuthController extends BaseController {
|
||||
@ApiOperation(value = "Reset password (resetPassword)",
|
||||
notes = "Checks the password reset token and updates the password. " +
|
||||
"If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
|
||||
"If token is not valid, returns '404 Bad Request'.")
|
||||
@RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
@ResponseBody
|
||||
public JwtPair resetPassword(
|
||||
@Parameter(description = "Reset password request.")
|
||||
@RequestBody ResetPasswordRequest resetPasswordRequest,
|
||||
HttpServletRequest request) throws ThingsboardException {
|
||||
"If token is not valid, returns '400 Bad Request'.")
|
||||
@PostMapping(value = "/noauth/resetPassword")
|
||||
public JwtPair resetPassword(@Parameter(description = "Reset password request.")
|
||||
@RequestBody ResetPasswordRequest resetPasswordRequest,
|
||||
HttpServletRequest request) throws ThingsboardException {
|
||||
String resetToken = resetPasswordRequest.getResetToken();
|
||||
String password = resetPasswordRequest.getPassword();
|
||||
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
|
||||
if (userCredentials != null) {
|
||||
if (userCredentials.isResetTokenExpired()) {
|
||||
throw new ThingsboardException("Password reset token expired", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
|
||||
}
|
||||
systemSecurityService.validatePassword(password, userCredentials);
|
||||
if (passwordEncoder.matches(password, userCredentials.getPassword())) {
|
||||
throw new ThingsboardException("New password should be different from existing!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
|
||||
@ -277,6 +273,7 @@ public class AuthController extends BaseController {
|
||||
String encodedPassword = passwordEncoder.encode(password);
|
||||
userCredentials.setPassword(encodedPassword);
|
||||
userCredentials.setResetToken(null);
|
||||
userCredentials.setResetTokenExpTime(null);
|
||||
userCredentials = userService.replaceUserCredentials(TenantId.SYS_TENANT_ID, userCredentials);
|
||||
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
|
||||
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
|
||||
|
||||
@ -178,7 +178,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
|
||||
@TbCoreComponent
|
||||
public abstract class BaseController {
|
||||
|
||||
private final Logger log = org.slf4j.LoggerFactory.getLogger(getClass());
|
||||
protected final Logger log = org.slf4j.LoggerFactory.getLogger(getClass());
|
||||
|
||||
/*Swagger UI description*/
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@ -130,12 +129,8 @@ public class UserController extends BaseController {
|
||||
private final SystemSecurityService systemSecurityService;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final TbUserService tbUserService;
|
||||
|
||||
@Autowired
|
||||
private EntityQueryService entityQueryService;
|
||||
|
||||
@Autowired
|
||||
private EntityService entityService;
|
||||
private final EntityQueryService entityQueryService;
|
||||
private final EntityService entityService;
|
||||
|
||||
@ApiOperation(value = "Get User (getUserById)",
|
||||
notes = "Fetch the User object based on the provided User Id. " +
|
||||
@ -212,7 +207,7 @@ public class UserController extends BaseController {
|
||||
public User saveUser(
|
||||
@Parameter(description = "A JSON value representing the User.", required = true)
|
||||
@RequestBody User user,
|
||||
@Parameter(description = "Send activation email (or use activation link)" , schema = @Schema(defaultValue = "true"))
|
||||
@Parameter(description = "Send activation email (or use activation link)", schema = @Schema(defaultValue = "true"))
|
||||
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException {
|
||||
if (!Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
|
||||
user.setTenantId(getCurrentUser().getTenantId());
|
||||
@ -231,19 +226,10 @@ public class UserController extends BaseController {
|
||||
@RequestParam(value = "email") String email,
|
||||
HttpServletRequest request) throws ThingsboardException {
|
||||
User user = checkNotNull(userService.findUserByEmail(getCurrentUser().getTenantId(), email));
|
||||
accessControlService.checkPermission(getCurrentUser(), Resource.USER, Operation.READ, user.getId(), user);
|
||||
|
||||
accessControlService.checkPermission(getCurrentUser(), Resource.USER, Operation.READ,
|
||||
user.getId(), user);
|
||||
|
||||
UserCredentials userCredentials = userService.findUserCredentialsByUserId(getCurrentUser().getTenantId(), user.getId());
|
||||
if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) {
|
||||
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
|
||||
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
|
||||
userCredentials.getActivateToken());
|
||||
mailService.sendActivationEmail(activateUrl, email);
|
||||
} else {
|
||||
throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
|
||||
}
|
||||
String activationLink = getActivationLink(user.getId(), request);
|
||||
mailService.sendActivationEmail(activationLink, email);
|
||||
}
|
||||
|
||||
@ApiOperation(value = "Get the activation link (getActivationLink)",
|
||||
@ -252,23 +238,13 @@ public class UserController extends BaseController {
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
||||
@RequestMapping(value = "/user/{userId}/activationLink", method = RequestMethod.GET, produces = "text/plain")
|
||||
@ResponseBody
|
||||
public String getActivationLink(
|
||||
@Parameter(description = USER_ID_PARAM_DESCRIPTION)
|
||||
@PathVariable(USER_ID) String strUserId,
|
||||
HttpServletRequest request) throws ThingsboardException {
|
||||
public String getActivationLink(@Parameter(description = USER_ID_PARAM_DESCRIPTION)
|
||||
@PathVariable(USER_ID) String strUserId,
|
||||
HttpServletRequest request) throws ThingsboardException {
|
||||
checkParameter(USER_ID, strUserId);
|
||||
UserId userId = new UserId(toUUID(strUserId));
|
||||
User user = checkUserId(userId, Operation.READ);
|
||||
SecurityUser authUser = getCurrentUser();
|
||||
UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), user.getId());
|
||||
if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) {
|
||||
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
|
||||
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
|
||||
userCredentials.getActivateToken());
|
||||
return activateUrl;
|
||||
} else {
|
||||
throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
|
||||
}
|
||||
checkUserId(userId, Operation.READ);
|
||||
return getActivationLink(userId, request);
|
||||
}
|
||||
|
||||
@ApiOperation(value = "Delete User (deleteUser)",
|
||||
@ -411,7 +387,7 @@ public class UserController extends BaseController {
|
||||
public void setUserCredentialsEnabled(
|
||||
@Parameter(description = USER_ID_PARAM_DESCRIPTION)
|
||||
@PathVariable(USER_ID) String strUserId,
|
||||
@Parameter(description = "Enable (\"true\") or disable (\"false\") the credentials." , schema = @Schema(defaultValue = "true"))
|
||||
@Parameter(description = "Enable (\"true\") or disable (\"false\") the credentials.", schema = @Schema(defaultValue = "true"))
|
||||
@RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException {
|
||||
checkParameter(USER_ID, strUserId);
|
||||
UserId userId = new UserId(toUUID(strUserId));
|
||||
@ -610,6 +586,22 @@ public class UserController extends BaseController {
|
||||
userService.removeMobileSession(user.getTenantId(), mobileToken);
|
||||
}
|
||||
|
||||
private String getActivationLink(UserId userId, HttpServletRequest request) throws ThingsboardException {
|
||||
TenantId tenantId = getTenantId();
|
||||
UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, userId);
|
||||
if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) {
|
||||
if (userCredentials.isActivationTokenExpired()) {
|
||||
userCredentials = userService.generateUserActivationToken(userCredentials);
|
||||
userCredentials = userService.saveUserCredentials(tenantId, userCredentials);
|
||||
log.debug("[{}][{}] Regenerated expired user activation token", tenantId, userId);
|
||||
}
|
||||
String baseUrl = systemSecurityService.getBaseUrl(tenantId, getCurrentUser().getCustomerId(), request);
|
||||
return String.format(ACTIVATE_URL_PATTERN, baseUrl, userCredentials.getActivateToken());
|
||||
} else {
|
||||
throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException {
|
||||
if (type.isReserved()) {
|
||||
throw new ThingsboardException("Settings with type: " + strType + " are reserved for internal use!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
|
||||
|
||||
@ -56,8 +56,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
|
||||
if (sendEmail) {
|
||||
UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, savedUser.getId());
|
||||
String baseUrl = systemSecurityService.getBaseUrl(tenantId, customerId, request);
|
||||
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
|
||||
userCredentials.getActivateToken());
|
||||
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl, userCredentials.getActivateToken());
|
||||
String email = savedUser.getEmail();
|
||||
try {
|
||||
mailService.sendActivationEmail(activateUrl, email);
|
||||
|
||||
@ -25,11 +25,12 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
||||
import org.thingsboard.server.common.data.security.model.JwtPair;
|
||||
import org.thingsboard.server.dao.entity.AbstractCachedService;
|
||||
import org.thingsboard.server.dao.settings.SecuritySettingsService;
|
||||
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;
|
||||
|
||||
import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_MOBILE_SECRET_KEY_LENGTH;
|
||||
import static org.thingsboard.server.dao.settings.DefaultSecuritySettingsService.DEFAULT_MOBILE_SECRET_KEY_LENGTH;
|
||||
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@ -37,12 +38,12 @@ import static org.thingsboard.server.service.security.system.DefaultSystemSecuri
|
||||
public class MobileAppSecretServiceImpl extends AbstractCachedService<String, JwtPair, MobileSecretEvictEvent> implements MobileAppSecretService {
|
||||
|
||||
private final JwtTokenFactory tokenFactory;
|
||||
private final SystemSecurityService systemSecurityService;
|
||||
private final SecuritySettingsService securitySettingsService;
|
||||
|
||||
@Override
|
||||
public String generateMobileAppSecret(SecurityUser securityUser) {
|
||||
log.trace("Executing generateSecret for user [{}]", securityUser.getId());
|
||||
Integer mobileSecretKeyLength = systemSecurityService.getSecuritySettings().getMobileSecretKeyLength();
|
||||
Integer mobileSecretKeyLength = securitySettingsService.getSecuritySettings().getMobileSecretKeyLength();
|
||||
String secret = StringUtils.generateSafeToken(mobileSecretKeyLength == null ? DEFAULT_MOBILE_SECRET_KEY_LENGTH : mobileSecretKeyLength);
|
||||
cache.put(secret, tokenFactory.createTokenPair(securityUser));
|
||||
return secret;
|
||||
@ -63,4 +64,5 @@ public class MobileAppSecretServiceImpl extends AbstractCachedService<String, Jw
|
||||
public void handleEvictEvent(MobileSecretEvictEvent event) {
|
||||
cache.evict(event.getSecret());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings;
|
||||
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
|
||||
import org.thingsboard.server.dao.customer.CustomerService;
|
||||
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||
import org.thingsboard.server.dao.settings.SecuritySettingsService;
|
||||
import org.thingsboard.server.dao.user.UserService;
|
||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
|
||||
@ -58,6 +59,7 @@ import java.util.UUID;
|
||||
public class RestAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final SystemSecurityService systemSecurityService;
|
||||
private final SecuritySettingsService securitySettingsService;
|
||||
private final UserService userService;
|
||||
private final CustomerService customerService;
|
||||
private final TwoFactorAuthService twoFactorAuthService;
|
||||
@ -66,10 +68,12 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
|
||||
public RestAuthenticationProvider(final UserService userService,
|
||||
final CustomerService customerService,
|
||||
final SystemSecurityService systemSecurityService,
|
||||
SecuritySettingsService securitySettingsService,
|
||||
TwoFactorAuthService twoFactorAuthService) {
|
||||
this.userService = userService;
|
||||
this.customerService = customerService;
|
||||
this.systemSecurityService = systemSecurityService;
|
||||
this.securitySettingsService = securitySettingsService;
|
||||
this.twoFactorAuthService = twoFactorAuthService;
|
||||
}
|
||||
|
||||
@ -82,13 +86,13 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
|
||||
throw new BadCredentialsException("Authentication Failed. Bad user principal.");
|
||||
}
|
||||
|
||||
UserPrincipal userPrincipal = (UserPrincipal) principal;
|
||||
UserPrincipal userPrincipal = (UserPrincipal) principal;
|
||||
SecurityUser securityUser;
|
||||
if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
|
||||
String username = userPrincipal.getValue();
|
||||
String password = (String) authentication.getCredentials();
|
||||
|
||||
SecuritySettings securitySettings = systemSecurityService.getSecuritySettings();
|
||||
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
|
||||
UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
|
||||
if (Boolean.TRUE.equals(passwordPolicy.getForceUserToResetPasswordIfNotValid())) {
|
||||
try {
|
||||
|
||||
@ -18,6 +18,8 @@ package org.thingsboard.server.service.security.system;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.passay.CharacterRule;
|
||||
import org.passay.EnglishCharacterData;
|
||||
@ -27,9 +29,6 @@ import org.passay.PasswordValidator;
|
||||
import org.passay.Rule;
|
||||
import org.passay.RuleResult;
|
||||
import org.passay.WhitespaceRule;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
@ -53,6 +52,7 @@ import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettin
|
||||
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.settings.SecuritySettingsService;
|
||||
import org.thingsboard.server.dao.user.UserService;
|
||||
import org.thingsboard.server.dao.user.UserServiceImpl;
|
||||
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
|
||||
@ -61,83 +61,28 @@ import org.thingsboard.server.service.security.model.SecurityUser;
|
||||
import org.thingsboard.server.utils.MiscUtils;
|
||||
import ua_parser.Client;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class DefaultSystemSecurityService implements SystemSecurityService {
|
||||
|
||||
public static final int DEFAULT_MOBILE_SECRET_KEY_LENGTH = 64;
|
||||
|
||||
@Autowired
|
||||
private AdminSettingsService adminSettingsService;
|
||||
|
||||
@Autowired
|
||||
private BCryptPasswordEncoder encoder;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private MailService mailService;
|
||||
|
||||
@Autowired
|
||||
private AuditLogService auditLogService;
|
||||
|
||||
@Resource
|
||||
private SystemSecurityService self;
|
||||
|
||||
@Cacheable(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
|
||||
@Override
|
||||
public SecuritySettings getSecuritySettings() {
|
||||
SecuritySettings securitySettings = null;
|
||||
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "securitySettings");
|
||||
if (adminSettings != null) {
|
||||
try {
|
||||
securitySettings = JacksonUtil.convertValue(adminSettings.getJsonValue(), SecuritySettings.class);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load security settings!", e);
|
||||
}
|
||||
} else {
|
||||
securitySettings = new SecuritySettings();
|
||||
securitySettings.setPasswordPolicy(new UserPasswordPolicy());
|
||||
securitySettings.getPasswordPolicy().setMinimumLength(6);
|
||||
securitySettings.getPasswordPolicy().setMaximumLength(72);
|
||||
securitySettings.setMobileSecretKeyLength(DEFAULT_MOBILE_SECRET_KEY_LENGTH);
|
||||
}
|
||||
return securitySettings;
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
|
||||
@Override
|
||||
public SecuritySettings saveSecuritySettings(SecuritySettings securitySettings) {
|
||||
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "securitySettings");
|
||||
if (adminSettings == null) {
|
||||
adminSettings = new AdminSettings();
|
||||
adminSettings.setTenantId(TenantId.SYS_TENANT_ID);
|
||||
adminSettings.setKey("securitySettings");
|
||||
}
|
||||
adminSettings.setJsonValue(JacksonUtil.valueToTree(securitySettings));
|
||||
AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings);
|
||||
try {
|
||||
return JacksonUtil.convertValue(savedAdminSettings.getJsonValue(), SecuritySettings.class);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load security settings!", e);
|
||||
}
|
||||
}
|
||||
private final AdminSettingsService adminSettingsService;
|
||||
private final BCryptPasswordEncoder encoder;
|
||||
private final UserService userService;
|
||||
private final MailService mailService;
|
||||
private final AuditLogService auditLogService;
|
||||
private final SecuritySettingsService securitySettingsService;
|
||||
|
||||
@Override
|
||||
public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException {
|
||||
if (!encoder.matches(password, userCredentials.getPassword())) {
|
||||
int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId());
|
||||
SecuritySettings securitySettings = self.getSecuritySettings();
|
||||
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
|
||||
if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) {
|
||||
if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) {
|
||||
lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
|
||||
@ -153,7 +98,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
|
||||
|
||||
userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId());
|
||||
|
||||
SecuritySettings securitySettings = self.getSecuritySettings();
|
||||
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
|
||||
if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
|
||||
if ((userCredentials.getCreatedTime()
|
||||
+ TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays()))
|
||||
@ -181,7 +126,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
|
||||
if (maxVerificationFailures != null && maxVerificationFailures > 0
|
||||
&& failedVerificationAttempts >= maxVerificationFailures) {
|
||||
userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false);
|
||||
SecuritySettings securitySettings = self.getSecuritySettings();
|
||||
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
|
||||
lockAccount(userId, securityUser.getEmail(), securitySettings.getUserLockoutNotificationEmail(), maxVerificationFailures);
|
||||
throw new LockedException("User account was locked due to exceeded 2FA verification attempts");
|
||||
}
|
||||
@ -200,7 +145,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
|
||||
|
||||
@Override
|
||||
public void validatePassword(String password, UserCredentials userCredentials) throws DataValidationException {
|
||||
SecuritySettings securitySettings = self.getSecuritySettings();
|
||||
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
|
||||
UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy();
|
||||
|
||||
validatePasswordByPolicy(password, passwordPolicy);
|
||||
@ -330,4 +275,5 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
|
||||
private static boolean isPositiveInteger(Integer val) {
|
||||
return val != null && val.intValue() > 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -15,26 +15,20 @@
|
||||
*/
|
||||
package org.thingsboard.server.service.security.system;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
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.security.UserCredentials;
|
||||
import org.thingsboard.server.common.data.security.model.SecuritySettings;
|
||||
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
|
||||
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
|
||||
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public interface SystemSecurityService {
|
||||
|
||||
SecuritySettings getSecuritySettings();
|
||||
|
||||
SecuritySettings saveSecuritySettings(SecuritySettings securitySettings);
|
||||
|
||||
void validatePasswordByPolicy(String password, UserPasswordPolicy passwordPolicy);
|
||||
|
||||
void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException;
|
||||
@ -48,4 +42,5 @@ public interface SystemSecurityService {
|
||||
void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e);
|
||||
|
||||
void logLoginAction(User user, Object authenticationDetails, ActionType actionType, String provider, Exception e);
|
||||
|
||||
}
|
||||
|
||||
@ -16,20 +16,28 @@
|
||||
package org.thingsboard.server.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.assertj.core.data.Offset;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
import org.thingsboard.server.common.data.security.Authority;
|
||||
import org.thingsboard.server.common.data.security.UserCredentials;
|
||||
import org.thingsboard.server.common.data.security.model.SecuritySettings;
|
||||
import org.thingsboard.server.dao.service.DaoSqlTest;
|
||||
import org.thingsboard.server.dao.user.UserCredentialsDao;
|
||||
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
|
||||
import org.thingsboard.server.service.security.model.ChangePasswordRequest;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
@ -39,55 +47,55 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
@DaoSqlTest
|
||||
public class AuthControllerTest extends AbstractControllerTest {
|
||||
|
||||
@SpyBean
|
||||
private UserCredentialsDao userCredentialsDao;
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
loginSysAdmin();
|
||||
SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class);
|
||||
|
||||
securitySettings.getPasswordPolicy().setMaximumLength(72);
|
||||
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(false);
|
||||
|
||||
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
|
||||
updateSecuritySettings(securitySettings -> {
|
||||
securitySettings.getPasswordPolicy().setMaximumLength(72);
|
||||
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUser() throws Exception {
|
||||
|
||||
doGet("/api/auth/user")
|
||||
.andExpect(status().isUnauthorized());
|
||||
|
||||
.andExpect(status().isUnauthorized());
|
||||
|
||||
loginSysAdmin();
|
||||
doGet("/api/auth/user")
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
|
||||
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
|
||||
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
|
||||
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
|
||||
|
||||
loginTenantAdmin();
|
||||
doGet("/api/auth/user")
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
|
||||
.andExpect(jsonPath("$.email",is(TENANT_ADMIN_EMAIL)));
|
||||
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name())))
|
||||
.andExpect(jsonPath("$.email", is(TENANT_ADMIN_EMAIL)));
|
||||
|
||||
loginCustomerUser();
|
||||
doGet("/api/auth/user")
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.authority",is(Authority.CUSTOMER_USER.name())))
|
||||
.andExpect(jsonPath("$.email",is(CUSTOMER_USER_EMAIL)));
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.authority", is(Authority.CUSTOMER_USER.name())))
|
||||
.andExpect(jsonPath("$.email", is(CUSTOMER_USER_EMAIL)));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testLoginLogout() throws Exception {
|
||||
loginSysAdmin();
|
||||
doGet("/api/auth/user")
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
|
||||
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
|
||||
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
|
||||
|
||||
TimeUnit.SECONDS.sleep(1); //We need to make sure that event for invalidating token was successfully processed
|
||||
|
||||
logout();
|
||||
doGet("/api/auth/user")
|
||||
.andExpect(status().isUnauthorized());
|
||||
.andExpect(status().isUnauthorized());
|
||||
|
||||
resetTokens();
|
||||
}
|
||||
@ -97,14 +105,14 @@ public class AuthControllerTest extends AbstractControllerTest {
|
||||
loginSysAdmin();
|
||||
doGet("/api/auth/user")
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
|
||||
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
|
||||
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
|
||||
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
|
||||
|
||||
refreshToken();
|
||||
doGet("/api/auth/user")
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
|
||||
.andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
|
||||
.andExpect(jsonPath("$.authority", is(Authority.SYS_ADMIN.name())))
|
||||
.andExpect(jsonPath("$.email", is(SYS_ADMIN_EMAIL)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -131,10 +139,10 @@ public class AuthControllerTest extends AbstractControllerTest {
|
||||
loginUser(TENANT_ADMIN_EMAIL, newPassword);
|
||||
|
||||
loginSysAdmin();
|
||||
SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class);
|
||||
securitySettings.getPasswordPolicy().setMaximumLength(15);
|
||||
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(true);
|
||||
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
|
||||
updateSecuritySettings(securitySettings -> {
|
||||
securitySettings.getPasswordPolicy().setMaximumLength(15);
|
||||
securitySettings.getPasswordPolicy().setForceUserToResetPasswordIfNotValid(true);
|
||||
});
|
||||
|
||||
//try to login with user password that is not valid after security settings was updated
|
||||
doPost("/api/auth/login", new LoginRequest(TENANT_ADMIN_EMAIL, newPassword))
|
||||
@ -142,6 +150,7 @@ public class AuthControllerTest extends AbstractControllerTest {
|
||||
.andExpect(jsonPath("$.message", is("The entered password violates our policies. If this is your real password, please reset it.")));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testShouldNotResetPasswordToTooLongValue() throws Exception {
|
||||
loginTenantAdmin();
|
||||
@ -163,9 +172,72 @@ public class AuthControllerTest extends AbstractControllerTest {
|
||||
|
||||
Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString());
|
||||
doPost("/api/noauth/resetPassword", resetPasswordRequest)
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message",
|
||||
is("Password must be no more than 72 characters in length.")));
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message",
|
||||
is("Password must be no more than 72 characters in length.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordResetLinkTtl() throws Exception {
|
||||
loginSysAdmin();
|
||||
int ttl = 24;
|
||||
updateSecuritySettings(securitySettings -> {
|
||||
securitySettings.setPasswordResetTokenTtl(ttl);
|
||||
});
|
||||
doPost("/api/noauth/resetPasswordByEmail", JacksonUtil.newObjectNode()
|
||||
.put("email", TENANT_ADMIN_EMAIL)).andExpect(status().isOk());
|
||||
|
||||
UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, tenantAdminUserId.getId());
|
||||
assertThat(userCredentials.getResetTokenExpTime()).isCloseTo(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttl), Offset.offset(120000L));
|
||||
userCredentials.setResetTokenExpTime(System.currentTimeMillis() - 1);
|
||||
userCredentialsDao.save(tenantId, userCredentials);
|
||||
|
||||
doGet("/api/noauth/resetPassword?resetToken={resetToken}", this.currentResetPasswordToken)
|
||||
.andExpect(status().isConflict());
|
||||
JsonNode resetPasswordRequest = JacksonUtil.newObjectNode()
|
||||
.put("resetToken", this.currentResetPasswordToken)
|
||||
.put("password", "wefwefe");
|
||||
doPost("/api/noauth/resetPassword", resetPasswordRequest).andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message", is("Password reset token expired")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActivationLinkTtl() throws Exception {
|
||||
loginSysAdmin();
|
||||
int ttl = 24;
|
||||
updateSecuritySettings(securitySettings -> {
|
||||
securitySettings.setUserActivationTokenTtl(ttl);
|
||||
});
|
||||
|
||||
loginTenantAdmin();
|
||||
User user = new User();
|
||||
user.setAuthority(Authority.TENANT_ADMIN);
|
||||
user.setEmail("tenant-admin-2@thingsboard.org");
|
||||
user = doPost("/api/user", user, User.class);
|
||||
|
||||
UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId());
|
||||
assertThat(userCredentials.getActivateTokenExpTime()).isCloseTo(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttl), Offset.offset(120000L));
|
||||
String initialActivationLink = doGet("/api/user/" + user.getId() + "/activationLink", String.class);
|
||||
String initialActivationToken = StringUtils.substringAfterLast(initialActivationLink, "activateToken=");
|
||||
|
||||
userCredentials.setActivateTokenExpTime(System.currentTimeMillis() - 1);
|
||||
userCredentialsDao.save(tenantId, userCredentials);
|
||||
doGet("/api/noauth/activate?activateToken={activateToken}", initialActivationToken)
|
||||
.andExpect(status().isConflict());
|
||||
doPost("/api/noauth/activate", JacksonUtil.newObjectNode()
|
||||
.put("activateToken", initialActivationToken)
|
||||
.put("password", "wefewe")).andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message", is("Activation token expired")));
|
||||
|
||||
String regeneratedActivationLink = doGet("/api/user/" + user.getId() + "/activationLink", String.class);
|
||||
String regeneratedActivationToken = StringUtils.substringAfterLast(regeneratedActivationLink, "activateToken=");
|
||||
assertThat(regeneratedActivationToken).isNotEqualTo(initialActivationLink);
|
||||
userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId());
|
||||
assertThat(userCredentials.getActivateTokenExpTime()).isCloseTo(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttl), Offset.offset(120000L));
|
||||
|
||||
doPost("/api/noauth/activate", JacksonUtil.newObjectNode()
|
||||
.put("activateToken", regeneratedActivationToken)
|
||||
.put("password", "wefewe")).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -173,4 +245,11 @@ public class AuthControllerTest extends AbstractControllerTest {
|
||||
doGet("/login").andExpect(status().isOk());
|
||||
doGet("/home").andExpect(status().isOk());
|
||||
}
|
||||
|
||||
private void updateSecuritySettings(Consumer<SecuritySettings> updater) throws Exception {
|
||||
SecuritySettings securitySettings = doGet("/api/admin/securitySettings", SecuritySettings.class);
|
||||
updater.accept(securitySettings);
|
||||
doPost("/api/admin/securitySettings", securitySettings).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -17,12 +17,12 @@ package org.thingsboard.server.dao.user;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
|
||||
import org.thingsboard.server.common.data.id.CustomerId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.id.TenantProfileId;
|
||||
import org.thingsboard.server.common.data.id.UserCredentialsId;
|
||||
import org.thingsboard.server.common.data.id.UserId;
|
||||
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
import org.thingsboard.server.common.data.page.PageLink;
|
||||
import org.thingsboard.server.common.data.security.UserCredentials;
|
||||
@ -59,6 +59,10 @@ public interface UserService extends EntityDaoService {
|
||||
|
||||
UserCredentials requestExpiredPasswordReset(TenantId tenantId, UserCredentialsId userCredentialsId);
|
||||
|
||||
UserCredentials generatePasswordResetToken(UserCredentials userCredentials);
|
||||
|
||||
UserCredentials generateUserActivationToken(UserCredentials userCredentials);
|
||||
|
||||
UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials);
|
||||
|
||||
void deleteUser(TenantId tenantId, User user);
|
||||
|
||||
@ -16,41 +16,31 @@
|
||||
package org.thingsboard.server.common.data.security;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.thingsboard.server.common.data.BaseData;
|
||||
import lombok.ToString;
|
||||
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
|
||||
import org.thingsboard.server.common.data.id.UserCredentialsId;
|
||||
import org.thingsboard.server.common.data.id.UserId;
|
||||
import org.thingsboard.server.common.data.validation.NoXss;
|
||||
|
||||
import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.getJson;
|
||||
import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.setJson;
|
||||
import java.io.Serial;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class UserCredentials extends BaseData<UserCredentialsId> {
|
||||
@ToString(callSuper = true)
|
||||
public class UserCredentials extends BaseDataWithAdditionalInfo<UserCredentialsId> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -2108436378880529163L;
|
||||
|
||||
private UserId userId;
|
||||
private boolean enabled;
|
||||
private String password;
|
||||
private String activateToken;
|
||||
private Long activateTokenExpTime;
|
||||
private String resetToken;
|
||||
private Long resetTokenExpTime;
|
||||
|
||||
@NoXss
|
||||
private transient JsonNode additionalInfo;
|
||||
|
||||
@JsonIgnore
|
||||
private byte[] additionalInfoBytes;
|
||||
|
||||
public JsonNode getAdditionalInfo() {
|
||||
return getJson(() -> additionalInfo, () -> additionalInfoBytes);
|
||||
}
|
||||
|
||||
public void setAdditionalInfo(JsonNode settings) {
|
||||
setJson(settings, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes);
|
||||
}
|
||||
|
||||
public UserCredentials() {
|
||||
super();
|
||||
}
|
||||
@ -59,75 +49,15 @@ public class UserCredentials extends BaseData<UserCredentialsId> {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public UserCredentials(UserCredentials userCredentials) {
|
||||
super(userCredentials);
|
||||
this.userId = userCredentials.getUserId();
|
||||
this.password = userCredentials.getPassword();
|
||||
this.enabled = userCredentials.isEnabled();
|
||||
this.activateToken = userCredentials.getActivateToken();
|
||||
this.resetToken = userCredentials.getResetToken();
|
||||
setAdditionalInfo(userCredentials.getAdditionalInfo());
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isActivationTokenExpired() {
|
||||
return activateTokenExpTime == null || System.currentTimeMillis() > activateTokenExpTime;
|
||||
}
|
||||
|
||||
public UserId getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(UserId userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getActivateToken() {
|
||||
return activateToken;
|
||||
}
|
||||
|
||||
public void setActivateToken(String activateToken) {
|
||||
this.activateToken = activateToken;
|
||||
}
|
||||
|
||||
public String getResetToken() {
|
||||
return resetToken;
|
||||
}
|
||||
|
||||
public void setResetToken(String resetToken) {
|
||||
this.resetToken = resetToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("UserCredentials [userId=");
|
||||
builder.append(userId);
|
||||
builder.append(", enabled=");
|
||||
builder.append(enabled);
|
||||
builder.append(", password=");
|
||||
builder.append(password);
|
||||
builder.append(", activateToken=");
|
||||
builder.append(activateToken);
|
||||
builder.append(", resetToken=");
|
||||
builder.append(resetToken);
|
||||
builder.append(", createdTime=");
|
||||
builder.append(createdTime);
|
||||
builder.append(", id=");
|
||||
builder.append(id);
|
||||
builder.append("]");
|
||||
return builder.toString();
|
||||
@JsonIgnore
|
||||
public boolean isResetTokenExpired() {
|
||||
return resetTokenExpTime == null || System.currentTimeMillis() > resetTokenExpTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -16,22 +16,39 @@
|
||||
package org.thingsboard.server.common.data.security.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Schema
|
||||
@Data
|
||||
public class SecuritySettings implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -1307613974597312465L;
|
||||
|
||||
@Schema(description = "The user password policy object." )
|
||||
@Schema(description = "The user password policy object.")
|
||||
private UserPasswordPolicy passwordPolicy;
|
||||
@Schema(description = "Maximum number of failed login attempts allowed before user account is locked." )
|
||||
|
||||
@Schema(description = "Maximum number of failed login attempts allowed before user account is locked.")
|
||||
private Integer maxFailedLoginAttempts;
|
||||
@Schema(description = "Email to use for notifications about locked users." )
|
||||
|
||||
@Schema(description = "Email to use for notifications about locked users.")
|
||||
private String userLockoutNotificationEmail;
|
||||
@Schema(description = "Mobile secret key length" )
|
||||
|
||||
@Schema(description = "Mobile secret key length")
|
||||
private Integer mobileSecretKeyLength;
|
||||
|
||||
@NotNull @Min(1) @Max(24)
|
||||
@Schema(description = "TTL in hours for user activation link", minimum = "1", maximum = "24", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Integer userActivationTokenTtl;
|
||||
|
||||
@NotNull @Min(1) @Max(24)
|
||||
@Schema(description = "TTL in hours for password reset link", minimum = "1", maximum = "24", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Integer passwordResetTokenTtl;
|
||||
|
||||
}
|
||||
|
||||
@ -77,7 +77,9 @@ public class ModelConstants {
|
||||
public static final String USER_CREDENTIALS_ENABLED_PROPERTY = "enabled";
|
||||
public static final String USER_CREDENTIALS_PASSWORD_PROPERTY = "password"; //NOSONAR, the constant used to identify password column name (not password value itself)
|
||||
public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY = "activate_token";
|
||||
public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_EXP_TIME_PROPERTY = "activate_token_exp_time";
|
||||
public static final String USER_CREDENTIALS_RESET_TOKEN_PROPERTY = "reset_token";
|
||||
public static final String USER_CREDENTIALS_RESET_TOKEN_EXP_TIME_PROPERTY = "reset_token_exp_time";
|
||||
public static final String USER_CREDENTIALS_ADDITIONAL_PROPERTY = "additional_info";
|
||||
|
||||
/**
|
||||
|
||||
@ -16,7 +16,10 @@
|
||||
package org.thingsboard.server.dao.model.sql;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.thingsboard.server.common.data.id.UserCredentialsId;
|
||||
@ -25,10 +28,6 @@ import org.thingsboard.server.common.data.security.UserCredentials;
|
||||
import org.thingsboard.server.dao.model.BaseEntity;
|
||||
import org.thingsboard.server.dao.model.BaseSqlEntity;
|
||||
import org.thingsboard.server.dao.model.ModelConstants;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import org.thingsboard.server.dao.util.mapping.JsonConverter;
|
||||
|
||||
import java.util.UUID;
|
||||
@ -51,9 +50,15 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
|
||||
@Column(name = ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY, unique = true)
|
||||
private String activateToken;
|
||||
|
||||
@Column(name = ModelConstants.USER_CREDENTIALS_ACTIVATE_TOKEN_EXP_TIME_PROPERTY)
|
||||
private Long activateTokenExpTime;
|
||||
|
||||
@Column(name = ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY, unique = true)
|
||||
private String resetToken;
|
||||
|
||||
@Column(name = ModelConstants.USER_CREDENTIALS_RESET_TOKEN_EXP_TIME_PROPERTY)
|
||||
private Long resetTokenExpTime;
|
||||
|
||||
@Convert(converter = JsonConverter.class)
|
||||
@Column(name = ModelConstants.USER_CREDENTIALS_ADDITIONAL_PROPERTY)
|
||||
private JsonNode additionalInfo;
|
||||
@ -73,7 +78,9 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
|
||||
this.enabled = userCredentials.isEnabled();
|
||||
this.password = userCredentials.getPassword();
|
||||
this.activateToken = userCredentials.getActivateToken();
|
||||
this.activateTokenExpTime = userCredentials.getActivateTokenExpTime();
|
||||
this.resetToken = userCredentials.getResetToken();
|
||||
this.resetTokenExpTime = userCredentials.getResetTokenExpTime();
|
||||
this.additionalInfo = userCredentials.getAdditionalInfo();
|
||||
}
|
||||
|
||||
@ -87,7 +94,9 @@ public final class UserCredentialsEntity extends BaseSqlEntity<UserCredentials>
|
||||
userCredentials.setEnabled(enabled);
|
||||
userCredentials.setPassword(password);
|
||||
userCredentials.setActivateToken(activateToken);
|
||||
userCredentials.setActivateTokenExpTime(activateTokenExpTime);
|
||||
userCredentials.setResetToken(resetToken);
|
||||
userCredentials.setResetTokenExpTime(resetTokenExpTime);
|
||||
userCredentials.setAdditionalInfo(additionalInfo);
|
||||
return userCredentials;
|
||||
}
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright © 2016-2024 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.dao.settings;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.AdminSettings;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.security.model.SecuritySettings;
|
||||
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
|
||||
import org.thingsboard.server.dao.service.ConstraintValidator;
|
||||
|
||||
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DefaultSecuritySettingsService implements SecuritySettingsService {
|
||||
|
||||
private final AdminSettingsService adminSettingsService;
|
||||
|
||||
public static final int DEFAULT_MOBILE_SECRET_KEY_LENGTH = 64;
|
||||
|
||||
@Cacheable(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
|
||||
@Override
|
||||
public SecuritySettings getSecuritySettings() {
|
||||
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "securitySettings");
|
||||
SecuritySettings securitySettings;
|
||||
if (adminSettings != null) {
|
||||
try {
|
||||
securitySettings = JacksonUtil.convertValue(adminSettings.getJsonValue(), SecuritySettings.class);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load security settings!", e);
|
||||
}
|
||||
} else {
|
||||
securitySettings = new SecuritySettings();
|
||||
securitySettings.setPasswordPolicy(new UserPasswordPolicy());
|
||||
securitySettings.getPasswordPolicy().setMinimumLength(6);
|
||||
securitySettings.getPasswordPolicy().setMaximumLength(72);
|
||||
securitySettings.setMobileSecretKeyLength(DEFAULT_MOBILE_SECRET_KEY_LENGTH);
|
||||
securitySettings.setPasswordResetTokenTtl(24);
|
||||
securitySettings.setUserActivationTokenTtl(24);
|
||||
}
|
||||
return securitySettings;
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'")
|
||||
@Override
|
||||
public SecuritySettings saveSecuritySettings(SecuritySettings securitySettings) {
|
||||
ConstraintValidator.validateFields(securitySettings);
|
||||
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "securitySettings");
|
||||
if (adminSettings == null) {
|
||||
adminSettings = new AdminSettings();
|
||||
adminSettings.setTenantId(TenantId.SYS_TENANT_ID);
|
||||
adminSettings.setKey("securitySettings");
|
||||
}
|
||||
adminSettings.setJsonValue(JacksonUtil.valueToTree(securitySettings));
|
||||
AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings);
|
||||
try {
|
||||
return JacksonUtil.convertValue(savedAdminSettings.getJsonValue(), SecuritySettings.class);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load security settings!", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright © 2016-2024 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.server.dao.settings;
|
||||
|
||||
import org.thingsboard.server.common.data.security.model.SecuritySettings;
|
||||
|
||||
public interface SecuritySettingsService {
|
||||
|
||||
SecuritySettings getSecuritySettings();
|
||||
|
||||
SecuritySettings saveSecuritySettings(SecuritySettings securitySettings);
|
||||
|
||||
}
|
||||
@ -60,6 +60,7 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
|
||||
import org.thingsboard.server.dao.exception.IncorrectParameterException;
|
||||
import org.thingsboard.server.dao.service.DataValidator;
|
||||
import org.thingsboard.server.dao.service.PaginatedRemover;
|
||||
import org.thingsboard.server.dao.settings.SecuritySettingsService;
|
||||
import org.thingsboard.server.dao.sql.JpaExecutorService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -69,6 +70,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thingsboard.server.common.data.StringUtils.generateSafeToken;
|
||||
import static org.thingsboard.server.dao.service.Validator.validateId;
|
||||
@ -99,6 +101,7 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
|
||||
private final UserAuthSettingsDao userAuthSettingsDao;
|
||||
private final UserSettingsService userSettingsService;
|
||||
private final UserSettingsDao userSettingsDao;
|
||||
private final SecuritySettingsService securitySettingsService;
|
||||
private final DataValidator<User> userValidator;
|
||||
private final DataValidator<UserCredentials> userCredentialsValidator;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
@ -175,9 +178,9 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
|
||||
countService.publishCountEntityEvictEvent(savedUser.getTenantId(), EntityType.USER);
|
||||
UserCredentials userCredentials = new UserCredentials();
|
||||
userCredentials.setEnabled(false);
|
||||
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
|
||||
userCredentials.setUserId(new UserId(savedUser.getUuidId()));
|
||||
userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode());
|
||||
userCredentials = generateUserActivationToken(userCredentials);
|
||||
userCredentialsDao.save(user.getTenantId(), userCredentials);
|
||||
}
|
||||
eventPublisher.publishEvent(SaveEntityEvent.builder()
|
||||
@ -239,8 +242,12 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
|
||||
if (userCredentials.isEnabled()) {
|
||||
throw new IncorrectParameterException("User credentials already activated");
|
||||
}
|
||||
if (userCredentials.isActivationTokenExpired()) {
|
||||
throw new IncorrectParameterException("Activation token expired");
|
||||
}
|
||||
userCredentials.setEnabled(true);
|
||||
userCredentials.setActivateToken(null);
|
||||
userCredentials.setActivateTokenExpTime(null);
|
||||
userCredentials.setPassword(password);
|
||||
if (userCredentials.getPassword() != null) {
|
||||
updatePasswordHistory(userCredentials);
|
||||
@ -260,7 +267,7 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
|
||||
if (!userCredentials.isEnabled()) {
|
||||
throw new DisabledException(String.format("User credentials not enabled [%s]", email));
|
||||
}
|
||||
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
|
||||
userCredentials = generatePasswordResetToken(userCredentials);
|
||||
return saveUserCredentials(tenantId, userCredentials);
|
||||
}
|
||||
|
||||
@ -270,10 +277,26 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
|
||||
if (!userCredentials.isEnabled()) {
|
||||
throw new IncorrectParameterException("Unable to reset password for inactive user");
|
||||
}
|
||||
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
|
||||
userCredentials = generatePasswordResetToken(userCredentials);
|
||||
return saveUserCredentials(tenantId, userCredentials);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserCredentials generatePasswordResetToken(UserCredentials userCredentials) {
|
||||
userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
|
||||
int ttlHours = securitySettingsService.getSecuritySettings().getPasswordResetTokenTtl();
|
||||
userCredentials.setResetTokenExpTime(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttlHours));
|
||||
return userCredentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserCredentials generateUserActivationToken(UserCredentials userCredentials) {
|
||||
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
|
||||
int ttlHours = securitySettingsService.getSecuritySettings().getUserActivationTokenTtl();
|
||||
userCredentials.setActivateTokenExpTime(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(ttlHours));
|
||||
return userCredentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials) {
|
||||
log.trace("Executing replaceUserCredentials [{}]", userCredentials);
|
||||
|
||||
@ -475,9 +475,11 @@ CREATE TABLE IF NOT EXISTS user_credentials (
|
||||
id uuid NOT NULL CONSTRAINT user_credentials_pkey PRIMARY KEY,
|
||||
created_time bigint NOT NULL,
|
||||
activate_token varchar(255) UNIQUE,
|
||||
activate_token_exp_time BIGINT,
|
||||
enabled boolean,
|
||||
password varchar(255),
|
||||
reset_token varchar(255) UNIQUE,
|
||||
reset_token_exp_time BIGINT,
|
||||
user_id uuid UNIQUE,
|
||||
additional_info varchar DEFAULT '{}'
|
||||
);
|
||||
|
||||
@ -63,7 +63,9 @@ public class JpaUserCredentialsDaoTest extends AbstractJpaDaoTest {
|
||||
userCredentials.setUserId(new UserId(UUID.randomUUID()));
|
||||
userCredentials.setPassword("password");
|
||||
userCredentials.setActivateToken("ACTIVATE_TOKEN_" + number);
|
||||
userCredentials.setActivateTokenExpTime(123L);
|
||||
userCredentials.setResetToken("RESET_TOKEN_" + number);
|
||||
userCredentials.setResetTokenExpTime(321L);
|
||||
return userCredentialsDao.save(SYSTEM_TENANT_ID, userCredentials);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user