diff --git a/core/modules/user/src/Controller/UserController.php b/core/modules/user/src/Controller/UserController.php index be15fac..495edaa 100644 --- a/core/modules/user/src/Controller/UserController.php +++ b/core/modules/user/src/Controller/UserController.php @@ -119,12 +119,11 @@ public function resetPass(Request $request, $uid, $timestamp, $hash) { // A different user is already logged in on the computer. else { /** @var \Drupal\user\UserInterface $reset_link_user */ - if ($reset_link_user = $this->userStorage->load($uid)) { + if ($this->validPassResetHash($uid, $timestamp, $hash) && $reset_link_user = $this->userStorage->load($uid)) { drupal_set_message($this->t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please log out and try using the link again.', ['%other_user' => $account->getUsername(), '%resetting_user' => $reset_link_user->getUsername(), ':logout' => $this->url('user.logout')]), 'warning'); } else { - // Invalid one-time link specifies an unknown user. drupal_set_message($this->t('The one-time login link you clicked is invalid.'), 'error'); } return $this->redirect(''); @@ -170,10 +169,35 @@ public function getResetPassForm(Request $request, $uid) { /** @var \Drupal\user\UserInterface $user */ $user = $this->userStorage->load($uid); - if ($user === NULL || !$user->isActive()) { - // Blocked or invalid user ID, so deny access. The parameters will be in - // the watchdog's URL for the administrator to check. - throw new AccessDeniedHttpException(); + + // Check whether the password reset URL is valid. + if (!$this->validPassResetHash($uid, $timestamp, $hash)) { + $current = REQUEST_TIME; + // Time out, in seconds, until login URL expires. + $timeout = $this->config('user.settings')->get('password_reset_timeout'); + $current_user = \Drupal::currentUser(); + if ($current_user->isAuthenticated() && $user != $current_user) { + // User is logged in - redirect to My Account. + drupal_set_message($this->t('You have tried to use a one-time login link that has either been used or is no longer valid.'), 'error'); + throw new AccessDeniedHttpException(); + } + elseif ($user->getLastLoginTime() && $current - $timestamp > $timeout) { + // No time out for first time login. + drupal_set_message($this->t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'error'); + return $this->redirect('user.pass'); + } + // Verify that the user exists and is active. + elseif (!$user->isActive()) { + // Blocked or invalid user ID, so deny access. The parameters will be in + // the watchdog's URL for the administrator to check. + drupal_set_message($this->t('The one-time login link you clicked is invalid.'), 'error'); + return $this->redirect('user.pass'); + } + else { + // Redirect to the password reset form. + drupal_set_message($this->t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'error'); + return $this->redirect('user.pass'); + } } // Time out, in seconds, until login URL expires. @@ -203,44 +227,55 @@ public function getResetPassForm(Request $request, $uid) { */ public function resetPassLogin($uid, $timestamp, $hash) { // The current user is not logged in, so check the parameters. - $current = REQUEST_TIME; /** @var \Drupal\user\UserInterface $user */ $user = $this->userStorage->load($uid); - // Verify that the user exists and is active. if ($user === NULL || !$user->isActive()) { - // Blocked or invalid user ID, so deny access. The parameters will be in - // the watchdog's URL for the administrator to check. - throw new AccessDeniedHttpException(); - } - - // Time out, in seconds, until login URL expires. - $timeout = $this->config('user.settings')->get('password_reset_timeout'); - // No time out for first time login. - if ($user->getLastLoginTime() && $current - $timestamp > $timeout) { - drupal_set_message($this->t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'error'); return $this->redirect('user.pass'); } - elseif ($user->isAuthenticated() && ($timestamp >= $user->getLastLoginTime()) && ($timestamp <= $current) && Crypt::hashEquals($hash, user_pass_rehash($user, $timestamp))) { - user_login_finalize($user); - $this->logger->notice('User %name used one-time login link at time %timestamp.', ['%name' => $user->getDisplayName(), '%timestamp' => $timestamp]); - drupal_set_message($this->t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.')); - // Let the user's password be changed without the current password - // check. - $token = Crypt::randomBytesBase64(55); - $_SESSION['pass_reset_' . $user->id()] = $token; - return $this->redirect( - 'entity.user.edit_form', - ['user' => $user->id()], - [ - 'query' => ['pass-reset-token' => $token], - 'absolute' => TRUE, - ] - ); - } + + user_login_finalize($user); + $this->logger->notice('User %name used one-time login link at time %timestamp.', ['%name' => $user->getDisplayName(), '%timestamp' => $timestamp]); + drupal_set_message($this->t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.')); + // Let the user's password be changed without the current password + // check. + $token = Crypt::randomBytesBase64(55); + $_SESSION['pass_reset_' . $user->id()] = $token; + return $this->redirect( + 'entity.user.edit_form', + ['user' => $user->id()], + [ + 'query' => ['pass-reset-token' => $token], + 'absolute' => TRUE, + ] + ); + } - drupal_set_message($this->t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'error'); - return $this->redirect('user.pass'); + /** + * Validate a password reset request. + * + * @param int $uid + * User ID of the user requesting reset. + * @param int $timestamp + * The current timestamp. + * @param string $hash + * Login link hash. + * + * @return bool + * Returns TRUE if the password reset request is valid. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * If $uid is for a blocked user or invalid user ID. + */ + protected function validPassResetHash($uid, $timestamp, $hash) { + /** @var \Drupal\user\UserInterface $user */ + $user = $this->userStorage->load($uid); + $current = \Drupal::time()->getRequestTime(); + + if ($user->isAuthenticated() && ($timestamp >= $user->getLastLoginTime()) && ($timestamp <= $current) && Crypt::hashEquals($hash, user_pass_rehash($user, $timestamp)) && $user->isActive()) { + return TRUE; + } + return FALSE; } /** diff --git a/core/modules/user/src/Tests/UserPasswordResetTest.php b/core/modules/user/src/Tests/UserPasswordResetTest.php index 528d38b..cdf6007 100644 --- a/core/modules/user/src/Tests/UserPasswordResetTest.php +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php @@ -135,7 +135,7 @@ public function testUserPasswordReset() { // Log out, and try to log in again using the same one-time link. $this->drupalLogout(); $this->drupalGet($resetURL); - $this->drupalPostForm(NULL, NULL, t('Log in')); + $this->assertUrl(Url::fromRoute('user.pass')); $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.'); // Request a new password again, this time using the email address. @@ -160,15 +160,15 @@ public function testUserPasswordReset() { $bogus_timestamp = REQUEST_TIME - $timeout - 60; $_uid = $this->account->id(); $this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account, $bogus_timestamp)); - $this->drupalPostForm(NULL, NULL, t('Log in')); - $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.'); + $this->assertUrl(Url::fromRoute('user.pass')); + $this->assertText(t('You have tried to use a one-time login link that has expired.'), 'Expired password reset request rejected.'); // Create a user, block the account, and verify that a login link is denied. $timestamp = REQUEST_TIME - 1; $blocked_account = $this->drupalCreateUser()->block(); $blocked_account->save(); $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp)); - $this->assertResponse(403); + $this->assertText(t('The one-time login link you clicked is invalid.'), 'Blocked password reset request rejected.'); // Verify a blocked user can not request a new password. $this->drupalGet('user/password'); @@ -187,7 +187,7 @@ public function testUserPasswordReset() { $this->account->setEmail("1" . $this->account->getEmail()); $this->account->save(); $this->drupalGet($old_email_reset_link); - $this->drupalPostForm(NULL, NULL, t('Log in')); + $this->assertUrl(Url::fromRoute('user.pass')); $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.'); // Verify a password reset link will automatically log a user when /login is @@ -207,11 +207,11 @@ public function testUserPasswordReset() { $blocked_account = $this->drupalCreateUser()->block(); $blocked_account->save(); $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login'); - $this->assertResponse(403); + $this->assertText(t('The one-time login link you clicked is invalid.'), 'One-time link for blocked account is not valid.'); $blocked_account->delete(); $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login'); - $this->assertResponse(403); + $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link for deleted account is not valid.'); } /**