Browse Source

[PROD-678] Authorization and password reset vulnerability fix (#4569)

* Fixed vulnerabilities for password reset and authorization

* Improvements to check user and credentials for null

* Correct messages and logs

* Improvements

* Reset Password Test: added delay after resetting password to synchronize test with server

* Executor removed from controller

* Correct method calling

* Formatting cleaned
pull/4872/head
AndrewVolosytnykhThingsboard 5 years ago
committed by GitHub
parent
commit
228fddb8cd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  2. 3
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
  3. 14
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  4. 1
      application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
  5. 2
      application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
  6. 11
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  7. 13
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java
  8. 5
      ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html
  9. 8
      ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts
  10. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -135,7 +135,7 @@ public class AuthController extends BaseController {
}
}
@RequestMapping(value = "/noauth/activate", params = { "activateToken" }, method = RequestMethod.GET)
@RequestMapping(value = "/noauth/activate", params = {"activateToken"}, method = RequestMethod.GET)
public ResponseEntity<String> checkActivateToken(
@RequestParam(value = "activateToken") String activateToken) {
HttpHeaders headers = new HttpHeaders();
@ -159,7 +159,7 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void requestResetPasswordByEmail (
public void requestResetPasswordByEmail(
@RequestBody JsonNode resetPasswordByEmailRequest,
HttpServletRequest request) throws ThingsboardException {
try {
@ -170,13 +170,13 @@ public class AuthController extends BaseController {
String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
userCredentials.getResetToken());
mailService.sendResetPasswordEmail(resetUrl, email);
mailService.sendResetPasswordEmailAsync(resetUrl, email);
} catch (Exception e) {
throw handleException(e);
log.warn("Error occurred: {}", e.getMessage());
}
}
@RequestMapping(value = "/noauth/resetPassword", params = { "resetToken" }, method = RequestMethod.GET)
@RequestMapping(value = "/noauth/resetPassword", params = {"resetToken"}, method = RequestMethod.GET)
public ResponseEntity<String> checkResetToken(
@RequestParam(value = "resetToken") String resetToken) {
HttpHeaders headers = new HttpHeaders();

3
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java

@ -25,6 +25,7 @@ import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@ -152,7 +153,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
private void handleAuthenticationException(AuthenticationException authenticationException, HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
if (authenticationException instanceof BadCredentialsException) {
if (authenticationException instanceof BadCredentialsException || authenticationException instanceof UsernameNotFoundException) {
mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
} else if (authenticationException instanceof DisabledException) {
mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));

14
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -73,6 +73,9 @@ public class DefaultMailService implements MailService {
@Autowired
private TbApiUsageStateService apiUsageStateService;
@Autowired
private MailExecutorService mailExecutorService;
private JavaMailSenderImpl mailSender;
private String mailFrom;
@ -221,6 +224,17 @@ public class DefaultMailService implements MailService {
sendMail(mailSender, mailFrom, email, subject, message);
}
@Override
public void sendResetPasswordEmailAsync(String passwordResetLink, String email) {
mailExecutorService.execute(() -> {
try {
this.sendResetPasswordEmail(passwordResetLink, email);
} catch (ThingsboardException e) {
log.error("Error occurred: {} ", e.getMessage());
}
});
}
@Override
public void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException {

1
application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java

@ -155,6 +155,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
.andExpect(status().isOk());
Thread.sleep(1000);
doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));

2
application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java

@ -51,7 +51,7 @@ public class TestMailService {
currentResetPasswordToken = passwordResetLink.split("=")[1];
return null;
}
}).when(mailService).sendResetPasswordEmail(Mockito.anyString(), Mockito.anyString());
}).when(mailService).sendResetPasswordEmailAsync(Mockito.anyString(), Mockito.anyString());
return mailService;
}

11
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -25,7 +25,10 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
@ -49,7 +52,6 @@ import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantDao;
import org.thingsboard.common.util.JacksonUtil;
import java.util.HashMap;
import java.util.Map;
@ -194,11 +196,11 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
DataValidator.validateEmail(email);
User user = userDao.findByEmail(tenantId, email);
if (user == null) {
throw new IncorrectParameterException(String.format("Unable to find user by email [%s]", email));
throw new UsernameNotFoundException(String.format("Unable to find user by email [%s]", email));
}
UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, user.getUuidId());
if (!userCredentials.isEnabled()) {
throw new IncorrectParameterException("Unable to reset password for inactive user");
throw new DisabledException(String.format("User credentials not enabled [%s]", email));
}
userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH));
return saveUserCredentials(tenantId, userCredentials);
@ -365,7 +367,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
JsonNode userPasswordHistoryJson;
if (additionalInfo.has(USER_PASSWORD_HISTORY)) {
userPasswordHistoryJson = additionalInfo.get(USER_PASSWORD_HISTORY);
userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>(){});
userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {
});
}
if (userPasswordHistoryMap != null) {
userPasswordHistoryMap.put(Long.toString(System.currentTimeMillis()), userCredentials.getPassword());

13
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java

@ -31,22 +31,25 @@ public interface MailService {
void updateMailConfiguration();
void sendEmail(TenantId tenantId, String email, String subject, String message) throws ThingsboardException;
void sendTestMail(JsonNode config, String email) throws ThingsboardException;
void sendActivationEmail(String activationLink, String email) throws ThingsboardException;
void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException;
void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException;
void sendResetPasswordEmailAsync(String passwordResetLink, String email);
void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException;
void sendAccountLockoutEmail( String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException;
void sendAccountLockoutEmail(String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException;
void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map<String, String> images) throws ThingsboardException;
void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map<String, String> images, JavaMailSender javaMailSender) throws ThingsboardException;
void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException;
}

5
ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html

@ -15,7 +15,8 @@
limitations under the License.
-->
<div class="tb-request-password-reset-content mat-app-background tb-dark" fxLayout="row" fxLayoutAlign="center center" style="width: 100%;">
<div class="tb-request-password-reset-content mat-app-background tb-dark" fxLayout="row" fxLayoutAlign="center center"
style="width: 100%;">
<mat-card fxFlex="initial" class="tb-request-password-reset-card">
<mat-card-title class="layout-padding">
<span translate class="mat-headline">login.request-password-reset</span>
@ -38,7 +39,7 @@
</mat-form-field>
<div fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="16px" fxLayoutAlign="start center"
fxLayoutAlign.gt-xs="center start">
<button mat-raised-button color="accent" type="submit" [disabled]="(isLoading$ | async)">
<button mat-raised-button color="accent" type="submit" [disabled]="(isLoading$ | async) || this.clicked">
{{ 'login.request-password-reset' | translate }}
</button>
<button mat-raised-button color="primary" type="button" [disabled]="(isLoading$ | async)"

8
ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts

@ -30,6 +30,8 @@ import { TranslateService } from '@ngx-translate/core';
})
export class ResetPasswordRequestComponent extends PageComponent implements OnInit {
clicked: boolean = false;
requestPasswordRequest = this.fb.group({
email: ['', [Validators.email, Validators.required]]
}, {updateOn: 'submit'});
@ -44,8 +46,14 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn
ngOnInit() {
}
disableInputs() {
this.requestPasswordRequest.disable();
this.clicked = true;
}
sendResetPasswordLink() {
if (this.requestPasswordRequest.valid) {
this.disableInputs();
this.authService.sendResetPasswordLink(this.requestPasswordRequest.get('email').value).subscribe(
() => {
this.store.dispatch(new ActionNotificationShow({

2
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2271,7 +2271,7 @@
"expired-password-reset-message": "Your credentials has been expired! Please create new password.",
"new-password": "New password",
"new-password-again": "New password again",
"password-link-sent-message": "Password reset link was successfully sent!",
"password-link-sent-message": "Reset link has been sent",
"email": "Email",
"login-with": "Login with {{name}}",
"or": "or",

Loading…
Cancel
Save